From 39a57164da56ddf1eee1a683f79c864ecd35af47 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Mon, 27 Apr 2026 14:02:40 +0400 Subject: [PATCH 001/490] Revert #38256 partially (AutoValueSubclassLeaked) --- .../org/apache/beam/gradle/BeamModulePlugin.groovy | 1 + .../sdk/io/gcp/bigquery/BigQueryIOTranslation.java | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 005a8b587804..9b4b83e1ebaf 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -1532,6 +1532,7 @@ class BeamModulePlugin implements Plugin { def disabledChecks = [ // TODO(https://github.com/apache/beam/issues/20955): Enable errorprone checks "AutoValueImmutableFields", + "AutoValueSubclassLeaked", "ComparableType", "DoNotMockAutoValue", "EmptyBlockTag", diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java index e5dd2b540019..acc71df14aaf 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java @@ -217,7 +217,7 @@ public TypedRead fromConfigRow(Row configRow, PipelineOptions options) { (updateCompatibilityBeamVersion != null) ? updateCompatibilityBeamVersion : "2.53.0"; try { - BigQueryIO.TypedRead.Builder builder = BigQueryIO.TypedRead.builder(); + BigQueryIO.TypedRead.Builder builder = new AutoValue_BigQueryIO_TypedRead.Builder<>(); String jsonTableRef = configRow.getString("json_table_ref"); if (jsonTableRef != null) { @@ -378,9 +378,7 @@ public static class ReadRegistrar implements TransformPayloadTranslatorRegistrar public Map, ? extends TransformPayloadTranslator> getTransformPayloadTranslators() { return ImmutableMap., TransformPayloadTranslator>builder() - .put( - BigQueryIO.read(BigQueryIO.TableRowParser.INSTANCE).getClass(), - new BigQueryIOReadTranslator()) + .put(AutoValue_BigQueryIO_TypedRead.class, new BigQueryIOReadTranslator()) .build(); } } @@ -617,7 +615,7 @@ public Write fromConfigRow(Row configRow, PipelineOptions options) { (updateCompatibilityBeamVersion != null) ? updateCompatibilityBeamVersion : "2.53.0"; try { - BigQueryIO.Write.Builder builder = BigQueryIO.Write.builder(); + BigQueryIO.Write.Builder builder = new AutoValue_BigQueryIO_Write.Builder<>(); String jsonTableRef = configRow.getString("json_table_ref"); if (jsonTableRef != null) { @@ -930,7 +928,7 @@ public static class WriteRegistrar implements TransformPayloadTranslatorRegistra public Map, ? extends TransformPayloadTranslator> getTransformPayloadTranslators() { return ImmutableMap., TransformPayloadTranslator>builder() - .put(BigQueryIO.write().getClass(), new BigQueryIOWriteTranslator()) + .put(AutoValue_BigQueryIO_Write.class, new BigQueryIOWriteTranslator()) .build(); } } From 8fdcc3c3ab26e9b6621e5da44fb243fd3f031975 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Tue, 28 Apr 2026 16:32:08 +0400 Subject: [PATCH 002/490] Supress warnings --- .../main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 1 - .../apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 9b4b83e1ebaf..005a8b587804 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -1532,7 +1532,6 @@ class BeamModulePlugin implements Plugin { def disabledChecks = [ // TODO(https://github.com/apache/beam/issues/20955): Enable errorprone checks "AutoValueImmutableFields", - "AutoValueSubclassLeaked", "ComparableType", "DoNotMockAutoValue", "EmptyBlockTag", diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java index acc71df14aaf..dd59939726bf 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java @@ -73,7 +73,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@SuppressWarnings({"rawtypes", "nullness"}) +@SuppressWarnings({"rawtypes", "nullness", "AutoValueSubclassLeaked"}) public class BigQueryIOTranslation { private static final Logger LOG = LoggerFactory.getLogger(BigQueryIOTranslation.class); From 7964830adbc3915d4ca47a749927655235c244a3 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 28 Apr 2026 10:58:18 -0400 Subject: [PATCH 003/490] update beam python container (#38298) --- sdks/python/apache_beam/runners/dataflow/internal/names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/names.py b/sdks/python/apache_beam/runners/dataflow/internal/names.py index b7443502a972..6ecd6b1769b3 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/names.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/names.py @@ -35,6 +35,6 @@ # Update this tag whenever there is a change that # requires changes to SDK harness container or SDK harness launcher. -BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260402' +BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260427' DATAFLOW_CONTAINER_IMAGE_REPOSITORY = 'gcr.io/cloud-dataflow/v1beta3' From 277249c3aaac3c55c4c9f320fb380fb1038913f7 Mon Sep 17 00:00:00 2001 From: Elia LIU Date: Wed, 29 Apr 2026 01:16:23 +1000 Subject: [PATCH 004/490] feat(ml): add stateless bundle-local size-aware batching and benchmark (#37532) * feat(ml): add stateless bundle-local size-aware batching and benchmark * fix(ml): improve test coverage for SortAndBatchElements - Exclude *_benchmark.py from codecov (standalone scripts, not production code) - Remove redundant validation from internal DoFn classes (already validated by PTransform) - Add direct in-process unit tests for DoFn internals to capture coverage (FnApiRunner runs DoFns in separate process, invisible to coverage tools) Co-Authored-By: Claude Opus 4.6 * Address PR review: clarify benchmark comment and warn on len() fallback Reframe benchmark docstring to clarify that sorting combined with weight-based splitting drives the improvement. Move default element size fallback into DoFn instances with a one-time warning when len() is unsupported, so users know to provide a custom element_size_fn. * Migrate JupyterLab sidepanel extension to prebuilt package distribution Replace deprecated jupyter labextension install/link workflow with pip-installable prebuilt extension for JupyterLab 4+ compatibility. - Add install.json for prebuilt extension discovery metadata - Add style/index.js CSS entry point and styleModule field in package.json - Include js in package.json files glob so style/index.js is published - Add Extensions and Extensions :: Prebuilt classifiers to pyproject.toml - Add missing src/yaml/* to tsconfig.json includes - Remove deprecated labextension install/link/build instructions from READMEs - Replace ipywidgets labextension install with pip install in Interactive README * Use real Beam pipelines in sort-and-batch benchmark * Address all review comments - Reuse _WindowAwareBatchingDoFn._MAX_LIVE_WINDOWS instead of keeping a separate hard-coded limit in SortAndBatchElements. - Drop the padding-efficiency unit test that compared incongruent batching strategies and keep the transform tests focused on deterministic behavior. - Align benchmark typing with modern Python style by using collections.abc imports and native built-in generics. - Make the sorted-order test clearer by naming the expected batch contents explicitly. --------- Co-authored-by: Claude Opus 4.6 --- .github/codecov.yml | 1 + .../apache_beam/runners/interactive/README.md | 15 +- .../README.md | 52 +- .../install.json | 5 + .../package.json | 3 +- .../pyproject.toml | 2 + .../style/index.js | 13 + .../tsconfig.json | 1 + .../benchmarks/sort_and_batch_benchmark.py | 655 ++++++++++++++++++ sdks/python/apache_beam/transforms/util.py | 280 ++++++++ .../apache_beam/transforms/util_test.py | 335 +++++++++ 11 files changed, 1308 insertions(+), 54 deletions(-) create mode 100644 sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/install.json create mode 100644 sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.js create mode 100644 sdks/python/apache_beam/testing/benchmarks/sort_and_batch_benchmark.py diff --git a/.github/codecov.yml b/.github/codecov.yml index 0936f392ccef..5d0eaccf22da 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -73,6 +73,7 @@ ignore: - "**/*_microbenchmark.py" - "sdks/go/pkg/beam/register/register.go" - "sdks/python/apache_beam/testing/benchmarks/nexmark/**" + - "**/*_benchmark.py" - "sdks/python/apache_beam/examples/**" # See https://docs.codecov.com/docs/flags for options. diff --git a/sdks/python/apache_beam/runners/interactive/README.md b/sdks/python/apache_beam/runners/interactive/README.md index ff6c57a94e61..f95b2765c3fa 100644 --- a/sdks/python/apache_beam/runners/interactive/README.md +++ b/sdks/python/apache_beam/runners/interactive/README.md @@ -244,23 +244,10 @@ a quick reference). For a more general and complete getting started guide, see jupyter kernelspec list ``` -* Extend JupyterLab through labextension. **Note**: labextension is different from nbextension - from pre-lab jupyter notebooks. - - All jupyter labextensions need nodejs - - ```bash - # Homebrew users do - brew install node - # Or Conda users do - conda install -c conda-forge nodejs - ``` - - Enable ipywidgets +* Install ipywidgets (includes the JupyterLab widget manager as a prebuilt extension): ```bash pip install ipywidgets - jupyter labextension install @jupyter-widgets/jupyterlab-manager ``` ### Start the notebook diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/README.md b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/README.md index 83fddf491f68..4c0baf3b2d53 100644 --- a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/README.md +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/README.md @@ -31,41 +31,22 @@ Includes two different side panels: ## Installation -There are two ways to install the extension: - -### 1. Via pip (recommended) - -The extension is now available as a Python package on PyPI. You can install it with: +This extension is distributed as a prebuilt Python package. Install it with pip: ```bash pip install apache-beam-jupyterlab-sidepanel ``` -After installation, rebuild JupyterLab to activate the extension: - -```bash -jupyter lab clean -jupyter lab build -``` - -Then restart JupyterLab. The side panels will be available automatically. +Then restart JupyterLab. The side panels will be available automatically — no +`jupyter lab build` step is needed. - -### 2. Via JupyterLab Extension Manager (legacy, will be deprecated soon) +You can verify the extension is installed: ```bash -jupyter labextension install apache-beam-jupyterlab-sidepanel +jupyter labextension list ``` -This installs the extension using JupyterLab's legacy extension system. - ---- - -## Notes - -- Pip installation is now the preferred method as it handles Python packaging and JupyterLab extension registration seamlessly. -- After any upgrade or reinstallation, always rebuild JupyterLab to ensure the extension is activated. -- For detailed usage and development, refer to the source code and issues on [GitHub](https://github.com/apache/beam). +The extension should appear under the **prebuilt extensions** section. --- @@ -90,15 +71,12 @@ The `jlpm` command is JupyterLab's pinned version of # Install dependencies jlpm -# Build Typescript source -jlpm build -# Link your development version of the extension with JupyterLab -jupyter labextension link . -# Rebuild Typescript source after making changes -jlpm build -# Rebuild JupyterLab after making any changes -jupyter lab build +# Install the extension in editable mode (runs an initial JS build) +pip install -e . + +# Verify installation +jupyter labextension list ``` You can watch the source directory and run JupyterLab in watch mode to watch for changes in the extension's source and automatically rebuild the extension and application. @@ -110,7 +88,7 @@ jlpm watch jupyter lab --watch ``` -Now every change will be built locally and bundled into JupyterLab. Be sure to refresh your browser page after saving file changes to reload the extension (note: you'll need to wait for webpack to finish, which can take 10s+ at times). +Now every change will be built locally and bundled into JupyterLab. Be sure to refresh your browser page after saving file changes to reload the extension (note: you'll need to wait for the build to finish, which can take 10s+ at times). ### Test @@ -214,9 +192,5 @@ $PREFIX/share/jupyter/labextensions/apache-beam-jupyterlab-sidepanel/ ### Uninstall ```bash -jupyter labextension uninstall apache-beam-jupyterlab-sidepanel -``` -or -```bash -pip uninstall apache-beam-jupyterlab-sidepanel +pip uninstall apache_beam_jupyterlab_sidepanel ``` diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/install.json b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/install.json new file mode 100644 index 000000000000..3ef6567c6a81 --- /dev/null +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/install.json @@ -0,0 +1,5 @@ +{ + "packageManager": "python", + "packageName": "apache_beam_jupyterlab_sidepanel", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package apache_beam_jupyterlab_sidepanel" +} diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json index eef3fcaa80f4..6bca80350ff7 100644 --- a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json @@ -15,7 +15,7 @@ "author": "apache-beam", "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", - "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -100,6 +100,7 @@ "style/*.css", "style/index.js" ], + "styleModule": "style/index.js", "jupyterlab": { "extension": true, "outputDir": "apache_beam_jupyterlab_sidepanel/labextension" diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/pyproject.toml b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/pyproject.toml index 6831535a2c1e..a28fd40b2ca6 100644 --- a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/pyproject.toml +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/pyproject.toml @@ -33,6 +33,8 @@ classifiers = [ "Framework :: Jupyter", "Framework :: Jupyter :: JupyterLab", "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", ] diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.js b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.js new file mode 100644 index 000000000000..b533d5a9c6d5 --- /dev/null +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.js @@ -0,0 +1,13 @@ +// Licensed under the Apache License, Version 2.0 (the 'License'); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import './index.css'; diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/tsconfig.json b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/tsconfig.json index c684cabf44a3..058bf17e1861 100644 --- a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/tsconfig.json +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/tsconfig.json @@ -29,6 +29,7 @@ "src/common/*", "src/kernel/*", "src/inspector/*", + "src/yaml/*", "src/__tests__/**/*" ] } diff --git a/sdks/python/apache_beam/testing/benchmarks/sort_and_batch_benchmark.py b/sdks/python/apache_beam/testing/benchmarks/sort_and_batch_benchmark.py new file mode 100644 index 000000000000..695c4c2c995c --- /dev/null +++ b/sdks/python/apache_beam/testing/benchmarks/sort_and_batch_benchmark.py @@ -0,0 +1,655 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Benchmark: BatchElements vs SortAndBatchElements on real Beam pipelines. + +Compares two batching strategies for variable-length inference workloads by +running the actual Beam transforms under DirectRunner: + +- Baseline (BatchElements): fixed-count batching by setting + ``min_batch_size == max_batch_size``. +- Stateless (SortAndBatchElements): sorts elements by size within each runner + bundle, then splits batches using ``max_batch_weight``. + +The benchmark materializes per-batch summaries through a temporary Beam sink and +analyzes them after the pipeline completes. This keeps the benchmark on the +normal Beam execution path rather than relying on InteractiveRunner-specific +result materialization or local side effects. + +Bundle boundaries are runner-defined. As a result, these measurements are meant +to compare the actual DirectRunner behavior of the two transforms rather than a +synthetic, user-configurable bundle model. + +Padding ratio:: + + padding_ratio = sum(max_len_in_batch * batch_size) / sum(actual_lengths) + Lower is better. 1.0 = no padding waste. + +Methodology: + +- N=20 independent trials per condition (3 warmup trials excluded). +- Same input corpus (seed=42) for A/B comparison. +- DirectRunner with in-memory execution and one worker for reproducibility. +- Percentile method: linear interpolation between adjacent ranks + (equivalent to numpy.percentile with method='linear'). + For N=20 trials: P50 interpolates ranks 10-11 (0-indexed 9-10), + P95 interpolates ranks 19-20 (0-indexed 18-19), + P99 interpolates near rank 20 (0-indexed 18.81). +- Reports median [IQR] and P95 for each metric. +- Inference model: latency = batch_size * (max_seq_len / 50)^1.5 ms + (simulates downstream transformer-like scaling). + +Run:: + + python3 -m apache_beam.testing.benchmarks.sort_and_batch_benchmark +""" + +import glob +import json +import math +import os +import random +import statistics +import tempfile +import time +from collections.abc import Sequence +from typing import Any + +import apache_beam as beam +from apache_beam.options.pipeline_options import PipelineOptions +from apache_beam.transforms import util + +# --------------------------------------------------------------------------- +# Data generators +# --------------------------------------------------------------------------- + + +def generate_highly_skewed_data( + num_elements: int, + min_length: int = 1, + max_length: int = 500, + seed: int = 42) -> list[str]: + """Pareto(alpha=1.2) -- most short, few very long.""" + random.seed(seed) + data = [] + for _ in range(num_elements): + length = int(random.paretovariate(1.2) * min_length) + length = min(max(length, min_length), max_length) + data.append('x' * length) + return data + + +def generate_lognormal_data( + num_elements: int, + mean_length: int = 50, + std_factor: float = 0.8, + min_length: int = 1, + max_length: int = 500, + seed: int = 42) -> list[str]: + """Log-normal -- moderate skew, typical NLP.""" + random.seed(seed) + mu = math.log(mean_length) + sigma = std_factor + data = [] + for _ in range(num_elements): + length = int(random.lognormvariate(mu, sigma)) + length = min(max(length, min_length), max_length) + data.append('x' * length) + return data + + +def generate_bimodal_data( + num_elements: int, + mode1_mean: int = 20, + mode2_mean: int = 200, + mode1_ratio: float = 0.7, + min_length: int = 1, + max_length: int = 500, + seed: int = 42) -> list[str]: + """Bimodal -- two distinct length groups.""" + random.seed(seed) + data = [] + for _ in range(num_elements): + if random.random() < mode1_ratio: + length = int(random.gauss(mode1_mean, mode1_mean * 0.3)) + else: + length = int(random.gauss(mode2_mean, mode2_mean * 0.3)) + length = min(max(length, min_length), max_length) + data.append('x' * length) + return data + + +def generate_low_variance_data( + num_elements: int, + mean_length: int = 100, + cv: float = 0.1, + min_length: int = 1, + max_length: int = 500, + seed: int = 42) -> list[str]: + """Low-variance control (CV=10%).""" + random.seed(seed) + std = mean_length * cv + data = [] + for _ in range(num_elements): + length = int(random.gauss(mean_length, std)) + length = min(max(length, min_length), max_length) + data.append('x' * length) + return data + + +# --------------------------------------------------------------------------- +# Real Beam batching +# --------------------------------------------------------------------------- + + +def _direct_runner_options() -> PipelineOptions: + return PipelineOptions([ + '--runner=DirectRunner', + '--direct_running_mode=in_memory', + '--direct_num_workers=1', + ]) + + +def _batch_to_json(batch: list[str]) -> str: + lengths = [len(element) for element in batch] + return json.dumps({ + 'batch_size': len(batch), + 'actual_total_length': sum(lengths), + 'max_len': max(lengths) if lengths else 0, + }) + + +def _read_batch_summaries(output_prefix: str) -> list[dict[str, int]]: + summaries = [] + for path in sorted(glob.glob(f'{output_prefix}*')): + if path.endswith('.crc'): + continue + with open(path, encoding='utf-8') as handle: + for line in handle: + line = line.strip() + if line: + summaries.append(json.loads(line)) + return summaries + + +def _run_batching_pipeline( + strategy: str, data: list[str], max_batch_size: int, + max_batch_weight: int) -> tuple[list[dict[str, int]], float]: + """Runs one Beam pipeline and returns batch summaries plus runtime.""" + with tempfile.TemporaryDirectory(prefix='beam_batch_benchmark_') as temp_dir: + output_prefix = os.path.join(temp_dir, strategy) + pipeline = beam.Pipeline(options=_direct_runner_options()) + batched = pipeline | 'CreateInput' >> beam.Create(data, reshuffle=False) + + if strategy == 'baseline': + batched = batched | 'BatchElements' >> util.BatchElements( + min_batch_size=max_batch_size, max_batch_size=max_batch_size) + elif strategy == 'stateless': + batched = batched | 'SortAndBatchElements' >> util.SortAndBatchElements( + min_batch_size=1, + max_batch_size=max_batch_size, + max_batch_weight=max_batch_weight) + else: + raise ValueError(f'Unknown strategy: {strategy}') + + _ = ( + batched + | 'SerializeBatchSummary' >> beam.Map(_batch_to_json) + | 'WriteBatchSummary' >> beam.io.WriteToText(output_prefix)) + + start = time.perf_counter() + result = pipeline.run() + result.wait_until_finish() + runtime_ms = (time.perf_counter() - start) * 1000 + + return _read_batch_summaries(output_prefix), runtime_ms + + +# --------------------------------------------------------------------------- +# Simulated inference +# --------------------------------------------------------------------------- + + +def simulate_inference_latency( + batch_size: int, max_len: int, base_latency_ms: float = 1.0) -> float: + """Simulate downstream inference: O(batch_size * seq_len^1.5).""" + if not batch_size or not max_len: + return 0.0 + return base_latency_ms * batch_size * (max_len / 50)**1.5 + + +# --------------------------------------------------------------------------- +# Stats helpers +# --------------------------------------------------------------------------- + + +def percentile(data: Sequence[float], p: float) -> float: + """Percentile via linear interpolation between adjacent ranks. + + Equivalent to numpy.percentile(data, p, method='linear'). + For N=20: P50 interpolates ranks 10-11, P95 ranks 19-20, + P99 near rank 20 (fractional index 18.81). + """ + if not data: + return 0.0 + s = sorted(data) + k = (len(s) - 1) * p / 100 + f = int(k) + c = min(f + 1, len(s) - 1) + return s[f] + (k - f) * (s[c] - s[f]) + + +def compute_padding_stats( + batch_summaries: list[dict[str, int]]) -> dict[str, Any]: + """Padding-efficiency statistics for materialized batch summaries.""" + total_actual = sum(s['actual_total_length'] for s in batch_summaries) + total_padded = sum(s['max_len'] * s['batch_size'] for s in batch_summaries) + batch_sizes = [s['batch_size'] for s in batch_summaries if s['batch_size']] + max_lengths = [s['max_len'] for s in batch_summaries if s['batch_size']] + + efficiency = total_actual / total_padded if total_padded else 0.0 + padding_ratio = total_padded / total_actual if total_actual else float('inf') + + return { + 'efficiency': efficiency, + 'padding_ratio': padding_ratio, + 'num_batches': len(batch_summaries), + 'avg_batch_size': statistics.mean(batch_sizes) if batch_sizes else 0, + 'total_actual_length': total_actual, + 'total_padded_length': total_padded, + 'padding_overhead': total_padded - total_actual, + 'batch_size_p50': percentile(batch_sizes, 50) if batch_sizes else 0, + 'batch_size_p95': percentile(batch_sizes, 95) if batch_sizes else 0, + 'batch_size_max': max(batch_sizes) if batch_sizes else 0, + 'max_len_p50': percentile(max_lengths, 50) if max_lengths else 0, + 'max_len_p95': percentile(max_lengths, 95) if max_lengths else 0, + } + + +# --------------------------------------------------------------------------- +# Invariant validation +# --------------------------------------------------------------------------- + + +def validate_invariants( + data: list[str], + baseline_summaries: list[dict[str, int]], + stateless_summaries: list[dict[str, int]]) -> dict[str, Any]: + """Validate element/token counts and batch-size equality.""" + n = len(data) + b_n = sum(s['batch_size'] for s in baseline_summaries) + s_n = sum(s['batch_size'] for s in stateless_summaries) + tok = sum(len(s) for s in data) + b_tok = sum(s['actual_total_length'] for s in baseline_summaries) + s_tok = sum(s['actual_total_length'] for s in stateless_summaries) + + return { + 'input_elements': n, + 'baseline_elements': b_n, + 'stateless_elements': s_n, + 'elements_match': n == b_n == s_n, + 'input_tokens': tok, + 'baseline_tokens': b_tok, + 'stateless_tokens': s_tok, + 'tokens_match': tok == b_tok == s_tok, + 'baseline_num_batches': len(baseline_summaries), + 'stateless_num_batches': len(stateless_summaries), + } + + +# --------------------------------------------------------------------------- +# Performance benchmark (N=20 trials) +# --------------------------------------------------------------------------- + + +def run_performance_benchmark( + data: list[str], + max_batch_size: int, + max_batch_weight: int, + num_trials: int = 20, + warmup_trials: int = 3 +) -> tuple[ + dict[str, Any], + dict[str, Any], + list[dict[str, int]], + list[dict[str, int]], +]: + """Run N=20 trials for baseline and stateless.""" + total_tokens = sum(len(s) for s in data) + + baseline_trials = [] + stateless_trials = [] + baseline_sample_summaries = [] + stateless_sample_summaries = [] + + for trial_idx in range(warmup_trials + num_trials): + is_warmup = trial_idx < warmup_trials + trial_results = {} + + if trial_idx % 2 == 0: + trial_order = ('baseline', 'stateless') + else: + trial_order = ('stateless', 'baseline') + + for strategy in trial_order: + summaries, runtime_ms = _run_batching_pipeline( + strategy, data, max_batch_size, max_batch_weight) + batch_latencies = [ + simulate_inference_latency(s['batch_size'], s['max_len']) + for s in summaries + ] + trial_results[strategy] = { + 'runtime_ms': runtime_ms, + 'inference_ms': sum(batch_latencies), + 'e2e_ms': runtime_ms + sum(batch_latencies), + 'batch_latencies': batch_latencies, + 'num_batches': len(summaries), + 'summaries': summaries, + } + + if not is_warmup: + baseline_trials.append(trial_results['baseline']) + stateless_trials.append(trial_results['stateless']) + if not baseline_sample_summaries: + baseline_sample_summaries = trial_results['baseline']['summaries'] + if not stateless_sample_summaries: + stateless_sample_summaries = trial_results['stateless']['summaries'] + + def _stats(trials): + e2e = [t['e2e_ms'] for t in trials] + tput = [total_tokens / (t['e2e_ms'] / 1000) for t in trials] + runtime = [t['runtime_ms'] for t in trials] + all_lat = [l for t in trials for l in t['batch_latencies']] + return { + 'e2e_median': percentile(e2e, 50), + 'e2e_p25': percentile(e2e, 25), + 'e2e_p75': percentile(e2e, 75), + 'e2e_p95': percentile(e2e, 95), + 'tput_median': percentile(tput, 50), + 'tput_p25': percentile(tput, 25), + 'tput_p75': percentile(tput, 75), + 'tput_p95': percentile(tput, 95), + 'runtime_median': percentile(runtime, 50), + 'runtime_p25': percentile(runtime, 25), + 'runtime_p75': percentile(runtime, 75), + 'runtime_p95': percentile(runtime, 95), + 'batch_lat_p50': percentile(all_lat, 50), + 'batch_lat_p95': percentile(all_lat, 95), + 'batch_lat_p99': percentile(all_lat, 99), + 'inf_p95': percentile(all_lat, 95), + 'num_trials': len(trials), + 'num_batches': trials[0]['num_batches'] if trials else 0, + } + + return ( + _stats(baseline_trials), + _stats(stateless_trials), + baseline_sample_summaries, + stateless_sample_summaries, + ) + + +# --------------------------------------------------------------------------- +# Single benchmark run +# --------------------------------------------------------------------------- + + +def run_benchmark( + num_elements: int = 10000, + min_length: int = 1, + max_length: int = 500, + max_batch_size: int = 32, + max_batch_weight: int = 2000, + distribution: str = 'pareto', + seed: int = 42) -> dict[str, Any]: + """Run baseline vs stateless comparison.""" + generators = { + 'pareto': lambda: generate_highly_skewed_data( + num_elements, min_length, max_length, seed), + 'lognormal': lambda: generate_lognormal_data( + num_elements, 50, 0.8, min_length, max_length, seed), + 'bimodal': lambda: generate_bimodal_data( + num_elements, 20, 200, 0.7, min_length, max_length, seed), + 'low_variance': lambda: generate_low_variance_data( + num_elements, 100, 0.1, min_length, max_length, seed), + } + if distribution not in generators: + raise ValueError(f"Unknown distribution: {distribution}") + + data = generators[distribution]() + lengths = [len(s) for s in data] + + baseline_perf, stateless_perf, baseline_summaries, stateless_summaries = ( + run_performance_benchmark(data, max_batch_size, max_batch_weight)) + baseline_pad = compute_padding_stats(baseline_summaries) + stateless_pad = compute_padding_stats(stateless_summaries) + baseline_pad.update(baseline_perf) + stateless_pad.update(stateless_perf) + + validation = validate_invariants( + data, baseline_summaries, stateless_summaries) + + return { + 'config': { + 'num_elements': num_elements, + 'max_batch_size': max_batch_size, + 'max_batch_weight': max_batch_weight, + 'distribution': distribution, + 'runner': 'DirectRunner', + }, + 'data_stats': { + 'min': min(lengths), + 'max': max(lengths), + 'mean': statistics.mean(lengths), + 'median': statistics.median(lengths), + 'std': statistics.stdev(lengths), + }, + 'baseline': baseline_pad, + 'stateless': stateless_pad, + 'validation': validation, + } + + +# --------------------------------------------------------------------------- +# Printing +# --------------------------------------------------------------------------- + + +def _fmt_iqr(median, p25, p75, unit=''): + return f"{median:.1f} [{p25:.1f}-{p75:.1f}]{unit}" + + +def print_results(results: dict[str, Any]) -> None: + cfg = results['config'] + ds = results['data_stats'] + bl = results['baseline'] + st = results['stateless'] + val = results['validation'] + + print("=" * 80) + print( + f"Distribution: {cfg['distribution']} | " + f"N={cfg['num_elements']} | " + f"runner={cfg['runner']} | " + f"max_batch_size={cfg['max_batch_size']} | " + f"max_batch_weight={cfg['max_batch_weight']}") + print( + f"Input lengths: min={ds['min']} max={ds['max']} " + f"mean={ds['mean']:.1f} median={ds['median']:.0f} std={ds['std']:.1f}") + print("-" * 80) + + def _arm(label, s): + print(f"\n {label}:") + print(f" Num batches: {s['num_batches']}") + print(f" Padding ratio: {s['padding_ratio']:.2f}x") + print(" ") + print(" Throughput (Ktok/s):") + med = s['tput_median'] / 1000 + p25 = s['tput_p25'] / 1000 + p75 = s['tput_p75'] / 1000 + print(f" Median [IQR]: {med:.1f}" + f" [{p25:.1f}-{p75:.1f}]") + print(f" P95: {s['tput_p95']/1000:.1f}") + print(" ") + print(" E2E latency (ms):") + print( + f" Median [IQR]: {s['e2e_median']:.1f}" + f" [{s['e2e_p25']:.1f}-{s['e2e_p75']:.1f}]") + print(f" P95: {s['e2e_p95']:.1f}") + print(" ") + print(" Pipeline runtime (ms):") + print( + f" Median [IQR]:" + f" {s['runtime_median']:.2f}" + f" [{s['runtime_p25']:.2f}" + f"-{s['runtime_p75']:.2f}]") + print(f" P95: {s['runtime_p95']:.2f}") + print(" ") + print(" Batch latency (ms):") + print(f" P50: {s['batch_lat_p50']:.1f}") + print(f" P95: {s['batch_lat_p95']:.1f}") + print(f" P99: {s['batch_lat_p99']:.1f}") + + _arm("Baseline (BatchElements)", bl) + _arm("Stateless (SortAndBatchElements w/ weight-based splitting)", st) + + # Explicit arrows so direction is unambiguous. + # down arrow = value decreased (good for latency/padding) + # up arrow = value increased (good for throughput) + def _delta_lower(base, new): + """For metrics where lower is better (latency, padding).""" + if base == 0: + return 'N/A' + pct = (base - new) / base * 100 + arrow = '\u2193' if pct > 0 else '\u2191' + return f"{arrow}{abs(pct):.1f}%" + + def _delta_higher(base, new): + """For metrics where higher is better (throughput).""" + if base == 0: + return 'N/A' + pct = (new - base) / base * 100 + arrow = '\u2191' if pct > 0 else '\u2193' + return f"{arrow}{abs(pct):.1f}%" + + print(f"\n {'_' * 76}") + print(" DELTA (Baseline -> Stateless):") + + def _line(label, bv, sv, delta_fn, fmt='.1f', unit=''): + d = delta_fn(bv, sv) + print(f" {label}: {bv:{fmt}}{unit}" + f" -> {sv:{fmt}}{unit} ({d})") + + bl_tmed = bl['tput_median'] / 1000 + st_tmed = st['tput_median'] / 1000 + bl_tp95 = bl['tput_p95'] / 1000 + st_tp95 = st['tput_p95'] / 1000 + + _line( + 'Padding ratio ', + bl['padding_ratio'], + st['padding_ratio'], + _delta_lower, + fmt='.2f', + unit='x') + _line('Throughput median', bl_tmed, st_tmed, _delta_higher, unit=' Ktok/s') + _line('Throughput p95 ', bl_tp95, st_tp95, _delta_higher, unit=' Ktok/s') + _line( + 'E2E latency med ', + bl['e2e_median'], + st['e2e_median'], + _delta_lower, + unit=' ms') + _line( + 'E2E latency p95 ', + bl['e2e_p95'], + st['e2e_p95'], + _delta_lower, + unit=' ms') + _line( + 'Pipeline runtime ', + bl['runtime_median'], + st['runtime_median'], + _delta_lower, + unit=' ms') + _line( + 'Batch lat p95 ', + bl['batch_lat_p95'], + st['batch_lat_p95'], + _delta_lower, + unit=' ms') + _line( + 'Batch lat p99 ', + bl['batch_lat_p99'], + st['batch_lat_p99'], + _delta_lower, + unit=' ms') + + # Invariants + e_ok = "Y" if val['elements_match'] else "X" + t_ok = "Y" if val['tokens_match'] else "X" + b_nb = val['baseline_num_batches'] + s_nb = val['stateless_num_batches'] + print( + f"\n Invariants: elements {e_ok} tokens {t_ok}" + f" (baseline {b_nb} -> stateless {s_nb}" + f" batches)") + print("=" * 80) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + print("=" * 80) + print("BASELINE (BatchElements) vs STATELESS (SortAndBatchElements)") + print("=" * 80) + print() + print("Experiment design:") + print(" A = Baseline : BatchElements with min=max=32") + print(" B = Stateless : SortAndBatchElements with max_batch_weight=2000") + print(" (sort within runner bundle, then split by weight)") + print() + print("Implementation notes:") + print(" - Runs beam.Create(...) pipelines on DirectRunner") + print(" - Materializes per-batch summaries through a temporary text sink") + print(" - Uses runner-defined bundle boundaries rather than a synthetic") + print(" bundle_size knob") + print() + print("Methodology:") + print(" - N=20 trials, 3 warmup excluded") + print(" - DirectRunner, in_memory mode, single worker") + print(" - Percentiles: linear interpolation (= numpy default)") + print(" - Same seed=42 for both arms") + print(" - Inference model: latency = batch_size * (max_seq_len/50)^1.5 ms") + print() + + dist = 'pareto' + print(f"\nRunning: {dist}...") + r = run_benchmark( + num_elements=10000, + max_batch_size=32, + max_batch_weight=2000, + distribution=dist, + seed=42) + print_results(r) + + +if __name__ == '__main__': + main() diff --git a/sdks/python/apache_beam/transforms/util.py b/sdks/python/apache_beam/transforms/util.py index f09e14f06ee6..4e16b805184a 100644 --- a/sdks/python/apache_beam/transforms/util.py +++ b/sdks/python/apache_beam/transforms/util.py @@ -106,6 +106,7 @@ 'RemoveDuplicates', 'Reshuffle', 'Secret', + 'SortAndBatchElements', 'ToString', 'Tee', 'Values', @@ -1376,6 +1377,285 @@ def expand(self, pcoll): self._batch_size_estimator, self._element_size_fn)) +class _SortAndBatchElementsDoFn(DoFn): + """DoFn that buffers, sorts by element size, and batches elements. + + This DoFn is used internally by ``SortAndBatchElements`` for + PCollections with the default (global) window. It accumulates all + elements in the current bundle, sorts them by size in ascending order, + and emits optimally-sized batches on ``finish_bundle``. + + Args: + min_batch_size: The minimum number of elements per batch. Must be >= 1. + max_batch_size: The maximum number of elements per batch. + Must be >= ``min_batch_size``. + max_batch_weight: The maximum total weight of elements in a batch, + where weight is computed by ``element_size_fn``. Must be >= 1. + element_size_fn: An optional callable mapping an element to its integer + size/weight. + """ + def __init__( + self, + min_batch_size: int, + max_batch_size: int, + max_batch_weight: int, + element_size_fn: Optional[Callable[[Any], int]]): + self._min_batch_size = min_batch_size + self._max_batch_size = max_batch_size + self._max_batch_weight = max_batch_weight + self._element_size_fn = element_size_fn or self._default_element_size + self._has_warned_type_error = False + self._buffer = [] + + def _default_element_size(self, element): + try: + return len(element) + except TypeError: + if not self._has_warned_type_error: + _LOGGER.warning( + 'Element of type %s does not support len(). Falling back to ' + 'size 1. Consider providing a custom element_size_fn to ' + 'SortAndBatchElements for meaningful size-based batching.', + type(element).__name__) + self._has_warned_type_error = True + return 1 + + def start_bundle(self): + self._buffer = [] + + def process(self, element): + self._buffer.append(element) + + def finish_bundle(self): + if not self._buffer: + return + + # Sort elements by size (ascending) for optimal batching + # Elements of similar sizes will be grouped together + sorted_elements = sorted(self._buffer, key=self._element_size_fn) + + batch = [] + batch_weight = 0 + + for element in sorted_elements: + element_size = self._element_size_fn(element) + + # Check if adding this element would exceed limits + would_exceed_count = len(batch) >= self._max_batch_size + would_exceed_weight = ( + batch_weight + element_size >= self._max_batch_weight and batch) + + if would_exceed_count or would_exceed_weight: + # Emit current batch + yield window.GlobalWindows.windowed_value_at_end_of_window(batch) + batch = [] + batch_weight = 0 + + batch.append(element) + batch_weight += element_size + + # Emit remaining elements + if batch: + yield window.GlobalWindows.windowed_value_at_end_of_window(batch) + + self._buffer = None + + +class _WindowAwareSortAndBatchElementsDoFn(DoFn): + """DoFn that buffers, sorts by element size, and batches elements per window. + + This DoFn is used internally by ``SortAndBatchElements`` for + PCollections with non-default (e.g. fixed, sliding, or session) windows. + Elements are buffered per window and each window is flushed independently. + To prevent a single bundle from retaining too many per-window buffers at + once, when the number of live windows exceeds ``_MAX_LIVE_WINDOWS`` the + largest window buffer is flushed early. This DoFn reuses + ``_WindowAwareBatchingDoFn._MAX_LIVE_WINDOWS`` so it follows the same + existing window-aware batching behavior already used in this module. + + Args: + min_batch_size: The minimum number of elements per batch. Must be >= 1. + max_batch_size: The maximum number of elements per batch. + Must be >= ``min_batch_size``. + max_batch_weight: The maximum total weight of elements in a batch, + where weight is computed by ``element_size_fn``. Must be >= 1. + element_size_fn: An optional callable mapping an element to its integer + size/weight. + """ + + _MAX_LIVE_WINDOWS = _WindowAwareBatchingDoFn._MAX_LIVE_WINDOWS + + def __init__( + self, + min_batch_size: int, + max_batch_size: int, + max_batch_weight: int, + element_size_fn: Optional[Callable[[Any], int]]): + self._min_batch_size = min_batch_size + self._max_batch_size = max_batch_size + self._max_batch_weight = max_batch_weight + self._element_size_fn = element_size_fn or self._default_element_size + self._has_warned_type_error = False + self._buffers = collections.defaultdict(list) + + def _default_element_size(self, element): + try: + return len(element) + except TypeError: + if not self._has_warned_type_error: + _LOGGER.warning( + 'Element of type %s does not support len(). Falling back to ' + 'size 1. Consider providing a custom element_size_fn to ' + 'SortAndBatchElements for meaningful size-based batching.', + type(element).__name__) + self._has_warned_type_error = True + return 1 + + def start_bundle(self): + self._buffers = collections.defaultdict(list) + + def process(self, element, window=DoFn.WindowParam): + self._buffers[window].append(element) + + # If we have too many live windows, flush the largest one + if len(self._buffers) > self._MAX_LIVE_WINDOWS: + largest_window = max( + self._buffers.keys(), key=lambda w: len(self._buffers[w])) + yield from self._flush_window(largest_window) + + def _flush_window(self, win): + """Flush all elements for a given window.""" + buffer = self._buffers.pop(win, []) + if not buffer: + return + + # Sort elements by size (ascending) + sorted_elements = sorted(buffer, key=self._element_size_fn) + + batch = [] + batch_weight = 0 + + for element in sorted_elements: + element_size = self._element_size_fn(element) + + would_exceed_count = len(batch) >= self._max_batch_size + would_exceed_weight = ( + batch_weight + element_size >= self._max_batch_weight and batch) + + if would_exceed_count or would_exceed_weight: + yield windowed_value.WindowedValue(batch, win.max_timestamp(), (win, )) + batch = [] + batch_weight = 0 + + batch.append(element) + batch_weight += element_size + + if batch: + yield windowed_value.WindowedValue(batch, win.max_timestamp(), (win, )) + + def finish_bundle(self): + for win in list(self._buffers.keys()): + yield from self._flush_window(win) + self._buffers = None + + +@typehints.with_input_types(T) +@typehints.with_output_types(list[T]) +class SortAndBatchElements(PTransform): + """A Transform that sorts elements by size before batching. + + This transform is designed to optimize batch processing by grouping elements + of similar sizes together. This is particularly useful for ML inference + workloads where input sequences of varying lengths need to be padded to the + maximum length in the batch - by sorting elements by size before batching, + padding overhead is minimized. + + The transform consumes a PCollection of element type T and produces a + PCollection of element type list[T], where elements within each batch are + sorted by their size (as determined by element_size_fn). + + Elements are batched per-window and batches emitted in the window + corresponding to its contents. Each batch is emitted with a timestamp at + the end of their window. + + Unlike BatchElements which emits batches as soon as size limits are reached, + SortAndBatchElements buffers all elements in a bundle, sorts them by size, + and then creates optimally-sized batches. This trade-off of increased memory + usage for better batch homogeneity can significantly reduce padding overhead. + + Args: + min_batch_size: The minimum number of elements in a batch. Must be >= 1. + max_batch_size: The maximum number of elements in a batch. + Must be >= min_batch_size. + max_batch_weight: The maximum total weight of elements in a batch, + where weight is computed by element_size_fn. Must be >= 1. + element_size_fn: (optional) A function mapping an element to its + size/weight. + If not provided, defaults to trying len(element) and falling back to 1 + if the element doesn't support len(). This default allows sorting to + work for common types like strings, lists, and arrays. + + Example usage:: + + # Batch strings by total character count + strings = ['a', 'bb', 'ccc', 'dddd', 'eeeee'] + batched = strings | SortAndBatchElements( + min_batch_size=1, + max_batch_size=3, + max_batch_weight=10) + # Possible output: [['a', 'bb', 'ccc'], ['dddd', 'eeeee']] + # Elements are sorted by length and batched optimally + + # Batch with custom size function + data = [{'text': 'short'}, {'text': 'medium text'}, + {'text': 'long text here'}] + batched = data | SortAndBatchElements( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=lambda x: len(x['text'])) + """ + def __init__( + self, + min_batch_size: int, + max_batch_size: int, + max_batch_weight: int, + element_size_fn: Optional[Callable[[Any], int]] = None): + if min_batch_size < 1: + raise ValueError(f'min_batch_size must be >= 1, got {min_batch_size}') + if max_batch_size < min_batch_size: + raise ValueError( + f'max_batch_size ({max_batch_size}) must be >= ' + f'min_batch_size ({min_batch_size})') + if max_batch_weight < 1: + raise ValueError(f'max_batch_weight must be >= 1, got {max_batch_weight}') + if element_size_fn is not None and not callable(element_size_fn): + raise TypeError('element_size_fn must be callable') + + self._min_batch_size = min_batch_size + self._max_batch_size = max_batch_size + self._max_batch_weight = max_batch_weight + + # None means the DoFn will use its own _default_element_size method, + # which tries len() and warns once on TypeError before falling back to 1. + self._element_size_fn = element_size_fn + + def expand(self, pcoll): + if pcoll.windowing.is_default(): + return pcoll | ParDo( + _SortAndBatchElementsDoFn( + self._min_batch_size, + self._max_batch_size, + self._max_batch_weight, + self._element_size_fn)) + return pcoll | ParDo( + _WindowAwareSortAndBatchElementsDoFn( + self._min_batch_size, + self._max_batch_size, + self._max_batch_weight, + self._element_size_fn)) + + class _IdentityWindowFn(NonMergingWindowFn): """Windowing function that preserves existing windows. diff --git a/sdks/python/apache_beam/transforms/util_test.py b/sdks/python/apache_beam/transforms/util_test.py index 47c3ee544520..a965ff33d829 100644 --- a/sdks/python/apache_beam/transforms/util_test.py +++ b/sdks/python/apache_beam/transforms/util_test.py @@ -1259,6 +1259,341 @@ def check_batch_homogeneity(batch): assert_that(checks, is_not_empty()) +class SortAndBatchElementsTest(unittest.TestCase): + """Tests for SortAndBatchElements transform.""" + def test_elements_are_sorted_by_size(self): + """Test that elements are sorted by size within batches.""" + with TestPipeline() as p: + # Create elements with varying sizes + data = ['aaaaa', 'bb', 'cccc', 'a', 'ddd'] + expected = [['a', 'bb', 'ddd', 'cccc', 'aaaaa']] + res = ( + p + | beam.Create(data, reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=5, max_batch_weight=100)) + # All elements fit in one batch, so the expected order is explicit. + assert_that(res, equal_to(expected)) + + def test_batch_respects_max_batch_size(self): + """Test that batches do not exceed max_batch_size.""" + with TestPipeline() as p: + res = ( + p + | beam.Create(['a'] * 10, reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=3, max_batch_weight=100) + | beam.Map(len)) + assert_that(res, equal_to([3, 3, 3, 1])) + + def test_batch_respects_max_batch_weight(self): + """Test that batches do not exceed max_batch_weight.""" + with TestPipeline() as p: + # Each element has size 5, max_batch_weight is 12 + # So we can fit at most 2 elements per batch + data = ['aaaaa', 'bbbbb', 'ccccc', 'ddddd'] + res = ( + p + | beam.Create(data, reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=10, max_batch_weight=12) + | beam.Map(len)) + assert_that(res, equal_to([2, 2])) + + def test_default_element_size_fn_with_strings(self): + """Test default element_size_fn works with strings.""" + with TestPipeline() as p: + data = ['a', 'bbb', 'cc'] + res = ( + p + | beam.Create(data, reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=3, max_batch_weight=100) + | beam.FlatMap(lambda batch: [len(s) for s in batch])) + # Elements should be sorted by length: 'a'(1), 'cc'(2), 'bbb'(3) + assert_that(res, equal_to([1, 2, 3])) + + def test_default_element_size_fn_with_integers(self): + """Test default element_size_fn falls back to 1 for integers.""" + with TestPipeline() as p: + data = [10, 20, 30, 40, 50] + res = ( + p + | beam.Create(data, reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=3, max_batch_weight=100) + | beam.Map(len)) + # With size=1 for all, should batch by max_batch_size + assert_that(res, equal_to([3, 2])) + + def test_custom_element_size_fn(self): + """Test using a custom element_size_fn.""" + with TestPipeline() as p: + data = [{'text': 'a'}, {'text': 'bbb'}, {'text': 'cc'}] + res = ( + p + | beam.Create(data, reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, + max_batch_size=3, + max_batch_weight=100, + element_size_fn=lambda x: len(x['text'])) + | beam.FlatMap(lambda batch: [len(e['text']) for e in batch])) + # Should be sorted by text length + assert_that(res, equal_to([1, 2, 3])) + + def test_empty_input(self): + """Test with empty input produces no output.""" + with TestPipeline() as p: + res = ( + p + | beam.Create([], reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=10, max_batch_weight=100) + | beam.Map(len)) + assert_that(res, equal_to([])) + + def test_single_element(self): + """Test with a single element.""" + with TestPipeline() as p: + res = ( + p + | beam.Create(['hello'], reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=10, max_batch_weight=100)) + assert_that(res, equal_to([['hello']])) + + def test_windowed_batches(self): + """Test that windowed elements are batched per window.""" + with TestPipeline('FnApiRunner') as p: + res = ( + p + | beam.Create(range(1, 8), reshuffle=False) + | beam.Map(lambda t: window.TimestampedValue('a' * t, t)) + | beam.WindowInto(window.FixedWindows(3)) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=10, max_batch_weight=100) + | beam.Map(lambda batch: ''.join(batch))) + # FixedWindows(3) with default offset 0 produces: + # Window [0, 3): elements at t=1,2 with sizes 1,2 + # Window [3, 6): elements at t=3,4,5 with sizes 3,4,5 + # Window [6, 9): elements at t=6,7 with sizes 6,7 + assert_that( + res, + equal_to([ + 'a' * (1 + 2), # Window [0, 3) + 'a' * (3 + 4 + 5), # Window [3, 6) + 'a' * (6 + 7), # Window [6, 9) + ])) + + def test_validation_min_batch_size(self): + """Test that min_batch_size validation raises ValueError.""" + with self.assertRaises(ValueError) as cm: + util.SortAndBatchElements( + min_batch_size=0, max_batch_size=10, max_batch_weight=100) + self.assertIn('min_batch_size must be >= 1', str(cm.exception)) + + def test_validation_max_batch_size(self): + """Test that max_batch_size < min_batch_size raises ValueError.""" + with self.assertRaises(ValueError) as cm: + util.SortAndBatchElements( + min_batch_size=10, max_batch_size=5, max_batch_weight=100) + self.assertIn('max_batch_size', str(cm.exception)) + self.assertIn('min_batch_size', str(cm.exception)) + + def test_validation_max_batch_weight(self): + """Test that max_batch_weight validation raises ValueError.""" + with self.assertRaises(ValueError) as cm: + util.SortAndBatchElements( + min_batch_size=1, max_batch_size=10, max_batch_weight=0) + self.assertIn('max_batch_weight must be >= 1', str(cm.exception)) + + def test_validation_element_size_fn_callable(self): + """Test that a non-callable element_size_fn raises TypeError.""" + with self.assertRaises(TypeError) as cm: + util.SortAndBatchElements( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=123) + self.assertIn('element_size_fn must be callable', str(cm.exception)) + + def test_batch_timestamps(self): + """Test that batches have correct timestamps.""" + with TestPipeline('FnApiRunner') as p: + res = ( + p + | beam.Create(['a', 'bb', 'ccc'], reshuffle=False) + | util.SortAndBatchElements( + min_batch_size=1, max_batch_size=10, max_batch_weight=100) + | + beam.Map(lambda batch, ts=beam.DoFn.TimestampParam: (len(batch), ts))) + # The single global-window batch is emitted at end-of-window. + expected = [(3, GlobalWindow().max_timestamp())] + assert_that(res, equal_to(expected)) + + +class SortAndBatchElementsDoFnDirectTest(unittest.TestCase): + """Direct unit tests for DoFn internals to ensure coverage. + + Beam's FnApiRunner executes DoFns in a separate SDK harness process, + so coverage tools in the main process cannot capture DoFn code paths. + These tests exercise the DoFn methods directly in-process. + """ + def test_default_element_size_len(self): + from apache_beam.transforms.util import _SortAndBatchElementsDoFn + dofn = _SortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=None) + self.assertEqual(dofn._element_size_fn('abc'), 3) + self.assertEqual(dofn._element_size_fn([1, 2]), 2) + + def test_default_element_size_fallback_warns_once(self): + from apache_beam.transforms.util import _SortAndBatchElementsDoFn + dofn = _SortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=None) + with self.assertLogs('apache_beam.transforms.util', level='WARNING') as cm: + self.assertEqual(dofn._element_size_fn(42), 1) + self.assertIn('does not support len()', cm.output[0]) + # Second call should not warn again + self.assertEqual(dofn._element_size_fn(3.14), 1) + self.assertTrue(dofn._has_warned_type_error) + + def test_global_dofn_sort_and_batch(self): + """Test _SortAndBatchElementsDoFn directly.""" + from apache_beam.transforms.util import _SortAndBatchElementsDoFn + dofn = _SortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=3, + max_batch_weight=100, + element_size_fn=len) + dofn.start_bundle() + for elem in ['ccccc', 'bb', 'dddd', 'a', 'eee']: + dofn.process(elem) + batches = [wv.value for wv in dofn.finish_bundle()] + # All elements emitted + self.assertEqual(sum(len(b) for b in batches), 5) + # Each batch respects max_batch_size=3 + for batch in batches: + self.assertLessEqual(len(batch), 3) + # Elements within each batch are sorted by size + for batch in batches: + lengths = [len(s) for s in batch] + self.assertEqual(lengths, sorted(lengths)) + + def test_global_dofn_empty_bundle(self): + """Test finish_bundle with no elements returns nothing.""" + from apache_beam.transforms.util import _SortAndBatchElementsDoFn + dofn = _SortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=len) + dofn.start_bundle() + result = list(dofn.finish_bundle() or []) + self.assertEqual(result, []) + + def test_global_dofn_weight_splitting(self): + """Test weight-based splitting in the global DoFn.""" + from apache_beam.transforms.util import _SortAndBatchElementsDoFn + + # Each element has size 5, max_batch_weight=12 -> 2 per batch + dofn = _SortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=100, + max_batch_weight=12, + element_size_fn=len) + dofn.start_bundle() + for elem in ['aaaaa', 'bbbbb', 'ccccc', 'ddddd']: + dofn.process(elem) + batches = [wv.value for wv in dofn.finish_bundle()] + self.assertEqual(len(batches), 2) + for batch in batches: + self.assertEqual(len(batch), 2) + + def test_windowed_dofn_flush_and_finish(self): + """Test _WindowAwareSortAndBatchElementsDoFn directly.""" + from apache_beam.transforms.util import _WindowAwareSortAndBatchElementsDoFn + + dofn = _WindowAwareSortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=len) + dofn.start_bundle() + win1 = IntervalWindow(0, 3) + win2 = IntervalWindow(3, 6) + # Manually add to buffers (bypass process() to avoid DoFn.WindowParam) + dofn._buffers[win1].extend(['aa', 'b', 'ccc']) + dofn._buffers[win2].extend(['dddd', 'ee']) + batches = list(dofn.finish_bundle()) + # All elements across both windows emitted + total_elements = sum(len(wv.value) for wv in batches) + self.assertEqual(total_elements, 5) + # Each batch has the correct window + for wv in batches: + self.assertIn(wv.windows[0], (win1, win2)) + + def test_windowed_dofn_overflow_flush(self): + """Test that exceeding _MAX_LIVE_WINDOWS triggers early flush.""" + from apache_beam.transforms.util import _WindowAwareSortAndBatchElementsDoFn + + dofn = _WindowAwareSortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=len) + dofn.start_bundle() + # Fill up to _MAX_LIVE_WINDOWS + for i in range(dofn._MAX_LIVE_WINDOWS): + win = IntervalWindow(i * 10, (i + 1) * 10) + dofn._buffers[win].append('x' * (i + 1)) + self.assertEqual(len(dofn._buffers), dofn._MAX_LIVE_WINDOWS) + # Adding one more window should trigger overflow flush + overflow_win = IntervalWindow(100, 110) + results = list(dofn.process('overflow', overflow_win)) + # One window was flushed, so buffer count stays at _MAX_LIVE_WINDOWS + self.assertLessEqual(len(dofn._buffers), dofn._MAX_LIVE_WINDOWS) + # The flushed window produced output + self.assertGreater(len(results), 0) + + def test_windowed_dofn_flush_empty_window(self): + """Test _flush_window with a non-existent window returns nothing.""" + from apache_beam.transforms.util import _WindowAwareSortAndBatchElementsDoFn + + dofn = _WindowAwareSortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=10, + max_batch_weight=100, + element_size_fn=len) + dofn.start_bundle() + result = list(dofn._flush_window(IntervalWindow(0, 10))) + self.assertEqual(result, []) + + def test_windowed_dofn_weight_splitting(self): + """Test weight-based splitting in the windowed DoFn.""" + from apache_beam.transforms.util import _WindowAwareSortAndBatchElementsDoFn + + dofn = _WindowAwareSortAndBatchElementsDoFn( + min_batch_size=1, + max_batch_size=100, + max_batch_weight=12, + element_size_fn=len) + dofn.start_bundle() + win = IntervalWindow(0, 10) + dofn._buffers[win].extend(['aaaaa', 'bbbbb', 'ccccc', 'ddddd']) + batches = list(dofn._flush_window(win)) + self.assertEqual(len(batches), 2) + for wv in batches: + self.assertEqual(len(wv.value), 2) + self.assertEqual(wv.windows[0], win) + + class IdentityWindowTest(unittest.TestCase): def test_window_preserved(self): expected_timestamp = timestamp.Timestamp(5) From edac5f50c8b2afbf5bfe1a1944beb1a481c74f70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:54:51 +0000 Subject: [PATCH 005/490] Bump pytest from 6.2.5 to 9.0.3 in /playground/infrastructure Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- playground/infrastructure/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/infrastructure/requirements.txt b/playground/infrastructure/requirements.txt index 93b4c4a44d9f..0e1da1660781 100644 --- a/playground/infrastructure/requirements.txt +++ b/playground/infrastructure/requirements.txt @@ -16,7 +16,7 @@ # under the License. mock==4.0.3 -pytest==6.2.5 +pytest==9.0.3 pytest-asyncio==0.18.2 pytest-mock==3.6.1 PyYAML==6.0.2 From c54bc2fd1d74805675db6ae33c50991afd007b3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:19:51 +0000 Subject: [PATCH 006/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.96.0 to 1.97.3. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.96.0...service/s3/v1.97.3) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.97.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- sdks/go.mod | 18 +++++++++--------- sdks/go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 0ebac1a6ca1c..37701285734e 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,11 +32,11 @@ require ( cloud.google.com/go/pubsub v1.50.1 cloud.google.com/go/spanner v1.87.0 cloud.google.com/go/storage v1.59.2 - github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 github.com/aws/smithy-go v1.24.2 github.com/docker/go-connections v0.6.0 github.com/dustin/go-humanize v1.0.1 @@ -149,14 +149,14 @@ require ( github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index be5abe6383d9..59d42c98adaf 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -749,8 +749,8 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= @@ -773,40 +773,40 @@ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= From 2b6f43ac6465d75471a5bafc7561ce1a42930990 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Tue, 28 Apr 2026 12:51:29 -0400 Subject: [PATCH 007/490] Enhance documentation for stateful conversations (#38247) Add recommendation for session_service_factory in stateful conversations. --- .../apache_beam/ml/inference/agent_development_kit.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/ml/inference/agent_development_kit.py b/sdks/python/apache_beam/ml/inference/agent_development_kit.py index 8f3f046c7f26..1130598f06f8 100644 --- a/sdks/python/apache_beam/ml/inference/agent_development_kit.py +++ b/sdks/python/apache_beam/ml/inference/agent_development_kit.py @@ -96,7 +96,12 @@ class ADKAgentModelHandler(ModelHandler[str | genai_Content, batch. By default every invocation uses a fresh, isolated session (stateless). Stateful multi-turn conversations can be achieved by passing a ``session_id`` key inside ``inference_args``; elements sharing the same ``session_id`` will - continue the same conversation history. + continue the same conversation history. When using stateful conversations, + it is recommended to use a custom session_service_factory to provide a session + service implementation which can be managed across multiple workers (e.g. + :class:`~google.adk.sessions.DatabaseSessionService`). The default + :class:`~google.adk.sessions.InMemorySessionService` will not correctly track + the same session across multiple workers. Args: agent: A pre-constructed :class:`~google.adk.agents.Agent` instance, or a From d26ef3b03c666f0f379272c26b9f26df65564f66 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 28 Apr 2026 13:09:30 -0400 Subject: [PATCH 008/490] Use registered type for Row (#38108) * Use registered type for Row * Introduce register_row to register with both coder and schema registry Save schema registry id->type mapping * Allow decorator usage; document register_row is preferred --- ..._PostCommit_Python_Xlang_Gcp_Dataflow.json | 2 +- ...am_PostCommit_Python_Xlang_Gcp_Direct.json | 2 +- ...eam_PostCommit_Python_Xlang_IO_Direct.json | 2 +- .../apache_beam/coders/row_coder_test.py | 4 +- sdks/python/apache_beam/coders/typecoders.py | 41 +++++++++++++++++++ .../internal/cloudpickle_pickler.py | 13 +++++- .../io/external/xlang_jdbcio_it_test.py | 13 ++++-- sdks/python/apache_beam/io/jdbc.py | 1 + .../apache_beam/typehints/row_type_test.py | 18 ++++---- .../apache_beam/typehints/schema_registry.py | 11 ++++- sdks/python/apache_beam/typehints/schemas.py | 29 +++++++------ 11 files changed, 103 insertions(+), 33 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json index e3d6056a5de9..83346d34aee0 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 16 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json index e3d6056a5de9..c5309eebb070 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 17 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json index e3d6056a5de9..c537844dc84a 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 3 } diff --git a/sdks/python/apache_beam/coders/row_coder_test.py b/sdks/python/apache_beam/coders/row_coder_test.py index c12250d64958..c2176e99a999 100644 --- a/sdks/python/apache_beam/coders/row_coder_test.py +++ b/sdks/python/apache_beam/coders/row_coder_test.py @@ -60,13 +60,13 @@ ("one_more_field", typing.Optional[str])]) +@coders_registry.register_row class People(typing.NamedTuple): primary: Person partner: typing.Optional[Person] -coders_registry.register_coder(Person, RowCoder) -coders_registry.register_coder(People, RowCoder) +coders_registry.register_row(Person) class RowCoderTest(unittest.TestCase): diff --git a/sdks/python/apache_beam/coders/typecoders.py b/sdks/python/apache_beam/coders/typecoders.py index 9683e00f0c2a..9ecb14c60f75 100644 --- a/sdks/python/apache_beam/coders/typecoders.py +++ b/sdks/python/apache_beam/coders/typecoders.py @@ -124,6 +124,19 @@ def _normalize_typehint_type(typehint_type): def register_coder( self, typehint_type: Any, typehint_coder_class: Type[coders.Coder]) -> None: + """ + Register a user type with a coder. + + Typical usage:: + + class MyCustomType: + pass + + coders.registry.register_coder(MyCustomType, MyCustomCoder) + + To register a supported user type (data class or named tuple) with Beam Row, + use :meth:`register_row` instead, as it registers both coder and schema. + """ if not isinstance(typehint_coder_class, type): raise TypeError( 'Coder registration requires a coder class object. ' @@ -133,6 +146,34 @@ def register_coder( self._register_coder_internal( self._normalize_typehint_type(typehint_type), typehint_coder_class) + def register_row(self, typehint_type: type[Any]) -> type[Any]: + """ + Register a user type with a Beam Row. + + This registers the type with a RowCoder and register its schema. + + Register a dataclass:: + + @coders.registry.register_row + @dataclass + class MyDataClass: + id: int + name: str + + Register a named tuple:: + + coders.registry.register_row(MyNamedTuple) + """ + from apache_beam.coders import RowCoder + from apache_beam.typehints.schemas import typing_to_runner_api + + # Register with row coder + self.register_coder(typehint_type, RowCoder) + # This call generated a schema id for the type and register it with + # schema registry + typing_to_runner_api(typehint_type) + return typehint_type + def get_coder(self, typehint: Any) -> coders.Coder: if typehint and typehint.__module__ == '__main__': # See https://github.com/apache/beam/issues/21541 diff --git a/sdks/python/apache_beam/internal/cloudpickle_pickler.py b/sdks/python/apache_beam/internal/cloudpickle_pickler.py index acdcc46cd40d..cea4f01f803c 100644 --- a/sdks/python/apache_beam/internal/cloudpickle_pickler.py +++ b/sdks/python/apache_beam/internal/cloudpickle_pickler.py @@ -256,20 +256,27 @@ def dump_session(file_path): # dump supported Beam Registries (currently only logical type registry) from apache_beam.coders import typecoders from apache_beam.typehints import schemas + from apache_beam.typehints.schema_registry import SCHEMA_REGISTRY with _pickle_lock, open(file_path, 'wb') as file: coder_reg = typecoders.registry.get_custom_type_coder_tuples() logical_type_reg = schemas.LogicalType._known_logical_types.copy_custom() + schema_reg = SCHEMA_REGISTRY.get_registered_typings() pickler = cloudpickle.CloudPickler(file) # TODO(https://github.com/apache/beam/issues/18500) add file system registry # once implemented - pickler.dump({"coder": coder_reg, "logical_type": logical_type_reg}) + pickler.dump({ + "coder": coder_reg, + "logical_type": logical_type_reg, + "schema": schema_reg + }) def load_session(file_path): from apache_beam.coders import typecoders from apache_beam.typehints import schemas + from apache_beam.typehints.schema_registry import SCHEMA_REGISTRY with _pickle_lock, open(file_path, 'rb') as file: registries = cloudpickle.load(file) @@ -284,3 +291,7 @@ def load_session(file_path): schemas.LogicalType._known_logical_types.load(registries["logical_type"]) else: _LOGGER.warning('No logical type registry found in saved session') + if "schema" in registries: + SCHEMA_REGISTRY.load_registered_typings(registries["schema"]) + else: + _LOGGER.warning('No schema registry found in saved session') diff --git a/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py b/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py index 069f13e11bfb..0e967f1beec3 100644 --- a/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py +++ b/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py @@ -30,6 +30,7 @@ import apache_beam as beam from apache_beam import coders +from apache_beam.io import jdbc from apache_beam.io.jdbc import ReadFromJdbc from apache_beam.io.jdbc import WriteToJdbc from apache_beam.options.pipeline_options import StandardOptions @@ -64,7 +65,7 @@ ("f_timestamp", Timestamp), ("f_decimal", Decimal), ("f_date", datetime.date), ("f_time", datetime.time)], ) -coders.registry.register_coder(JdbcTestRow, coders.RowCoder) +coders.registry.register_row(JdbcTestRow) CustomSchemaRow = typing.NamedTuple( "CustomSchemaRow", @@ -82,11 +83,17 @@ ("renamed_time", datetime.time), ], ) -coders.registry.register_coder(CustomSchemaRow, coders.RowCoder) + +# Need to put inside enforce_millis_instant_for_timestamp context to align +# with the same setup in ReadFromJdbc.__init__. Remove once Beam moved to +# micros instant for timestamp +# Alternatively, use coders.registry.register_coder(CustomSchemaRow, RowCoder) +with jdbc.enforce_millis_instant_for_timestamp(): + coders.registry.register_row(CustomSchemaRow) SimpleRow = typing.NamedTuple( "SimpleRow", [("id", int), ("name", str), ("value", float)]) -coders.registry.register_coder(SimpleRow, coders.RowCoder) +coders.registry.register_row(SimpleRow) @pytest.mark.uses_gcp_java_expansion_service diff --git a/sdks/python/apache_beam/io/jdbc.py b/sdks/python/apache_beam/io/jdbc.py index e6646443bfb0..741507634539 100644 --- a/sdks/python/apache_beam/io/jdbc.py +++ b/sdks/python/apache_beam/io/jdbc.py @@ -264,6 +264,7 @@ def enforce_millis_instant_for_timestamp(): LogicalType._known_logical_types = old_registry.copy() try: LogicalType.register_logical_type(MillisInstant) + LogicalType.register_logical_type(JdbcDateType) yield finally: LogicalType._known_logical_types = old_registry diff --git a/sdks/python/apache_beam/typehints/row_type_test.py b/sdks/python/apache_beam/typehints/row_type_test.py index 97012d9561d7..54e64caf6fa7 100644 --- a/sdks/python/apache_beam/typehints/row_type_test.py +++ b/sdks/python/apache_beam/typehints/row_type_test.py @@ -33,8 +33,10 @@ class RowTypeTest(unittest.TestCase): @staticmethod def _check_key_type_and_count(x) -> int: key_type = type(x[0]) - if not row_type._user_type_is_generated(key_type): - raise RuntimeError("Expect type after GBK to be generated user type") + if row_type._user_type_is_generated(key_type): + raise RuntimeError("Type after GBK not preserved, get generated type") + if not hasattr(key_type, row_type._BEAM_SCHEMA_ID): + raise RuntimeError("Type after GBK missing Beam schema ID") return len(x[1]) @@ -42,8 +44,7 @@ def test_group_by_key_namedtuple(self): MyNamedTuple = typing.NamedTuple( "MyNamedTuple", [("id", int), ("name", str)]) - beam.coders.typecoders.registry.register_coder( - MyNamedTuple, beam.coders.RowCoder) + beam.coders.typecoders.registry.register_row(MyNamedTuple) def generate(num: int): for i in range(100): @@ -67,8 +68,7 @@ class MyDataClass: id: int name: str - beam.coders.typecoders.registry.register_coder( - MyDataClass, beam.coders.RowCoder) + beam.coders.typecoders.registry.register_row(MyDataClass) def generate(num: int): for i in range(100): @@ -120,10 +120,8 @@ class DataClassInt: class DataClassStr(DataClassInt): name: str - beam.coders.typecoders.registry.register_coder( - DataClassInt, beam.coders.RowCoder) - beam.coders.typecoders.registry.register_coder( - DataClassStr, beam.coders.RowCoder) + beam.coders.typecoders.registry.register_row(DataClassInt) + beam.coders.typecoders.registry.register_row(DataClassStr) def generate(num: int): for i in range(10): diff --git a/sdks/python/apache_beam/typehints/schema_registry.py b/sdks/python/apache_beam/typehints/schema_registry.py index 7d8cdcf57d3f..684bf8734a5f 100644 --- a/sdks/python/apache_beam/typehints/schema_registry.py +++ b/sdks/python/apache_beam/typehints/schema_registry.py @@ -26,7 +26,7 @@ class SchemaTypeRegistry(object): def __init__(self): self.by_id = {} - self.by_typing = {} + self.by_typing = {} # currently not used def generate_new_id(self): for _ in range(100): @@ -43,6 +43,15 @@ def add(self, typing, schema): if schema.id: self.by_id[schema.id] = (typing, schema) + def load_registered_typings(self, by_id): + for id, typing in by_id.items(): + if id not in self.by_id: + self.by_id[id] = (typing, None) + + def get_registered_typings(self): + # Used by save_main_session, as pb2.schema isn't picklable + return {k: v[0] for k, v in self.by_id.items()} + def get_typing_by_id(self, unique_id): if not unique_id: return None diff --git a/sdks/python/apache_beam/typehints/schemas.py b/sdks/python/apache_beam/typehints/schemas.py index 2e028bb37e17..eb2990d4222e 100644 --- a/sdks/python/apache_beam/typehints/schemas.py +++ b/sdks/python/apache_beam/typehints/schemas.py @@ -620,19 +620,22 @@ def named_tuple_from_schema(self, schema: schema_pb2.Schema) -> type: descriptions[field.name] = field.description subfields.append((field.name, field_py_type)) - user_type = NamedTuple(type_name, subfields) - - # Define a reduce function, otherwise these types can't be pickled - # (See BEAM-9574) - setattr( - user_type, - '__reduce__', - _named_tuple_reduce_method(schema.SerializeToString())) - setattr(user_type, "_field_descriptions", descriptions) - setattr(user_type, row_type._BEAM_SCHEMA_ID, schema.id) - - self.schema_registry.add(user_type, schema) - coders.registry.register_coder(user_type, coders.RowCoder) + if schema.id in self.schema_registry.by_id: + user_type = self.schema_registry.by_id[schema.id][0] + else: + user_type = NamedTuple(type_name, subfields) + + # Define a reduce function, otherwise these types can't be pickled + # (See BEAM-9574) + setattr( + user_type, + '__reduce__', + _named_tuple_reduce_method(schema.SerializeToString())) + setattr(user_type, "_field_descriptions", descriptions) + setattr(user_type, row_type._BEAM_SCHEMA_ID, schema.id) + + self.schema_registry.add(user_type, schema) + coders.registry.register_coder(user_type, coders.RowCoder) return user_type From 0353233ca3b3c074fb1a38f6e6d6aa2bd85b7b96 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:29:28 -0400 Subject: [PATCH 009/490] Bump Docker PreCommit timeout, address errant setuptools warning (#38305) --- .github/workflows/beam_PreCommit_PythonDocker.yml | 2 +- sdks/python/container/Dockerfile | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beam_PreCommit_PythonDocker.yml b/.github/workflows/beam_PreCommit_PythonDocker.yml index c40190201d04..040b4dd71fc3 100644 --- a/.github/workflows/beam_PreCommit_PythonDocker.yml +++ b/.github/workflows/beam_PreCommit_PythonDocker.yml @@ -56,7 +56,7 @@ jobs: beam_PreCommit_PythonDocker: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 30 + timeout-minutes: 45 strategy: fail-fast: false matrix: diff --git a/sdks/python/container/Dockerfile b/sdks/python/container/Dockerfile index af45b29d0444..3bdef2dc1ddc 100644 --- a/sdks/python/container/Dockerfile +++ b/sdks/python/container/Dockerfile @@ -87,7 +87,10 @@ RUN \ # Update ensurepip to use most recent versions of setuptools and pip. This avoids some vulnerabilities which won't be fixed on older versions of python. pip install upgrade_ensurepip; \ python3 -m upgrade_ensurepip; \ - find /usr/local/lib/python${py_version}/ensurepip/_bundled/setuptools-* -type f ! -name $(basename $(ls -v /usr/local/lib/python${py_version}/ensurepip/_bundled/setuptools-*-py3-none-any.whl | tail -n 1)) -delete; \ + # setuptools is not bundled with ensurepip in Python 3.12+ + if [ "${py_version}" = "3.10" ] || [ "${py_version}" = "3.11" ]; then \ + find /usr/local/lib/python${py_version}/ensurepip/_bundled/setuptools-* -type f ! -name $(basename $(ls -v /usr/local/lib/python${py_version}/ensurepip/_bundled/setuptools-*-py3-none-any.whl | tail -n 1)) -delete; \ + fi; \ find /usr/local/lib/python${py_version}/ensurepip/_bundled/pip-* -type f ! -name $(basename $(ls -v /usr/local/lib/python${py_version}/ensurepip/_bundled/pip-*-py3-none-any.whl | tail -n 1)) -delete; \ pip uninstall upgrade_ensurepip -y; \ python3 -m ensurepip; From 800bef3e5300c95b7f544a3508dbfc76197f8601 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 28 Apr 2026 15:26:29 -0400 Subject: [PATCH 010/490] Update Flink Version Compatibility (#38277) --- .../www/site/content/en/documentation/runners/flink.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/www/site/content/en/documentation/runners/flink.md b/website/www/site/content/en/documentation/runners/flink.md index 8356b9067a6b..1aafa41ecb9c 100644 --- a/website/www/site/content/en/documentation/runners/flink.md +++ b/website/www/site/content/en/documentation/runners/flink.md @@ -330,6 +330,16 @@ To find out which version of Flink is compatible with Beam please see the table Artifact Id Supported Beam Versions + + 2.0.x + beam-runners-flink-2.0 + ≥ 2.72.0 + + + 1.20.x + beam-runners-flink-1.20 + ≥ 2.70.0 + 1.19.x beam-runners-flink-1.19 From fbabff5ea3dd8259a30d653f6aff603d1c18c26d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:51:08 +0000 Subject: [PATCH 011/490] Bump nodemailer from 7.0.11 to 8.0.5 in /scripts/ci/issue-report Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.11 to 8.0.5. - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.11...v8.0.5) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 8.0.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- scripts/ci/issue-report/package-lock.json | 14 +++++++------- scripts/ci/issue-report/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/ci/issue-report/package-lock.json b/scripts/ci/issue-report/package-lock.json index 8989783e9d7c..5d7b321e01b0 100644 --- a/scripts/ci/issue-report/package-lock.json +++ b/scripts/ci/issue-report/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@octokit/rest": "^21.1.1", "node-fetch": "^2.6.1", - "nodemailer": "^7.0.11" + "nodemailer": "^8.0.5" } }, "node_modules/@octokit/auth-token": { @@ -207,9 +207,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", "engines": { "node": ">=6.0.0" } @@ -367,9 +367,9 @@ } }, "nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==" + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==" }, "tr46": { "version": "0.0.3", diff --git a/scripts/ci/issue-report/package.json b/scripts/ci/issue-report/package.json index 5e365333f42d..53a61e3a1d15 100644 --- a/scripts/ci/issue-report/package.json +++ b/scripts/ci/issue-report/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@octokit/rest": "^21.1.1", - "nodemailer": "^7.0.11", + "nodemailer": "^8.0.5", "node-fetch": "^2.6.1" }, "type": "module" From dde918c39221bcef616ba1a0c9633b8ecbd1e7e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:38:11 +0000 Subject: [PATCH 012/490] Bump picomatch from 2.3.1 to 2.3.2 in /scripts/ci/pr-bot Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) --- updated-dependencies: - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- scripts/ci/pr-bot/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/ci/pr-bot/package-lock.json b/scripts/ci/pr-bot/package-lock.json index d1b77b0a26da..d1220a65482d 100644 --- a/scripts/ci/pr-bot/package-lock.json +++ b/scripts/ci/pr-bot/package-lock.json @@ -1050,9 +1050,9 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "engines": { "node": ">=8.6" @@ -2339,9 +2339,9 @@ } }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "prettier": { From 3dd1fc6dbff27605e6585a97ab65ac4fae1a28af Mon Sep 17 00:00:00 2001 From: reuvenlax Date: Tue, 28 Apr 2026 19:07:13 -0700 Subject: [PATCH 013/490] Refactor StorageAPI BigQuery sink (#38264) * Refactor StorageApi BigQuery sink to simplify cache management and management of pins. * fixes * fixes * fixes * typo --- .../io/gcp/bigquery/AppendClientCache.java | 140 +++ .../sdk/io/gcp/bigquery/AppendClientInfo.java | 52 +- .../sdk/io/gcp/bigquery/BigQueryServices.java | 2 +- .../io/gcp/bigquery/BigQueryServicesImpl.java | 2 +- .../StorageApiWriteUnshardedRecords.java | 152 +-- .../StorageApiWritesShardedRecords.java | 950 +++++++++--------- .../io/gcp/testing/FakeDatasetService.java | 4 +- 7 files changed, 676 insertions(+), 626 deletions(-) create mode 100644 sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java new file mode 100644 index 000000000000..b172a4ac3046 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.RemovalNotification; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Encapsulates the cache of {@link AppendClientInfo} objects and the synchronization protocol + * required to use them safely. The Guava cache object is thread-safe. However our protocol requires + * that client pin the StreamAppendClient after looking up the cache, and we must ensure that the + * cache is not accessed in between the lookup and the pin (any access of the cache could trigger + * element expiration). + */ +class AppendClientCache { + private static final Logger LOG = LoggerFactory.getLogger(AppendClientCache.class); + private final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); + + private final Cache appendCache; + + @SuppressWarnings({"FutureReturnValueIgnored"}) + AppendClientCache(Duration expireAfterAccess) { + this.appendCache = + CacheBuilder.newBuilder() + .expireAfterAccess(expireAfterAccess.getMillis(), TimeUnit.MILLISECONDS) + .removalListener( + (RemovalNotification removal) -> { + LOG.info("Expiring append client for {}", removal.getKey()); + final @Nullable AppendClientInfo appendClientInfo = removal.getValue(); + if (appendClientInfo != null) { + // Remove the pin owned by the cache itself. Since the client has not been + // marked as closed, we + // can call unpin in this thread without worrying about blocking the thread. + appendClientInfo.unpinAppendClient(null); + // Close the client in another thread to avoid blocking the main thread. + closeWriterExecutor.submit(appendClientInfo::close); + } + }) + .build(); + } + + // The cache itself always own one pin on the object. This Callable is always used to ensure that + // the cache + // adds a pin before loading a value. + private static Callable wrapWithPin(Callable loader) { + return () -> { + AppendClientInfo client = loader.call(); + client.pinAppendClient(); + return client; + }; + } + + /** + * Atomically get an append client from the cache and add a pin. This pin is owned by the client, + * which has the responsibility of removing it. If the client is not in the cache, loader will be + * used to load the client; in this case an additional pin will be added owned by the cache, + * removed when the item is evicted. + */ + public AppendClientInfo getAndPin(KeyT key, Callable loader) throws Exception { + synchronized (this) { + AppendClientInfo info = appendCache.get(key, wrapWithPin(loader)); + info.pinAppendClient(); + return info; + } + } + + /** "Refresh" an object by invalidating the old cache entry. */ + public AppendClientInfo refreshObjectAndPin(KeyT key, Callable loader) + throws Exception { + synchronized (this) { + appendCache.invalidate(key); + return getAndPin(key, loader); + } + } + + public void invalidate(KeyT key, AppendClientInfo expectedClient) { + // The default stream is cached across multiple different DoFns. If they all try + // and + // invalidate, then we can get races between threads invalidating and recreating + // streams. For this reason, + // we check to see that the cache still contains the object we created before + // invalidating (in case another + // thread has already invalidated and recreated the stream). + synchronized (this) { + AppendClientInfo cachedAppendClient = appendCache.getIfPresent(key); + if (cachedAppendClient != null + && System.identityHashCode(cachedAppendClient) + == System.identityHashCode(expectedClient)) { + appendCache.invalidate(key); + } + } + } + + public void invalidate(KeyT key) { + synchronized (this) { + appendCache.invalidate(key); + } + } + + public void tickle(KeyT key) { + synchronized (this) { + appendCache.getIfPresent(key); + } + } + + public void clear() { + synchronized (this) { + appendCache.invalidateAll(); + } + } + + public void unpinAsync(AppendClientInfo appendClientInfo) { + appendClientInfo.unpinAppendClient(closeWriterExecutor); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java index e9adc8097604..55c6007e1986 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java @@ -27,13 +27,17 @@ import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.google.protobuf.Message; -import java.util.function.Consumer; +import java.util.concurrent.ExecutorService; import java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nullable; +import org.apache.beam.sdk.function.ThrowingConsumer; +import org.apache.beam.sdk.function.ThrowingRunnable; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Container class used by {@link StorageApiWritesShardedRecords} and {@link @@ -42,6 +46,8 @@ */ @AutoValue abstract class AppendClientInfo { + private static final Logger LOG = LoggerFactory.getLogger(AppendClientInfo.class); + private final Counter activeStreamAppendClients = Metrics.counter(AppendClientInfo.class, "activeStreamAppendClients"); @@ -49,7 +55,7 @@ abstract class AppendClientInfo { abstract TableSchema getTableSchema(); - abstract Consumer getCloseAppendClient(); + abstract ThrowingConsumer getCloseAppendClient(); abstract com.google.api.services.bigquery.model.TableSchema getJsonTableSchema(); @@ -65,7 +71,8 @@ abstract static class Builder { abstract Builder setTableSchema(TableSchema value); - abstract Builder setCloseAppendClient(Consumer value); + abstract Builder setCloseAppendClient( + ThrowingConsumer value); abstract Builder setJsonTableSchema(com.google.api.services.bigquery.model.TableSchema value); @@ -83,7 +90,7 @@ abstract static class Builder { static AppendClientInfo of( TableSchema tableSchema, DescriptorProtos.DescriptorProto descriptor, - Consumer closeAppendClient) + ThrowingConsumer closeAppendClient) throws Exception { return new AutoValue_AppendClientInfo.Builder() .setTableSchema(tableSchema) @@ -97,7 +104,7 @@ static AppendClientInfo of( static AppendClientInfo of( TableSchema tableSchema, - Consumer closeAppendClient, + ThrowingConsumer closeAppendClient, boolean includeCdcColumns) throws Exception { return of( @@ -134,7 +141,12 @@ public AppendClientInfo withAppendClient( public void close() { BigQueryServices.StreamAppendClient client = getStreamAppendClient(); if (client != null) { - getCloseAppendClient().accept(client); + try { + getCloseAppendClient().accept(client); + } catch (Exception e) { + // We ignore errors when closing clients. + LOG.warn("Caught exception whilw trying to close append client. Ignoring", e); + } activeStreamAppendClients.dec(); } } @@ -199,4 +211,32 @@ public TableRow toTableRow(ByteString protoBytes, Predicate includeField throw new RuntimeException(e); } } + + public void pinAppendClient() { + BigQueryServices.StreamAppendClient client = + Preconditions.checkStateNotNull(getStreamAppendClient()); + client.pin(); + } + + public void unpinAppendClient(@Nullable ExecutorService executor) { + BigQueryServices.StreamAppendClient client = + Preconditions.checkStateNotNull(getStreamAppendClient()); + if (executor != null) { + runAsyncIgnoreFailure(executor, client::unpin); + } else { + client.unpin(); + } + } + + @SuppressWarnings({"FutureReturnValueIgnored"}) + private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunnable task) { + executor.submit( + () -> { + try { + task.run(); + } catch (Throwable e) { + LOG.info("Exception happened while executing async task. Ignoring: ", e); + } + }); + } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java index 78e714a7ccd6..66458a8339f9 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java @@ -270,7 +270,7 @@ default long getInflightWaitSeconds() { /** * Unpin this object. If the object has been closed, this will release any underlying resources. */ - void unpin() throws Exception; + void unpin(); } /** diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java index 2a9cc7649c21..aa9a5fd310b0 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java @@ -1617,7 +1617,7 @@ public void pin() { } @Override - public void unpin() throws Exception { + public void unpin() { boolean closeWriter; synchronized (this) { Preconditions.checkState(pins > 0, "Tried to unpin when pins==0"); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java index 600f49b0be3e..2dfc8b2f1c00 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java @@ -47,9 +47,6 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; @@ -89,9 +86,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.RemovalNotification; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; @@ -122,56 +116,17 @@ public class StorageApiWriteUnshardedRecords private final Coder successfulRowsCoder; private final boolean autoUpdateSchema; private final boolean ignoreUnknownValues; - private static final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); private final BigQueryIO.Write.CreateDisposition createDisposition; private final @Nullable String kmsKey; private final boolean usesCdc; private final AppendRowsRequest.MissingValueInterpretation defaultMissingValueInterpretation; private final @Nullable Map bigLakeConfiguration; - /** - * The Guava cache object is thread-safe. However our protocol requires that client pin the - * StreamAppendClient after looking up the cache, and we must ensure that the cache is not - * accessed in between the lookup and the pin (any access of the cache could trigger element - * expiration). Therefore most used of APPEND_CLIENTS should synchronize. - */ - private static final Cache APPEND_CLIENTS = - CacheBuilder.newBuilder() - .expireAfterAccess(15, TimeUnit.MINUTES) - .removalListener( - (RemovalNotification removal) -> { - LOG.info("Expiring append client for {}", removal.getKey()); - final @Nullable AppendClientInfo appendClientInfo = removal.getValue(); - if (appendClientInfo != null) { - appendClientInfo.close(); - } - }) - .build(); + private static final AppendClientCache APPEND_CLIENTS = + new AppendClientCache<>(Duration.standardMinutes(15)); static void clearCache() { - APPEND_CLIENTS.invalidateAll(); - } - - // Run a closure asynchronously, ignoring failures. - private interface ThrowingRunnable { - void run() throws Exception; - } - - private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunnable task) { - executor.submit( - () -> { - try { - task.run(); - } catch (Exception e) { - String msg = - e.toString() - + "\n" - + Arrays.stream(e.getStackTrace()) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); - System.err.println("Exception happened while executing async task. Ignoring: " + msg); - } - }); + APPEND_CLIENTS.clear(); } public StorageApiWriteUnshardedRecords( @@ -362,18 +317,9 @@ public TableDestination getTableDestination() { void teardown() { maybeTickleCache(); - if (appendClientInfo != null) { - StreamAppendClient client = appendClientInfo.getStreamAppendClient(); - if (client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, client::unpin); - } - // if this is a PENDING stream, we won't be using it again after cleaning up this - // destination state, so clear it from the cache - if (!useDefaultStream) { - APPEND_CLIENTS.invalidate(streamName); - } - appendClientInfo = null; - } + // if this is a PENDING stream, we won't be using it again after cleaning up this + // destination state, so clear it from the cache + invalidateAppendClient(!useDefaultStream); } String getDefaultStreamName() { @@ -419,18 +365,7 @@ AppendClientInfo generateClient(@Nullable TableSchema updatedSchema) throws Exce AppendClientInfo.of( schemaAndDescriptor.tableSchema, schemaAndDescriptor.descriptor, - // Make sure that the client is always closed in a different thread to avoid - // blocking. - client -> - runAsyncIgnoreFailure( - closeWriterExecutor, - () -> { - synchronized (APPEND_CLIENTS) { - // Remove the pin owned by the cache. - client.unpin(); - client.close(); - } - }))); + AutoCloseable::close)); CreateTableHelpers.createTableWrapper( () -> { @@ -446,9 +381,6 @@ AppendClientInfo generateClient(@Nullable TableSchema updatedSchema) throws Exce return null; }, tryCreateTable); - - // This pin is "owned" by the cache. - Preconditions.checkStateNotNull(appendClientInfo.get().getStreamAppendClient()).pin(); return appendClientInfo.get(); } @@ -515,23 +447,14 @@ AppendClientInfo getAppendClientInfo( try { if (this.appendClientInfo == null) { getOrCreateStreamName(); - final AppendClientInfo newAppendClientInfo; - synchronized (APPEND_CLIENTS) { - if (lookupCache) { - newAppendClientInfo = - APPEND_CLIENTS.get( + this.appendClientInfo = + lookupCache + ? APPEND_CLIENTS.getAndPin( + getStreamAppendClientCacheEntryKey(), () -> generateClient(updatedSchema)) + : APPEND_CLIENTS.refreshObjectAndPin( getStreamAppendClientCacheEntryKey(), () -> generateClient(updatedSchema)); - } else { - newAppendClientInfo = generateClient(updatedSchema); - // override the clients in the cache. - APPEND_CLIENTS.put(getStreamAppendClientCacheEntryKey(), newAppendClientInfo); - } - // This pin is "owned" by the current DoFn. - Preconditions.checkStateNotNull(newAppendClientInfo.getStreamAppendClient()).pin(); - } - nextCacheTickle = Instant.now().plus(java.time.Duration.ofMinutes(1)); - this.appendClientInfo = newAppendClientInfo; } + nextCacheTickle = Instant.now().plus(java.time.Duration.ofMinutes(1)); return Preconditions.checkStateNotNull(appendClientInfo); } catch (Exception e) { throw new RuntimeException(e); @@ -540,37 +463,24 @@ AppendClientInfo getAppendClientInfo( void maybeTickleCache() { if (appendClientInfo != null && Instant.now().isAfter(nextCacheTickle)) { - synchronized (APPEND_CLIENTS) { - APPEND_CLIENTS.getIfPresent(getStreamAppendClientCacheEntryKey()); - } + APPEND_CLIENTS.tickle(getStreamAppendClientCacheEntryKey()); nextCacheTickle = Instant.now().plus(java.time.Duration.ofMinutes(1)); } } - void invalidateWriteStream() { - if (appendClientInfo != null) { - synchronized (APPEND_CLIENTS) { - // Unpin in a different thread, as it may execute a blocking close. - StreamAppendClient client = appendClientInfo.getStreamAppendClient(); - if (client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, client::unpin); - } - // The default stream is cached across multiple different DoFns. If they all try and - // invalidate, then we can get races between threads invalidating and recreating - // streams. For this reason, - // we check to see that the cache still contains the object we created before - // invalidating (in case another - // thread has already invalidated and recreated the stream). - String cacheEntryKey = getStreamAppendClientCacheEntryKey(); - @Nullable - AppendClientInfo cachedAppendClient = APPEND_CLIENTS.getIfPresent(cacheEntryKey); - if (cachedAppendClient != null - && System.identityHashCode(cachedAppendClient) - == System.identityHashCode(appendClientInfo)) { - APPEND_CLIENTS.invalidate(cacheEntryKey); - } + void invalidateAppendClient(boolean invalidateCache) { + if (this.appendClientInfo != null) { + // Unpin in a different thread, as it may execute a blocking close. + StreamAppendClient client = appendClientInfo.getStreamAppendClient(); + if (client != null) { + APPEND_CLIENTS.unpinAsync(Preconditions.checkStateNotNull(this.appendClientInfo)); + } + if (invalidateCache) { + APPEND_CLIENTS.invalidate( + getStreamAppendClientCacheEntryKey(), + Preconditions.checkStateNotNull(this.appendClientInfo)); } - appendClientInfo = null; + this.appendClientInfo = null; } } @@ -677,7 +587,7 @@ long flush( LOG.info("Schema out of date: refreshing table schema for {}.", tableUrn); // Refresh our view of the schema and try again.. this.messageConverter.updateSchemaFromTable(); - invalidateWriteStream(); + invalidateAppendClient(true); this.appendClientInfo = Preconditions.checkStateNotNull( getAppendClientInfo( @@ -874,7 +784,7 @@ long flush( if (!quotaError) { // This forces us to close and reopen all gRPC connections to Storage API on error, // which empirically fixes random stuckness issues. - invalidateWriteStream(); + invalidateAppendClient(true); allowedRetry = 5; } else { allowedRetry = 35; @@ -1031,7 +941,11 @@ void postFlush() { TableSchemaUpdateUtils.getUpdatedSchema( this.messageConverter.getTableSchema(), updatedTableSchemaReturned); if (updatedTableSchema.isPresent()) { - invalidateWriteStream(); + invalidateAppendClient(false); + // TODO: This overwrites whatever is in the cache which can cause races between + // threads. + // A better approach would be to check the cache, and keep whichever schema is + // "larger". appendClientInfo = Preconditions.checkStateNotNull( getAppendClientInfo(false, updatedTableSchema.get())); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java index 5771ea5074b8..cbace6e7ff40 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java @@ -37,23 +37,18 @@ import io.grpc.Status.Code; import java.io.IOException; import java.time.Instant; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; @@ -104,9 +99,6 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.RemovalNotification; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; @@ -149,44 +141,12 @@ public class StorageApiWritesShardedRecords succussfulRowsCoder; private final TupleTag> flushTag = new TupleTag<>("flushTag"); - private static final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); - - private static final Cache>, AppendClientInfo> APPEND_CLIENTS = - CacheBuilder.newBuilder() - .expireAfterAccess(5, TimeUnit.MINUTES) - .removalListener( - (RemovalNotification>, AppendClientInfo> removal) -> { - final @Nullable AppendClientInfo appendClientInfo = removal.getValue(); - if (appendClientInfo != null) { - appendClientInfo.close(); - } - }) - .build(); - static void clearCache() { - APPEND_CLIENTS.invalidateAll(); - } + private static final AppendClientCache>> APPEND_CLIENTS = + new AppendClientCache<>(Duration.standardMinutes(5)); - // Run a closure asynchronously, ignoring failures. - private interface ThrowingRunnable { - void run() throws Exception; - } - - private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunnable task) { - executor.submit( - () -> { - try { - task.run(); - } catch (Exception e) { - String msg = - e.toString() - + "\n" - + Arrays.stream(e.getStackTrace()) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); - System.err.println("Exception happened while executing async task. Ignoring: " + msg); - } - }); + static void clearCache() { + APPEND_CLIENTS.clear(); } public StorageApiWritesShardedRecords( @@ -282,7 +242,6 @@ static class AppendRowsContext extends RetryManager.Operation.Context { final ShardedKey key; String streamName = ""; - @Nullable StreamAppendClient client = null; long offset = -1; long numRows = 0; long tryIteration = 0; @@ -487,6 +446,46 @@ public void onTeardown() { } } + // Holder for an AppendClientHolder. Maintains a pin on the client as long as it's active. + private class AppendClientHolder implements AutoCloseable { + private final KV> key; + private final Callable appendClientInfoCallable; + + private AppendClientInfo appendClientInfo; + private boolean valid; + + public AppendClientHolder( + ShardedKey key, Callable appendClientInfoCallable) + throws Exception { + this.key = messageConverters.getAppendClientKey(key); + this.appendClientInfoCallable = appendClientInfoCallable; + this.appendClientInfo = APPEND_CLIENTS.getAndPin(this.key, appendClientInfoCallable); + this.valid = true; + } + + void invalidateAndReset() throws Exception { + APPEND_CLIENTS.unpinAsync(this.appendClientInfo); // Make sure to unpin in another thread. + APPEND_CLIENTS.invalidate(key); + this.appendClientInfo = APPEND_CLIENTS.getAndPin(key, appendClientInfoCallable); + } + + @Override + public void close() { + APPEND_CLIENTS.unpinAsync(this.appendClientInfo); // Make sure to unpin in another thread. + this.valid = false; + } + + AppendClientInfo get() { + org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState( + valid); + return appendClientInfo; + } + + StreamAppendClient getStreamAppendClient() { + return Preconditions.checkStateNotNull(appendClientInfo.getStreamAppendClient()); + } + } + private CreateRetryManagerResult createRetryManager( ShardedKey key, Iterable messages, @@ -554,6 +553,214 @@ private CreateRetryManagerResult createRetryManager( retryManager, failedRows, recordsAppended, histogramUpdates); } + private void handleAppendFailure( + Iterable> failedContexts, + TableReference tableReference, + String shortTableId, + AppendClientInfo appendClientInfo, + Callable tryCreateTable, + BiConsumer>, Boolean> initializeContexts, + Consumer>> clearClients, + ValueState streamOffset, + MultiOutputReceiver o) { + // The first context is always the one that fails. + AppendRowsContext failedContext = + Preconditions.checkStateNotNull(Iterables.getFirst(failedContexts, null)); + BigQuerySinkMetrics.reportFailedRPCMetrics( + failedContext, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); + String errorCode = BigQuerySinkMetrics.throwableToGRPCCodeString(failedContext.getError()); + + // AppendSerializationError means that BigQuery detected errors on individual rows, e.g. + // a row not conforming to bigQuery invariants. These errors are persistent, so we redirect + // those rows to the + // failedInserts PCollection, and retry with the remaining rows. + if (failedContext.getError() != null + && failedContext.getError() instanceof Exceptions.AppendSerializationError) { + Exceptions.AppendSerializationError error = + Preconditions.checkArgumentNotNull( + (Exceptions.AppendSerializationError) failedContext.getError()); + + Set failedRowIndices = error.getRowIndexToErrorMessage().keySet(); + for (int failedIndex : failedRowIndices) { + // Convert the message to a TableRow and send it to the failedRows collection. + TableRow failedRow = failedContext.failsafeTableRows.get(failedIndex); + if (failedRow == null) { + ByteString protoBytes = failedContext.protoRows.getSerializedRows(failedIndex); + failedRow = appendClientInfo.toTableRow(protoBytes, Predicates.alwaysTrue()); + } + org.joda.time.Instant timestamp = failedContext.timestamps.get(failedIndex); + o.get(failedRowsTag) + .outputWithTimestamp( + new BigQueryStorageApiInsertError( + failedRow, + error.getRowIndexToErrorMessage().get(failedIndex), + tableReference), + timestamp); + } + int failedRows = failedRowIndices.size(); + rowsSentToFailedRowsCollection.inc(failedRows); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.FAILED, errorCode, shortTableId) + .inc(failedRows); + + // Remove the failed row from the payload, so we retry the batch without the failed + // rows. + ProtoRows.Builder retryRows = ProtoRows.newBuilder(); + @Nullable List timestamps = Lists.newArrayList(); + for (int i = 0; i < failedContext.protoRows.getSerializedRowsCount(); ++i) { + if (!failedRowIndices.contains(i)) { + ByteString rowBytes = failedContext.protoRows.getSerializedRows(i); + retryRows.addSerializedRows(rowBytes); + timestamps.add(failedContext.timestamps.get(i)); + } + } + failedContext.protoRows = retryRows.build(); + failedContext.timestamps = timestamps; + int retriedRows = failedContext.protoRows.getSerializedRowsCount(); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) + .inc(retriedRows); + + // Since we removed rows, we need to update the insert offsets for all remaining rows. + long offset = failedContext.offset; + for (AppendRowsContext context : failedContexts) { + context.offset = offset; + offset += context.protoRows.getSerializedRowsCount(); + } + streamOffset.write(offset); + return; + } + + Throwable error = Preconditions.checkStateNotNull(failedContext.getError()); + Status.Code statusCode = Status.fromThrowable(error).getCode(); + + // This means that the offset we have stored does not match the current end of + // the stream in the Storage API. Usually this happens because a crash or a bundle + // failure + // happened after an append but before the worker could checkpoint it's + // state. The records that were appended in a failed bundle will be retried, + // meaning that the unflushed tail of the stream must be discarded to prevent + // duplicates. + boolean offsetMismatch = + statusCode.equals(Code.OUT_OF_RANGE) || statusCode.equals(Code.ALREADY_EXISTS); + + boolean quotaError = statusCode.equals(Code.RESOURCE_EXHAUSTED); + if (!offsetMismatch) { + // Don't log errors for expected offset mismatch. These will be logged as warnings + // below. + LOG.error("Got error {} closing {}", failedContext.getError(), failedContext.streamName); + } + + try { + // TODO: Only do this on explicit NOT_FOUND errors once BigQuery reliably produces + // them. + tryCreateTable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (!quotaError) { + // For known errors (offset mismatch, not found) we must reestablish + // the streams. + // However we've seen that doing this fixes random stuckness issues by reestablishing + // gRPC connections, + // so we close the clients for all non-quota errors. + + clearClients.accept(failedContexts); + } + appendFailures.inc(); + int retriedRows = failedContext.protoRows.getSerializedRowsCount(); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) + .inc(retriedRows); + + // Schema mismatched exceptions can happen if the table was recently updated. Since + // vortex caches schemas + // we might see the new schema before vortex does. In this case, we simply need to + // retry. + Exceptions.@Nullable StorageException storageException = Exceptions.toStorageException(error); + boolean schemaMismatchError = + (storageException instanceof Exceptions.SchemaMismatchedException); + if (!schemaMismatchError) { + // There's no special error code for missing required fields, and that can also happen + // due to vortex + // being delayed at seeing a new schema. We're forced to parse the description to + // determine that this has happened. + // TODO: Vortex team to introduce a special storage error code for this, so we don't + // have to parse + // descriptions. + Status status = Status.fromThrowable(error); + if (status.getCode() == Code.INVALID_ARGUMENT) { + String description = status.getDescription(); + schemaMismatchError = description != null && description.contains("incompatible fields"); + } + } + if (schemaMismatchError) { + LOG.info( + "Vortex failed stream open due to incompatible fields. This is likely because the BigQuery " + + "schema was recently updated and Vortex hasn't noticed yet, so retrying. error {}", + Preconditions.checkStateNotNull(error).toString()); + } + + boolean explicitStreamFinalized = + failedContext.getError() instanceof StreamFinalizedException; + // This implies that the stream doesn't exist or has already been finalized. In this + // case we have no choice but to create a new stream. + boolean streamDoesNotExist = + explicitStreamFinalized + || statusCode.equals(Code.INVALID_ARGUMENT) + || statusCode.equals(Code.NOT_FOUND) + || statusCode.equals(Code.FAILED_PRECONDITION); + streamDoesNotExist = streamDoesNotExist && !schemaMismatchError; + + if (offsetMismatch || streamDoesNotExist) { + appendOffsetFailures.inc(); + LOG.warn( + "Append to {} failed. Will retry with a new stream", + failedContext, + failedContext.getError()); + // Finalize the stream and clear streamName so a new stream will be created. + o.get(flushTag) + .output(KV.of(failedContext.streamName, new Operation(failedContext.offset - 1, true))); + // Reinitialize all contexts with the new stream and new offsets. + initializeContexts.accept(failedContexts, true); + + // Offset failures imply that all subsequent parallel appends will also fail. + // Retry them all. + } + } + + private void handleAppendSuccess( + AppendRowsContext context, + String shortTableId, + AppendClientInfo appendClientInfo, + MultiOutputReceiver o) { + AppendRowsResponse response = Preconditions.checkStateNotNull(context.getResult()); + o.get(flushTag) + .output( + KV.of( + context.streamName, + new Operation( + context.offset + context.protoRows.getSerializedRowsCount() - 1, false))); + int flushedRows = context.protoRows.getSerializedRowsCount(); + flushesScheduled.inc(flushedRows); + BigQuerySinkMetrics.reportSuccessfulRpcMetrics( + context, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.SUCCESSFUL, BigQuerySinkMetrics.OK, shortTableId) + .inc(flushedRows); + + if (successfulRowsTag != null) { + for (int i = 0; i < context.protoRows.getSerializedRowsCount(); ++i) { + ByteString protoBytes = context.protoRows.getSerializedRows(i); + org.joda.time.Instant timestamp = context.timestamps.get(i); + o.get(successfulRowsTag) + .outputWithTimestamp( + appendClientInfo.toTableRow(protoBytes, successfulRowsPredicate), timestamp); + } + } + } + @ProcessElement public void process( ProcessContext c, @@ -631,10 +838,12 @@ public void process( () -> { @Nullable TableSchema tableSchema; DescriptorProtos.DescriptorProto descriptor; - TableSchema updatedSchemaValue = updatedSchema.read(); + TableSchema updatedSchemaValue = autoUpdateSchema ? updatedSchema.read() : null; + if (autoUpdateSchema && updatedSchemaValue != null) { - // We've seen an updated schema, so we use that instead of querying the - // MessageConverter. + // This means that Vortex has told us in the past that the table schema has been + // updated. We should use + // this updated schema instead of the initial schema from the messageConverter. tableSchema = updatedSchemaValue; descriptor = TableRowToStorageApiProto.descriptorSchemaFromTableSchema( @@ -648,8 +857,8 @@ public void process( if (autoUpdateSchema) { // A StreamWriter ignores table schema updates that happen prior to its creation. // So before creating a StreamWriter below, we fetch the table schema to check if we - // missed an update. - // If so, use the new schema instead of the base schema + // missed an update. If so, use the new schema instead of the base schema. + // TODO: There's still a race here! @Nullable TableSchema streamSchema = MoreObjects.firstNonNull( @@ -671,475 +880,222 @@ public void process( AppendClientInfo.of( Preconditions.checkStateNotNull(tableSchema), descriptor, - // Make sure that the client is always closed in a different thread - // to - // avoid blocking. - client -> - runAsyncIgnoreFailure( - closeWriterExecutor, - () -> { - // Remove the pin that is "owned" by the cache. - client.unpin(); - client.close(); - })) + AutoCloseable::close) .withAppendClient( writeStreamService, getOrCreateStream, false, defaultMissingValueInterpretation); - // This pin is "owned" by the cache. - Preconditions.checkStateNotNull(info.getStreamAppendClient()).pin(); return info; }; - AtomicReference appendClientInfo = - new AtomicReference<>( - APPEND_CLIENTS.get( - messageConverters.getAppendClientKey(element.getKey()), getAppendClientInfo)); - String currentStream = getOrCreateStream.get(); - if (!currentStream.equals(appendClientInfo.get().getStreamName())) { - // Cached append client is inconsistent with persisted state. Throw away cached item and - // force it to be - // recreated. - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - appendClientInfo.set( - APPEND_CLIENTS.get( - messageConverters.getAppendClientKey(element.getKey()), getAppendClientInfo)); - } - - TableSchema updatedSchemaValue = updatedSchema.read(); - if (autoUpdateSchema && updatedSchemaValue != null) { - if (appendClientInfo.get().hasSchemaChanged(updatedSchemaValue)) { - appendClientInfo.set( - AppendClientInfo.of( - updatedSchemaValue, appendClientInfo.get().getCloseAppendClient(), false)); - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - APPEND_CLIENTS.put( - messageConverters.getAppendClientKey(element.getKey()), appendClientInfo.get()); + // The StreamWriter has two pins on it. The static cache holds a pin, as it continues to cache + // values after this + // method exits, so must hold the pin. The local AppendClientHolder also holds a pin, as the + // cache could in + // theory evict the object during execution and we want a pin held throughout the execution of + // this function. + try (AppendClientHolder appendClientHolder = + new AppendClientHolder(element.getKey(), getAppendClientInfo)) { + String currentStream = getOrCreateStream.get(); + if (!currentStream.equals(appendClientHolder.get().getStreamName())) { + // Cached append client is inconsistent with persisted state. Throw away cached item and + // force it to be recreated. + appendClientHolder.invalidateAndReset(); } - } - - // Initialize stream names and offsets for all contexts. This will be called initially, but - // will also be called if we roll over to a new stream on a retry. - BiConsumer>, Boolean> initializeContexts = - (contexts, isFailure) -> { - try { - if (isFailure) { - // Clear the stream name, forcing a new one to be created. - streamName.write(""); - } - appendClientInfo.set( - appendClientInfo - .get() - .withAppendClient( - writeStreamService, - getOrCreateStream, - false, - defaultMissingValueInterpretation)); - StreamAppendClient streamAppendClient = - Preconditions.checkArgumentNotNull( - appendClientInfo.get().getStreamAppendClient()); - String streamNameRead = Preconditions.checkArgumentNotNull(streamName.read()); - long currentOffset = Preconditions.checkArgumentNotNull(streamOffset.read()); - for (AppendRowsContext context : contexts) { - context.streamName = streamNameRead; - streamAppendClient.pin(); - context.client = appendClientInfo.get().getStreamAppendClient(); - context.offset = currentOffset; - ++context.tryIteration; - currentOffset = context.offset + context.protoRows.getSerializedRowsCount(); - } - streamOffset.write(currentOffset); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; - - Consumer>> clearClients = - (contexts) -> { - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - appendClientInfo.set(appendClientInfo.get().withNoAppendClient()); - APPEND_CLIENTS.put( - messageConverters.getAppendClientKey(element.getKey()), appendClientInfo.get()); - for (AppendRowsContext context : contexts) { - if (context.client != null) { - // Unpin in a different thread, as it may execute a blocking close. - runAsyncIgnoreFailure(closeWriterExecutor, context.client::unpin); - context.client = null; - } - } - }; - Function, ApiFuture> runOperation = - context -> { - if (context.protoRows.getSerializedRowsCount() == 0) { - // This might happen if all rows in a batch failed and were sent to the failed-rows - // PCollection. - return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); - } - try { - appendClientInfo.set( - appendClientInfo - .get() - .withAppendClient( - writeStreamService, - getOrCreateStream, - false, - defaultMissingValueInterpretation)); - return Preconditions.checkStateNotNull(appendClientInfo.get().getStreamAppendClient()) - .appendRows(context.offset, context.protoRows); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; + TableSchema updatedSchemaValue = autoUpdateSchema ? updatedSchema.read() : null; + if ((autoUpdateSchema && updatedSchemaValue != null) + && appendClientHolder.get().hasSchemaChanged(updatedSchemaValue)) { + appendClientHolder.invalidateAndReset(); + } - Function>, RetryType> onError = - failedContexts -> { - // The first context is always the one that fails. - AppendRowsContext failedContext = - Preconditions.checkStateNotNull(Iterables.getFirst(failedContexts, null)); - BigQuerySinkMetrics.reportFailedRPCMetrics( - failedContext, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); - String errorCode = - BigQuerySinkMetrics.throwableToGRPCCodeString(failedContext.getError()); - - // AppendSerializationError means that BigQuery detected errors on individual rows, e.g. - // a row not conforming - // to bigQuery invariants. These errors are persistent, so we redirect those rows to the - // failedInserts - // PCollection, and retry with the remaining rows. - if (failedContext.getError() != null - && failedContext.getError() instanceof Exceptions.AppendSerializationError) { - Exceptions.AppendSerializationError error = - Preconditions.checkArgumentNotNull( - (Exceptions.AppendSerializationError) failedContext.getError()); - - Set failedRowIndices = error.getRowIndexToErrorMessage().keySet(); - for (int failedIndex : failedRowIndices) { - // Convert the message to a TableRow and send it to the failedRows collection. - TableRow failedRow = failedContext.failsafeTableRows.get(failedIndex); - if (failedRow == null) { - // TODO: MAKE SURE WE USE UPDATED DESCRIPTOR - ByteString protoBytes = failedContext.protoRows.getSerializedRows(failedIndex); - failedRow = - appendClientInfo.get().toTableRow(protoBytes, Predicates.alwaysTrue()); + // Initialize stream names and offsets for all contexts. This will be called initially, but + // will also be called if we roll over to a new stream on a retry. + BiConsumer>, Boolean> initializeContexts = + (contexts, isFailure) -> { + try { + if (isFailure) { + // Clear the stream name, forcing a new one to be created. + streamName.write(""); } - org.joda.time.Instant timestamp = failedContext.timestamps.get(failedIndex); - o.get(failedRowsTag) - .outputWithTimestamp( - new BigQueryStorageApiInsertError( - failedRow, - error.getRowIndexToErrorMessage().get(failedIndex), - tableReference), - timestamp); - } - int failedRows = failedRowIndices.size(); - rowsSentToFailedRowsCollection.inc(failedRows); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.FAILED, errorCode, shortTableId) - .inc(failedRows); - - // Remove the failed row from the payload, so we retry the batch without the failed - // rows. - ProtoRows.Builder retryRows = ProtoRows.newBuilder(); - @Nullable List timestamps = Lists.newArrayList(); - for (int i = 0; i < failedContext.protoRows.getSerializedRowsCount(); ++i) { - if (!failedRowIndices.contains(i)) { - ByteString rowBytes = failedContext.protoRows.getSerializedRows(i); - retryRows.addSerializedRows(rowBytes); - timestamps.add(failedContext.timestamps.get(i)); + String streamNameRead = Preconditions.checkArgumentNotNull(streamName.read()); + long currentOffset = Preconditions.checkArgumentNotNull(streamOffset.read()); + for (AppendRowsContext context : contexts) { + context.streamName = streamNameRead; + context.offset = currentOffset; + ++context.tryIteration; + currentOffset = context.offset + context.protoRows.getSerializedRowsCount(); } + streamOffset.write(currentOffset); + } catch (Exception e) { + throw new RuntimeException(e); } - failedContext.protoRows = retryRows.build(); - failedContext.timestamps = timestamps; - int retriedRows = failedContext.protoRows.getSerializedRowsCount(); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) - .inc(retriedRows); - - // Since we removed rows, we need to update the insert offsets for all remaining rows. - long offset = failedContext.offset; - for (AppendRowsContext context : failedContexts) { - context.offset = offset; - offset += context.protoRows.getSerializedRowsCount(); + }; + + Consumer>> clearClients = + (contexts) -> { + try { + appendClientHolder.invalidateAndReset(); + } catch (Exception e) { + throw new RuntimeException(e); } - streamOffset.write(offset); - return RetryType.RETRY_ALL_OPERATIONS; - } - - Throwable error = Preconditions.checkStateNotNull(failedContext.getError()); - - Status.Code statusCode = Status.fromThrowable(error).getCode(); - - // This means that the offset we have stored does not match the current end of - // the stream in the Storage API. Usually this happens because a crash or a bundle - // failure - // happened after an append but before the worker could checkpoint it's - // state. The records that were appended in a failed bundle will be retried, - // meaning that the unflushed tail of the stream must be discarded to prevent - // duplicates. - boolean offsetMismatch = - statusCode.equals(Code.OUT_OF_RANGE) || statusCode.equals(Code.ALREADY_EXISTS); - - boolean quotaError = statusCode.equals(Code.RESOURCE_EXHAUSTED); - if (!offsetMismatch) { - // Don't log errors for expected offset mismatch. These will be logged as warnings - // below. - LOG.error( - "Got error {} closing {}", failedContext.getError(), failedContext.streamName); - } - - try { - // TODO: Only do this on explicit NOT_FOUND errors once BigQuery reliably produces - // them. - tryCreateTable.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - if (!quotaError) { - // For known errors (offset mismatch, not found) we must reestablish - // the streams. - // However we've seen that doing this fixes random stuckness issues by reestablishing - // gRPC connections, - // so we close the clients for all non-quota errors. - - clearClients.accept(failedContexts); - } - appendFailures.inc(); - int retriedRows = failedContext.protoRows.getSerializedRowsCount(); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) - .inc(retriedRows); - - // Schema mismatched exceptions can happen if the table was recently updated. Since - // vortex caches schemas - // we might see the new schema before vortex does. In this case, we simply need to - // retry. - Exceptions.@Nullable StorageException storageException = - Exceptions.toStorageException(error); - boolean schemaMismatchError = - (storageException instanceof Exceptions.SchemaMismatchedException); - if (!schemaMismatchError) { - // There's no special error code for missing required fields, and that can also happen - // due to vortex - // being delayed at seeing a new schema. We're forced to parse the description to - // determine that this has happened. - // TODO: Vortex team to introduce a special storage error code for this, so we don't - // have to parse - // descriptions. - Status status = Status.fromThrowable(error); - if (status.getCode() == Code.INVALID_ARGUMENT) { - String description = status.getDescription(); - schemaMismatchError = - description != null && description.contains("incompatible fields"); + }; + + Function, ApiFuture> runOperation = + context -> { + if (context.protoRows.getSerializedRowsCount() == 0) { + // This might happen if all rows in a batch failed and were sent to the failed-rows + // PCollection. + return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); } - } - if (schemaMismatchError) { - LOG.info( - "Vortex failed stream open due to incompatible fields. This is likely because the BigTable " - + "schema was recently updated and Vortex hasn't noticed yet, so retrying. error {}", - Preconditions.checkStateNotNull(error).toString()); - } - - boolean explicitStreamFinalized = - failedContext.getError() instanceof StreamFinalizedException; - // This implies that the stream doesn't exist or has already been finalized. In this - // case we have no choice but to create a new stream. - boolean streamDoesNotExist = - explicitStreamFinalized - || statusCode.equals(Code.INVALID_ARGUMENT) - || statusCode.equals(Code.NOT_FOUND) - || statusCode.equals(Code.FAILED_PRECONDITION); - streamDoesNotExist = streamDoesNotExist && !schemaMismatchError; - - if (offsetMismatch || streamDoesNotExist) { - appendOffsetFailures.inc(); - LOG.warn( - "Append to {} failed. Will retry with a new stream", - failedContext, - failedContext.getError()); - // Finalize the stream and clear streamName so a new stream will be created. - o.get(flushTag) - .output( - KV.of( - failedContext.streamName, new Operation(failedContext.offset - 1, true))); - // Reinitialize all contexts with the new stream and new offsets. - initializeContexts.accept(failedContexts, true); - - // Offset failures imply that all subsequent parallel appends will also fail. - // Retry them all. + try { + return appendClientHolder + .getStreamAppendClient() + .appendRows(context.offset, context.protoRows); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + + Function>, RetryType> onError = + failedContexts -> { + handleAppendFailure( + failedContexts, + tableReference, + shortTableId, + appendClientHolder.get(), + tryCreateTable, + initializeContexts, + clearClients, + streamOffset, + o); return RetryType.RETRY_ALL_OPERATIONS; - } - - return RetryType.RETRY_ALL_OPERATIONS; - }; + }; + Consumer> onSuccess = + context -> handleAppendSuccess(context, shortTableId, appendClientHolder.get(), o); + + BackOff backoff = + FluentBackoff.DEFAULT + .withInitialBackoff(Duration.standardSeconds(1)) + .withMaxBackoff(Duration.standardMinutes(1)) + .withMaxRetries(500) + .withThrottledTimeCounter( + BigQuerySinkMetrics.throttledTimeCounter( + BigQuerySinkMetrics.RpcMethod.OPEN_WRITE_STREAM)) + .backoff(); + CreateRetryManagerResult createRetryManagerResult; + do { + // Each ProtoRows object contains at most 1MB of rows. + // TODO: Push messageFromTableRow up to top level. That we we cans skip TableRow entirely + // if + // already proto or already schema. + Iterable messages = + new SplittingIterable( + element.getValue(), + splitSize, + // Unknown field merger + (bytes, tableRow) -> + appendClientHolder.get().mergeNewFields(bytes, tableRow, ignoreUnknownValues), + // Convert back to TableRow + bytes -> appendClientHolder.get().toTableRow(bytes, Predicates.alwaysTrue()), + // Failed rows consumer + (failedRow, errorMessage) -> { + o.get(failedRowsTag) + .outputWithTimestamp( + new BigQueryStorageApiInsertError( + failedRow.getValue(), errorMessage, tableReference), + failedRow.getTimestamp()); + rowsSentToFailedRowsCollection.inc(); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.FAILED, + BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, + shortTableId) + .inc(1); + }, + // Get the currently-known TableSchema hash + () -> appendClientHolder.get().getTableSchemaHash(), + () -> + TableRowToStorageApiProto.wrapDescriptorProto( + messageConverter.getDescriptor(false)), + autoUpdateSchema, + elementTs); + + createRetryManagerResult = + createRetryManager( + element.getKey(), + messages, + runOperation, + onError, + onSuccess, + appendClientHolder.get(), + tableReference); + if (createRetryManagerResult.getSchemaMismatchSeen()) { + // TODO: The call to updateSchemaFromTable will throttle the DoFn (both because of the + // RPC + // call and because + // the cache has a delay on refresh). We should update throttling counters here as well. + LOG.info("Schema out of date: refreshing table schema for {}", tableId); + // Force the message converter to get the schema again from the table. + messageConverter.updateSchemaFromTable(); + // Close all RPC clients that were opened with the old descriptor. Clear the cache, + // forcing us to create a new append client with the updated descriptor. + appendClientHolder.invalidateAndReset(); + } + } while (createRetryManagerResult.getSchemaMismatchSeen() + && BackOffUtils.next(Sleeper.DEFAULT, backoff)); + + // Output any rows that failed along they way. + createRetryManagerResult + .getFailedRows() + .forEach( + tv -> o.get(failedRowsTag).outputWithTimestamp(tv.getValue(), tv.getTimestamp())); + rowsSentToFailedRowsCollection.inc(createRetryManagerResult.getFailedRows().size()); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.FAILED, + BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, + shortTableId) + .inc(createRetryManagerResult.getFailedRows().size()); + + recordsAppended.inc(createRetryManagerResult.getRecordsAppended()); + createRetryManagerResult.getHistogramValues().forEach(appendSizeDistribution::update); + + Instant now = Instant.now(); + + RetryManager> retryManager = + Preconditions.checkStateNotNull(createRetryManagerResult.getRetryManager()); + int numAppends = retryManager.getRemainingOperationCount(); + Iterable> contexts = retryManager.getRemainingContexts(); + + if (numAppends > 0) { + initializeContexts.accept(contexts, false); + retryManager.run(true); - Consumer> onSuccess = - context -> { - AppendRowsResponse response = Preconditions.checkStateNotNull(context.getResult()); - o.get(flushTag) - .output( - KV.of( - context.streamName, - new Operation( - context.offset + context.protoRows.getSerializedRowsCount() - 1, - false))); - int flushedRows = context.protoRows.getSerializedRowsCount(); - flushesScheduled.inc(flushedRows); - BigQuerySinkMetrics.reportSuccessfulRpcMetrics( - context, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.SUCCESSFUL, BigQuerySinkMetrics.OK, shortTableId) - .inc(flushedRows); - - if (successfulRowsTag != null) { - for (int i = 0; i < context.protoRows.getSerializedRowsCount(); ++i) { - ByteString protoBytes = context.protoRows.getSerializedRows(i); - org.joda.time.Instant timestamp = context.timestamps.get(i); - o.get(successfulRowsTag) - .outputWithTimestamp( - appendClientInfo.get().toTableRow(protoBytes, successfulRowsPredicate), - timestamp); + appendSplitDistribution.update(numAppends); + if (autoUpdateSchema) { + @Nullable + StreamAppendClient streamAppendClient = appendClientHolder.getStreamAppendClient(); + TableSchema originalSchema = appendClientHolder.get().getTableSchema(); + + @Nullable + TableSchema updatedSchemaReturned = + (streamAppendClient != null) ? streamAppendClient.getUpdatedSchema() : null; + // Update the table schema and clear the append client. + if (updatedSchemaReturned != null) { + Optional newSchema = + TableSchemaUpdateUtils.getUpdatedSchema(originalSchema, updatedSchemaReturned); + if (newSchema.isPresent()) { + APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); + LOG.debug( + "Fetched updated schema for table {}:\n\t{}", tableId, updatedSchemaReturned); + updatedSchema.write(newSchema.get()); } } - }; - - BackOff backoff = - FluentBackoff.DEFAULT - .withInitialBackoff(Duration.standardSeconds(1)) - .withMaxBackoff(Duration.standardMinutes(1)) - .withMaxRetries(500) - .withThrottledTimeCounter( - BigQuerySinkMetrics.throttledTimeCounter( - BigQuerySinkMetrics.RpcMethod.OPEN_WRITE_STREAM)) - .backoff(); - CreateRetryManagerResult createRetryManagerResult; - do { - // Each ProtoRows object contains at most 1MB of rows. - // TODO: Push messageFromTableRow up to top level. That we we cans skip TableRow entirely if - // already proto or already schema. - Iterable messages = - new SplittingIterable( - element.getValue(), - splitSize, - // Unknown field merger - (bytes, tableRow) -> - appendClientInfo.get().mergeNewFields(bytes, tableRow, ignoreUnknownValues), - // Convert back to TableRow - bytes -> appendClientInfo.get().toTableRow(bytes, Predicates.alwaysTrue()), - // Failed rows consumer - (failedRow, errorMessage) -> { - o.get(failedRowsTag) - .outputWithTimestamp( - new BigQueryStorageApiInsertError( - failedRow.getValue(), errorMessage, tableReference), - failedRow.getTimestamp()); - rowsSentToFailedRowsCollection.inc(); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.FAILED, - BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, - shortTableId) - .inc(1); - }, - // Get the currently-known TableSchema hash - () -> appendClientInfo.get().getTableSchemaHash(), - () -> - TableRowToStorageApiProto.wrapDescriptorProto( - messageConverter.getDescriptor(false)), - autoUpdateSchema, - elementTs); - - createRetryManagerResult = - createRetryManager( - element.getKey(), - messages, - runOperation, - onError, - onSuccess, - appendClientInfo.get(), - tableReference); - if (createRetryManagerResult.getSchemaMismatchSeen()) { - // TODO: The call to updateSchemaFromTable will throttle the DoFn (both because of the RPC - // call and because - // the cache has a delay on refresh). We should update throttling counters here as well. - LOG.info("Schema out of date: refreshing table schema for {}", tableId); - // Force the message converter to get the schema again from the table. - messageConverter.updateSchemaFromTable(); - // Close all RPC clients that were opened with the old descriptor. Clear the cache, - // forcing us to create a new append client with the updated descriptor. - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - appendClientInfo.set( - APPEND_CLIENTS.get( - messageConverters.getAppendClientKey(element.getKey()), getAppendClientInfo)); - } - } while (createRetryManagerResult.getSchemaMismatchSeen() - && BackOffUtils.next(Sleeper.DEFAULT, backoff)); - - // Output any rows that failed along they way. - createRetryManagerResult - .getFailedRows() - .forEach( - tv -> o.get(failedRowsTag).outputWithTimestamp(tv.getValue(), tv.getTimestamp())); - rowsSentToFailedRowsCollection.inc(createRetryManagerResult.getFailedRows().size()); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.FAILED, - BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, - shortTableId) - .inc(createRetryManagerResult.getFailedRows().size()); - - recordsAppended.inc(createRetryManagerResult.getRecordsAppended()); - createRetryManagerResult.getHistogramValues().forEach(appendSizeDistribution::update); - - Instant now = Instant.now(); - - RetryManager> retryManager = - Preconditions.checkStateNotNull(createRetryManagerResult.getRetryManager()); - int numAppends = retryManager.getRemainingOperationCount(); - Iterable> contexts = retryManager.getRemainingContexts(); - - if (numAppends > 0) { - initializeContexts.accept(contexts, false); - try { - retryManager.run(true); - } finally { - // Make sure that all pins are removed. - for (AppendRowsContext context : contexts) { - if (context.client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, context.client::unpin); - } - } - } - appendSplitDistribution.update(numAppends); - - if (autoUpdateSchema) { - @Nullable - StreamAppendClient streamAppendClient = appendClientInfo.get().getStreamAppendClient(); - TableSchema originalSchema = appendClientInfo.get().getTableSchema(); - ; - @Nullable - TableSchema updatedSchemaReturned = - (streamAppendClient != null) ? streamAppendClient.getUpdatedSchema() : null; - // Update the table schema and clear the append client. - if (updatedSchemaReturned != null) { - Optional newSchema = - TableSchemaUpdateUtils.getUpdatedSchema(originalSchema, updatedSchemaReturned); - if (newSchema.isPresent()) { - appendClientInfo.set( - AppendClientInfo.of( - newSchema.get(), appendClientInfo.get().getCloseAppendClient(), false)); - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - APPEND_CLIENTS.put( - messageConverters.getAppendClientKey(element.getKey()), appendClientInfo.get()); - LOG.debug( - "Fetched updated schema for table {}:\n\t{}", tableId, updatedSchemaReturned); - updatedSchema.write(newSchema.get()); - } } - } - java.time.Duration timeElapsed = java.time.Duration.between(now, Instant.now()); - appendLatencyDistribution.update(timeElapsed.toMillis()); + java.time.Duration timeElapsed = java.time.Duration.between(now, Instant.now()); + appendLatencyDistribution.update(timeElapsed.toMillis()); + } } idleTimer.offset(streamIdleTime).withNoOutputTimestamp().setRelative(); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java index 1849df422425..814f4eec421f 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java @@ -764,7 +764,7 @@ public StreamAppendClient getStreamAppendClient( AppendRowsRequest.MissingValueInterpretation missingValueInterpretation) throws Exception { return new StreamAppendClient() { - private Descriptor protoDescriptor; + private Descriptor protoDescriptor = null; private TableSchema currentSchema; private @Nullable com.google.cloud.bigquery.storage.v1.TableSchema updatedSchema; TableRowToStorageApiProto.SchemaInformation schemaInformation; @@ -900,7 +900,7 @@ public void close() throws Exception {} public void pin() {} @Override - public void unpin() throws Exception {} + public void unpin() {} }; } From ffa258af6ac407bcd99a106eae530f716f0c713d Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 29 Apr 2026 08:25:04 -0400 Subject: [PATCH 014/490] add auto gemini review comment workflow (#38309) * add auto gemini review comment workflow * add workflow dispatch * add pr number input option * remove comment * make pull request target * add back opened type --- .github/workflows/gemini-review-comment.yml | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/gemini-review-comment.yml diff --git a/.github/workflows/gemini-review-comment.yml b/.github/workflows/gemini-review-comment.yml new file mode 100644 index 000000000000..058a6e46b361 --- /dev/null +++ b/.github/workflows/gemini-review-comment.yml @@ -0,0 +1,49 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Gemini Review Comment + +on: + workflow_dispatch: + inputs: + pr_number: + description: 'The Pull Request number to review' + required: true + type: string + pull_request_target: + types: [ opened ] + branches: [ master ] + +jobs: + add-comment: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/github-script@v8 + with: + script: | + const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pr_number; + + if (!prNumber) { + throw new Error("Could not find a PR number from context or inputs."); + } + + await github.rest.issues.createComment({ + issue_number: parseInt(prNumber), + owner: context.repo.owner, + repo: context.repo.repo, + body: '/gemini review' + }); From cc6aa6c0dfbf276696f1fbf41d400d8346ae7029 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 29 Apr 2026 09:52:20 -0400 Subject: [PATCH 015/490] Set gemini config.yaml to post code review on PR open (#38316) --- .gemini/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gemini/config.yaml b/.gemini/config.yaml index 9fe8d66315e2..70e33f3d77cd 100644 --- a/.gemini/config.yaml +++ b/.gemini/config.yaml @@ -46,7 +46,7 @@ code_review: # Post code review on PR open. # Type boolean, default: true. - code_review: false + code_review: true # List of glob patterns to ignore (files and directories). # Type: array of string, default: []. From d822309e97902b973da5e326ce484df0a1592450 Mon Sep 17 00:00:00 2001 From: bambadiouf1 Date: Wed, 29 Apr 2026 08:58:00 -0500 Subject: [PATCH 016/490] Add DiskProvisionedIops/ThroughputMibps options and update Go client libraries (#37377) * feat: add support for disk provisioned IOPS and throughput options for python sdk * feat: add disk provisioned IOPS and throughput options to Dataflow worker pools for java sdk * Add iops and throughput pipeline options for Go SDK * Update dataflow api service versions * Update dataflow services api version to latest * cast disk provisioned values to Long and reformat comments in DataflowPipelineTranslator * remove unrelated lint changes * Fix python format issues * fix format * Update runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Adding disk iops and throughput field to job.go * fix format * Update runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * git pull * feat: add support for disk provisioned IOPS and throughput options for python sdk * feat: add disk provisioned IOPS and throughput options to Dataflow worker pools for java sdk * Add iops and throughput pipeline options for Go SDK * Update dataflow api service versions * Update dataflow services api version to latest * cast disk provisioned values to Long and reformat comments in DataflowPipelineTranslator * remove unrelated lint changes * Fix python format issues * fix format * Update runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Adding disk iops and throughput field to job.go * fix format * Update runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * git pull * fix versions * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix versions * update gcp dependencies to latest version * change disk provisioned IOPS and throughput types from Integer to Long in DataflowPipelineOptions * remove redundant long casting when setting disk provisioned IOPS and throughput in DataflowPipelineTranslator * update Dataflow client credentials and regenerate v1b3 API messages * update FHIR search requests to pass nil instead of empty struct to match API requirements * default client infos * restore changes * update google api versions to latest * update go.sum dependencies * update Go module dependencies in go.sum * update Google Cloud SDK dependencies to latest versions * undo dependencies changes * update dependencies * update dependencies in go.mod * update go.sum dependencies * update FHIR search calls to pass nil as the request body * initialize Options map in job to prevent nil pointer exceptions * update disk provisioned options to use Long literals in DataflowPipelineOptionsTest * add autogenerated v1b3 client library definitions * delete * undo * gen_client * undo versions update * update api version * update versions * update long running version * go mod tidy * add accidentally deleted encoding fields * reset * git pull origin master + revert json field deletion * Update changes.md doc with the support for disk provisioned IOPS and throughput pipeline options * gen_client * revert changes cause gen_client --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- CHANGES.md | 1 + .../beam/gradle/BeamModulePlugin.groovy | 2 +- .../dataflow/DataflowPipelineTranslator.java | 7 + .../DataflowPipelineWorkerPoolOptions.java | 14 + .../options/DataflowPipelineOptionsTest.java | 9 + sdks/go.mod | 28 +- sdks/go.sum | 901 +----------------- sdks/go/container/boot.go | 14 +- sdks/go/container/boot_test.go | 14 +- sdks/go/pkg/beam/io/fhirio/common.go | 7 +- sdks/go/pkg/beam/runners/dataflow/dataflow.go | 174 ++-- .../beam/runners/dataflow/dataflow_test.go | 22 + .../beam/runners/dataflow/dataflowlib/job.go | 71 +- .../runners/dataflow/dataflowlib/job_test.go | 36 + sdks/go/test/integration/expansions.go | 8 +- .../apache_beam/options/pipeline_options.py | 18 + .../options/pipeline_options_test.py | 6 + .../runners/dataflow/internal/apiclient.py | 5 + .../dataflow/internal/apiclient_test.py | 17 + .../clients/dataflow/dataflow_v1b3_client.py | 1 - .../dataflow/dataflow_v1b3_messages.py | 101 +- 21 files changed, 393 insertions(+), 1063 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 39e0d5c89716..79700735c06a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,7 @@ ## New Features / Improvements +* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Python/Go) ([#37377](https://github.com/apache/beam/issues/37377)). * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to encode finished bitset. SentinelBitSetCoder and BitSetCoder are state diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 005a8b587804..fe9f1f750862 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -740,7 +740,7 @@ class BeamModulePlugin implements Plugin { google_api_common : "com.google.api:api-common", // google_cloud_platform_libraries_bom sets version google_api_services_bigquery : "com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0", // [bomupgrader] sets version google_api_services_cloudresourcemanager : "com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0", // [bomupgrader] sets version - google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260118-$google_clients_version", + google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260405-$google_clients_version", google_api_services_healthcare : "com.google.apis:google-api-services-healthcare:v1-rev20240130-$google_clients_version", google_api_services_pubsub : "com.google.apis:google-api-services-pubsub:v1-rev20220904-$google_clients_version", google_api_services_storage : "com.google.apis:google-api-services-storage:v1-rev20260204-2.0.0", // [bomupgrader] sets version diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index c57b5e3b1a0e..4016f31a5475 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -489,6 +489,13 @@ public Job translate(List packages) { if (options.getDiskSizeGb() > 0) { workerPool.setDiskSizeGb(options.getDiskSizeGb()); } + if (options.getDiskProvisionedIops() != null && options.getDiskProvisionedIops() > 0) { + workerPool.setDiskProvisionedIops(options.getDiskProvisionedIops()); + } + if (options.getDiskProvisionedThroughputMibps() != null + && options.getDiskProvisionedThroughputMibps() > 0) { + workerPool.setDiskProvisionedThroughputMibps(options.getDiskProvisionedThroughputMibps()); + } AutoscalingSettings settings = new AutoscalingSettings(); if (options.getAutoscalingAlgorithm() != null) { settings.setAlgorithm(options.getAutoscalingAlgorithm().getAlgorithm()); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java index fd4af6d5e043..3fcf69f4cacf 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java @@ -193,6 +193,20 @@ public String getAlgorithm() { void setWorkerDiskType(String value); + /** Provisioned IOPS for the worker disk. */ + @Description("Provisioned IOPS for the worker disk.") + @Nullable + Long getDiskProvisionedIops(); + + void setDiskProvisionedIops(Long value); + + /** Provisioned throughput in MiB/s for the worker disk. */ + @Description("Provisioned throughput in MiB/s for the worker disk.") + @Nullable + Long getDiskProvisionedThroughputMibps(); + + void setDiskProvisionedThroughputMibps(Long value); + /** * Specifies whether worker pools should be started with public IP addresses. * diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java index b381396f2bcc..faa19eefddc5 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java @@ -322,4 +322,13 @@ public void destroy() { TimeoutException.class, () -> DefaultGcpRegionFactory.getRegionFromGcloudCli(1L)); } } + + @Test + public void testDiskProvisionedIopsAndThroughput() { + DataflowPipelineOptions options = PipelineOptionsFactory.as(DataflowPipelineOptions.class); + options.setDiskProvisionedIops(1000L); + options.setDiskProvisionedThroughputMibps(100L); + assertEquals(Long.valueOf(1000), options.getDiskProvisionedIops()); + assertEquals(Long.valueOf(100), options.getDiskProvisionedThroughputMibps()); + } } diff --git a/sdks/go.mod b/sdks/go.mod index 37701285734e..a73c8f3d44f2 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -25,12 +25,12 @@ go 1.26.0 toolchain go1.26.2 require ( - cloud.google.com/go/bigquery v1.72.0 - cloud.google.com/go/bigtable v1.41.0 - cloud.google.com/go/datastore v1.21.0 + cloud.google.com/go/bigquery v1.74.0 + cloud.google.com/go/bigtable v1.42.0 + cloud.google.com/go/datastore v1.22.0 cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.1 - cloud.google.com/go/spanner v1.87.0 + cloud.google.com/go/spanner v1.88.0 cloud.google.com/go/storage v1.59.2 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.7 @@ -56,12 +56,12 @@ require ( github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 golang.org/x/net v0.52.0 - golang.org/x/oauth2 v0.35.0 + golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 - google.golang.org/api v0.257.0 - google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 + google.golang.org/api v0.276.0 + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 @@ -77,13 +77,13 @@ require ( require ( cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/pubsub/v2 v2.0.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect - github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect + github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect @@ -124,8 +124,8 @@ require ( go.einride.tech/aip v0.73.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect @@ -141,7 +141,7 @@ require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.7.0 // indirect + cloud.google.com/go/longrunning v0.8.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect @@ -175,8 +175,8 @@ require ( github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.21.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 59d42c98adaf..074c3122592a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -5,7 +5,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -19,7 +18,6 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -34,421 +32,52 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/bigquery v1.72.0 h1:D/yLju+3Ens2IXx7ou1DJ62juBm+/coBInn4VVOg5Cw= -cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= -cloud.google.com/go/bigtable v1.41.0 h1:99KOWShm/MUyuIbXBeVscdWJFV7GdgiYwFUrB5Iu4BI= -cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/bigquery v1.74.0 h1:Q6bAMv+eyvufOpIrfrYxhM46qq1D3ZQTdgUDQqKS+n8= +cloud.google.com/go/bigquery v1.74.0/go.mod h1:iViO7Cx3A/cRKcHNRsHB3yqGAMInFBswrE9Pxazsc90= +cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= +cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastore v1.21.0 h1:dUrYq47ysCA4nM7u8kRT0WnbfXc6TzX49cP3TCwIiA0= -cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/datastore v1.22.0 h1:FOyx2Ag6ibD2wFkz9S8EiNrmBugia8pQOfpyJxi2yqA= +cloud.google.com/go/datastore v1.22.0/go.mod h1:aopSX+Whx0lHspWWBj+AjWt68/zjYsPfDe3LjWtqZg8= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= -cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= -cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI= cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -456,196 +85,26 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0= cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= -cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= +cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw= cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= @@ -655,7 +114,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= @@ -703,8 +161,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= @@ -713,29 +171,21 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apache/arrow/go/arrow v0.0.0-20200730104253-651201b0f516/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= @@ -831,17 +281,14 @@ github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqx github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc= @@ -854,15 +301,10 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -882,7 +324,6 @@ github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GK github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -903,7 +344,6 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -917,9 +357,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= @@ -927,9 +364,6 @@ github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9O github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -948,7 +382,6 @@ github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -962,7 +395,6 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -972,8 +404,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -987,7 +417,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1004,8 +433,6 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1041,7 +468,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -1055,7 +481,6 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -1072,7 +497,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -1100,7 +524,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -1122,28 +545,15 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -1151,8 +561,6 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= @@ -1160,7 +568,6 @@ github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO0 github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -1227,9 +634,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -1239,16 +644,13 @@ github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -1270,9 +672,6 @@ github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolM github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -1281,10 +680,6 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= @@ -1345,20 +740,15 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -1369,15 +759,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/proullon/ramsql v0.1.4 h1:yTFRTn46gFH/kPbzCx+mGjuFlyTBUeDr3h2ldwxddl0= github.com/proullon/ramsql v0.1.4/go.mod h1:CFGqeQHQpdRfWqYmWD3yXqPTEaHkF4zgXy1C6qDWc9E= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -1388,7 +773,6 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1404,9 +788,6 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -1431,7 +812,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= @@ -1466,7 +846,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -1494,10 +873,10 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -1515,8 +894,6 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHS go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= @@ -1553,11 +930,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1580,7 +955,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= @@ -1592,10 +966,6 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1620,11 +990,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= @@ -1662,7 +1029,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= @@ -1670,27 +1036,15 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= @@ -1718,19 +1072,8 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1742,10 +1085,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= @@ -1793,14 +1133,12 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1810,12 +1148,10 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1828,23 +1164,11 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1858,9 +1182,6 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= @@ -1878,8 +1199,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1890,9 +1209,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1950,22 +1266,17 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= @@ -1975,22 +1286,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -2035,30 +1340,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= -google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= +google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2104,13 +1387,10 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= @@ -2154,71 +1434,9 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= @@ -2250,21 +1468,8 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -2282,9 +1487,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2323,41 +1525,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/sdks/go/container/boot.go b/sdks/go/container/boot.go index b75201520f39..ab2da3169319 100644 --- a/sdks/go/container/boot.go +++ b/sdks/go/container/boot.go @@ -83,13 +83,13 @@ func configureGoogleCloudProfilerEnvVars(ctx context.Context, logger *tools.Logg } // Fallback to job_name from metadata - if profilerServiceName == "" { - if jobName, jobNameExists := metadata["job_name"]; jobNameExists { - profilerServiceName = jobName - } else { - return errors.New("required job_name missing from metadata, profiling will not be enabled without it") - } - } + if profilerServiceName == "" { + if jobName, jobNameExists := metadata["job_name"]; jobNameExists { + profilerServiceName = jobName + } else { + return errors.New("required job_name missing from metadata, profiling will not be enabled without it") + } + } jobID, idExists := metadata["job_id"] if !idExists { diff --git a/sdks/go/container/boot_test.go b/sdks/go/container/boot_test.go index 244f91fe42e7..bb94aca36be3 100644 --- a/sdks/go/container/boot_test.go +++ b/sdks/go/container/boot_test.go @@ -267,19 +267,19 @@ func TestConfigureGoogleCloudProfilerEnvVars(t *testing.T) { expectingError: true, }, { - name: "Missing profiler name and job_name", - options: `{ + name: "Missing profiler name and job_name", + options: `{ "beam:option:go_options:v1": { "options": { "dataflow_service_options": "enable_google_cloud_profiler" } } }`, - metadata: map[string]string{ - "job_id": "job-789", - }, - expectingError: true, - }, + metadata: map[string]string{ + "job_id": "job-789", + }, + expectingError: true, + }, } for _, tt := range tests { diff --git a/sdks/go/pkg/beam/io/fhirio/common.go b/sdks/go/pkg/beam/io/fhirio/common.go index 75781f7ba0cc..504e65270d58 100644 --- a/sdks/go/pkg/beam/io/fhirio/common.go +++ b/sdks/go/pkg/beam/io/fhirio/common.go @@ -127,11 +127,12 @@ func (c *fhirStoreClientImpl) search(storePath, resourceType string, queries map queryParams = append(queryParams, googleapi.QueryParameter(pageTokenParameterKey, pageToken)) } - searchRequest := &healthcare.SearchResourcesRequest{} + // Pass nil as the body because search parameters are passed via queryParams, + // and the new API expects an io.Reader instead of a specific struct. if resourceType == "" { - return c.fhirService().Search(storePath, searchRequest).Do(queryParams...) + return c.fhirService().Search(storePath, nil).Do(queryParams...) } - return c.fhirService().SearchType(storePath, resourceType, searchRequest).Do(queryParams...) + return c.fhirService().SearchType(storePath, resourceType, nil).Do(queryParams...) } func (c *fhirStoreClientImpl) deidentify(srcStorePath, dstStorePath string, deidConfig *healthcare.DeidentifyConfig) (operationResults, error) { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow.go b/sdks/go/pkg/beam/runners/dataflow/dataflow.go index 101441dbcb56..ecbfe53939ec 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow.go @@ -52,32 +52,34 @@ import ( // TODO(herohde) 5/16/2017: the Dataflow flags should match the other SDKs. var ( - endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") - stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") - workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") - image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") - labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") - serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") - numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") - workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") - maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") - diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") - diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") - autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") - zone = flag.String("zone", "", "GCP zone (optional)") - kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") - network = flag.String("network", "", "GCP network (optional)") - subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") - noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") - usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") - tempLocation = flag.String("temp_location", "", "Temp location (optional)") - workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") - machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") - minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") - workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") - workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") - dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") - flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") + endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") + stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") + workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") + image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") + labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") + serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") + numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") + workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") + maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") + diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") + diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") + diskProvisionedIops = flag.Int64("disk_provisioned_iops", 0, "Provisioned IOPS for the worker disk (optional).") + diskProvisionedThroughputMibps = flag.Int64("disk_provisioned_throughput_mibps", 0, "Provisioned throughput in MiB/s for the worker disk (optional).") + autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") + zone = flag.String("zone", "", "GCP zone (optional)") + kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") + network = flag.String("network", "", "GCP network (optional)") + subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") + noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") + usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") + tempLocation = flag.String("temp_location", "", "Temp location (optional)") + workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") + machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") + minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") + workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") + workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") + dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") + flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") // TODO(https://github.com/apache/beam/issues/21604) Turn this on once TO_STRING is implemented // enableHotKeyLogging = flag.Bool("enable_hot_key_logging", false, "Specifies that when a hot key is detected in the pipeline, the literal, human-readable key is printed in the user's Cloud Logging project (optional).") @@ -105,33 +107,35 @@ func init() { // should be added to this map. // Don't filter temp_location since we need this included in PipelineOptions to correctly upload heap dumps. var flagFilter = map[string]bool{ - "dataflow_endpoint": true, - "staging_location": true, - "worker_harness_container_image": true, - "sdk_container_image": true, - "labels": true, - "service_account_email": true, - "num_workers": true, - "max_num_workers": true, - "disk_size_gb": true, - "disk_type": true, - "autoscaling_algorithm": true, - "zone": true, - "network": true, - "subnetwork": true, - "no_use_public_ips": true, - "template_location": true, - "worker_machine_type": true, - "machine_type": true, - "min_cpu_platform": true, - "dataflow_worker_jar": true, - "worker_region": true, - "worker_zone": true, - "teardown_policy": true, - "cpu_profiling": true, - "session_recording": true, - "update": true, - "transform_name_mapping": true, + "dataflow_endpoint": true, + "staging_location": true, + "worker_harness_container_image": true, + "sdk_container_image": true, + "labels": true, + "service_account_email": true, + "num_workers": true, + "max_num_workers": true, + "disk_size_gb": true, + "disk_type": true, + "disk_provisioned_iops": true, + "disk_provisioned_throughput_mibps": true, + "autoscaling_algorithm": true, + "zone": true, + "network": true, + "subnetwork": true, + "no_use_public_ips": true, + "template_location": true, + "worker_machine_type": true, + "machine_type": true, + "min_cpu_platform": true, + "dataflow_worker_jar": true, + "worker_region": true, + "worker_zone": true, + "teardown_policy": true, + "cpu_profiling": true, + "session_recording": true, + "update": true, + "transform_name_mapping": true, // Job Options flags "endpoint": true, @@ -379,37 +383,39 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions beam.PipelineOptions.LoadOptionsFromFlags(flagFilter) opts := &dataflowlib.JobOptions{ - Name: jobopts.GetJobName(), - Streaming: streaming, - Experiments: experiments, - DataflowServiceOptions: dfServiceOptions, - Options: beam.PipelineOptions.Export(), - Project: project, - Region: region, - Zone: *zone, - KmsKey: *kmsKey, - Network: *network, - Subnetwork: *subnetwork, - NoUsePublicIPs: *noUsePublicIPs, - NumWorkers: *numWorkers, - MaxNumWorkers: *maxNumWorkers, - WorkerHarnessThreads: *workerHarnessThreads, - DiskSizeGb: *diskSizeGb, - DiskType: *diskType, - Algorithm: *autoscalingAlgorithm, - FlexRSGoal: *flexRSGoal, - MachineType: *firstNonEmpty(workerMachineType, machineType), - Labels: jobLabels, - ServiceAccountEmail: *serviceAccountEmail, - TempLocation: *tempLocation, - TemplateLocation: *templateLocation, - Worker: *jobopts.WorkerBinary, - WorkerRegion: *workerRegion, - WorkerZone: *workerZone, - TeardownPolicy: *teardownPolicy, - ContainerImage: getContainerImage(ctx), - Update: *update, - TransformNameMapping: updateTransformMapping, + Name: jobopts.GetJobName(), + Streaming: streaming, + Experiments: experiments, + DataflowServiceOptions: dfServiceOptions, + Options: beam.PipelineOptions.Export(), + Project: project, + Region: region, + Zone: *zone, + KmsKey: *kmsKey, + Network: *network, + Subnetwork: *subnetwork, + NoUsePublicIPs: *noUsePublicIPs, + NumWorkers: *numWorkers, + MaxNumWorkers: *maxNumWorkers, + WorkerHarnessThreads: *workerHarnessThreads, + DiskSizeGb: *diskSizeGb, + DiskType: *diskType, + DiskProvisionedIops: *diskProvisionedIops, + DiskProvisionedThroughputMibps: *diskProvisionedThroughputMibps, + Algorithm: *autoscalingAlgorithm, + FlexRSGoal: *flexRSGoal, + MachineType: *firstNonEmpty(workerMachineType, machineType), + Labels: jobLabels, + ServiceAccountEmail: *serviceAccountEmail, + TempLocation: *tempLocation, + TemplateLocation: *templateLocation, + Worker: *jobopts.WorkerBinary, + WorkerRegion: *workerRegion, + WorkerZone: *workerZone, + TeardownPolicy: *teardownPolicy, + ContainerImage: getContainerImage(ctx), + Update: *update, + TransformNameMapping: updateTransformMapping, } if opts.TempLocation == "" { opts.TempLocation = gcsx.Join(*stagingLocation, "tmp") diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go index fa418ebbe6a0..23dcd034120a 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go @@ -516,6 +516,26 @@ func getFieldFromOpt(fieldName string, opts *dataflowlib.JobOptions) string { return reflect.ValueOf(opts).Elem().FieldByName(fieldName).String() } +func TestGetJobOptions_DiskProvisionedOptions(t *testing.T) { + resetGlobals() + *stagingLocation = "gs://testStagingLocation" + *gcpopts.Project = "testProject" + *gcpopts.Region = "testRegion" + *diskProvisionedIops = 1000 + *diskProvisionedThroughputMibps = 100 + + opts, err := getJobOptions(context.Background(), false) + if err != nil { + t.Fatalf("getJobOptions() returned error %q, want %q", err, "nil") + } + if got, want := opts.DiskProvisionedIops, int64(1000); got != want { + t.Errorf("opts.DiskProvisionedIops = %d, want %d", got, want) + } + if got, want := opts.DiskProvisionedThroughputMibps, int64(100); got != want { + t.Errorf("opts.DiskProvisionedThroughputMibps = %d, want %d", got, want) + } +} + func resetGlobals() { *autoscalingAlgorithm = "" *dataflowServiceOptions = "" @@ -537,4 +557,6 @@ func resetGlobals() { *workerHarnessImage = "" *workerMachineType = "" *machineType = "" + *diskProvisionedIops = 0 + *diskProvisionedThroughputMibps = 0 } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go index db83992fbebc..26412031cb0d 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go @@ -47,26 +47,28 @@ type JobOptions struct { // Pipeline options Options runtime.RawOptions - Streaming bool - Project string - Region string - Zone string - KmsKey string - Network string - Subnetwork string - NoUsePublicIPs bool - NumWorkers int64 - DiskSizeGb int64 - DiskType string - MachineType string - Labels map[string]string - ServiceAccountEmail string - WorkerRegion string - WorkerZone string - ContainerImage string - ArtifactURLs []string // Additional packages for workers. - FlexRSGoal string - EnableHotKeyLogging bool + Streaming bool + Project string + Region string + Zone string + KmsKey string + Network string + Subnetwork string + NoUsePublicIPs bool + NumWorkers int64 + DiskSizeGb int64 + DiskType string + DiskProvisionedIops int64 + DiskProvisionedThroughputMibps int64 + MachineType string + Labels map[string]string + ServiceAccountEmail string + WorkerRegion string + WorkerZone string + ContainerImage string + ArtifactURLs []string // Additional packages for workers. + FlexRSGoal string + EnableHotKeyLogging bool // Streaming update settings Update bool @@ -154,6 +156,9 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker return nil, err } + if opts.Options.Options == nil { + opts.Options.Options = make(map[string]string) + } opts.Options.Options["experiments"] = strings.Join(opts.Experiments, ",") job := &df.Job{ ProjectId: opts.Project, @@ -189,18 +194,20 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker AutoscalingSettings: &df.AutoscalingSettings{ MaxNumWorkers: opts.MaxNumWorkers, }, - DiskSizeGb: opts.DiskSizeGb, - DiskType: opts.DiskType, - IpConfiguration: ipConfiguration, - Kind: "harness", - Packages: packages, - WorkerHarnessContainerImage: opts.ContainerImage, - SdkHarnessContainerImages: dfImages, - NumWorkers: 1, - MachineType: opts.MachineType, - Network: opts.Network, - Subnetwork: opts.Subnetwork, - Zone: opts.Zone, + DiskSizeGb: opts.DiskSizeGb, + DiskType: opts.DiskType, + DiskProvisionedIops: opts.DiskProvisionedIops, + DiskProvisionedThroughputMibps: opts.DiskProvisionedThroughputMibps, + IpConfiguration: ipConfiguration, + Kind: "harness", + Packages: packages, + WorkerHarnessContainerImage: opts.ContainerImage, + SdkHarnessContainerImages: dfImages, + NumWorkers: 1, + MachineType: opts.MachineType, + Network: opts.Network, + Subnetwork: opts.Subnetwork, + Zone: opts.Zone, }}, WorkerRegion: opts.WorkerRegion, WorkerZone: opts.WorkerZone, diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go index fb44ff9c0133..4b908900db75 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go @@ -280,3 +280,39 @@ func Test_containerImages(t *testing.T) { } } + +func TestTranslate(t *testing.T) { + ctx := context.Background() + p := &pipepb.Pipeline{} + opts := &JobOptions{ + Project: "test-project", + Name: "test-job", + DiskProvisionedIops: 4000, + DiskProvisionedThroughputMibps: 200, + } + workerURL := "gs://any-location/temp" + modelURL := "gs://any-location/temp" + + job, err := Translate(ctx, p, opts, workerURL, modelURL) + if err != nil { + t.Fatalf("Translate(...) error = %v, want nil", err) + } + + if job.Environment == nil { + t.Fatal("Translate(...) job has nil environment") + } + + if len(job.Environment.WorkerPools) == 0 { + t.Fatal("Translate(...) job has no worker pools") + } + + workerPool := job.Environment.WorkerPools[0] + + if workerPool.DiskProvisionedIops != 4000 { + t.Errorf("DiskProvisionedIops = %v, want %v", workerPool.DiskProvisionedIops, 4000) + } + + if workerPool.DiskProvisionedThroughputMibps != 200 { + t.Errorf("DiskProvisionedThroughputMibps = %v, want %v", workerPool.DiskProvisionedThroughputMibps, 200) + } +} diff --git a/sdks/go/test/integration/expansions.go b/sdks/go/test/integration/expansions.go index 633f88d02930..74700e839269 100644 --- a/sdks/go/test/integration/expansions.go +++ b/sdks/go/test/integration/expansions.go @@ -103,9 +103,9 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { if err != nil { return "", fmt.Errorf("cannot run jar for expansion service labeled \"%s\": %w", label, err) } - + addr := "localhost:" + portStr - + // Use different wait strategies for test mode vs production if es.testMode { // In test mode, use simple wait time for compatibility with mock processes @@ -114,7 +114,7 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { // In production, wait for the jar to start with improved retry logic maxRetries := 30 retryDelay := time.Second - + for i := 0; i < maxRetries; i++ { time.Sleep(retryDelay) // Try to connect to the expansion service to verify it's ready @@ -128,7 +128,7 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { } } } - + es.procs = append(es.procs, proc) es.addrs[label] = addr return addr, nil diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index d60d75283eab..91dd77d6cf02 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -1405,6 +1405,24 @@ def _add_argparse_args(cls, parser): dest='disk_type', default=None, help=('Specifies what type of persistent disk should be used.')) + parser.add_argument( + '--disk_provisioned_iops', + type=int, + default=None, + dest='disk_provisioned_iops', + help=( + 'The provisioned IOPS of the disk. If not set, the Dataflow service' + ' will choose a reasonable default.'), + ) + parser.add_argument( + '--disk_provisioned_throughput_mibps', + type=int, + default=None, + dest='disk_provisioned_throughput_mibps', + help=( + 'The provisioned throughput of the disk in MiB/s. If not set, the' + ' Dataflow service will choose a reasonable default.'), + ) parser.add_argument( '--worker_region', default=None, diff --git a/sdks/python/apache_beam/options/pipeline_options_test.py b/sdks/python/apache_beam/options/pipeline_options_test.py index 215c44156ea6..901f56b99cb6 100644 --- a/sdks/python/apache_beam/options/pipeline_options_test.py +++ b/sdks/python/apache_beam/options/pipeline_options_test.py @@ -444,12 +444,18 @@ def test_worker_options(self): 'abc', '--disk_type', 'def', + '--disk_provisioned_iops', + '4000', + '--disk_provisioned_throughput_mibps', + '200', '--element_processing_timeout_minutes', '10', ]) worker_options = options.view_as(WorkerOptions) self.assertEqual(worker_options.machine_type, 'abc') self.assertEqual(worker_options.disk_type, 'def') + self.assertEqual(worker_options.disk_provisioned_iops, 4000) + self.assertEqual(worker_options.disk_provisioned_throughput_mibps, 200) self.assertEqual(worker_options.element_processing_timeout_minutes, 10) options = PipelineOptions( diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 29cb36071488..871f227c7c73 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -207,6 +207,11 @@ def __init__( pool.diskSizeGb = self.worker_options.disk_size_gb if self.worker_options.disk_type: pool.diskType = self.worker_options.disk_type + if self.worker_options.disk_provisioned_iops is not None: + pool.diskProvisionedIops = self.worker_options.disk_provisioned_iops + if self.worker_options.disk_provisioned_throughput_mibps is not None: + pool.diskProvisionedThroughputMibps = ( + self.worker_options.disk_provisioned_throughput_mibps) if self.worker_options.zone: pool.zone = self.worker_options.zone if self.worker_options.network: diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index 66b1c8e1e5bb..3870bb4a2a4a 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -609,6 +609,23 @@ def test_number_of_worker_harness_threads(self): FAKE_PIPELINE_URL) self.assertEqual(env.proto.workerPools[0].numThreadsPerWorker, 2) + def test_disk_provisioning_options(self): + pipeline_options = PipelineOptions([ + '--temp_location', + 'gs://any-location/temp', + '--disk_provisioned_iops', + '4000', + '--disk_provisioned_throughput_mibps', + '200' + ]) + env = apiclient.Environment([], + pipeline_options, + '2.0.0', + FAKE_PIPELINE_URL) + self.assertEqual(env.proto.workerPools[0].diskProvisionedIops, 4000) + self.assertEqual( + env.proto.workerPools[0].diskProvisionedThroughputMibps, 200) + @mock.patch( 'apache_beam.runners.dataflow.internal.apiclient.' 'beam_version.__version__', diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py index 179e51eb95e8..66c57dd46837 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py @@ -1,6 +1,5 @@ """Generated client library for dataflow version v1b3.""" # NOTE: This file is autogenerated and should not be edited by hand. - from apitools.base.py import base_api from . import dataflow_v1b3_messages as messages diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py index 0c096e73c1ac..f38e1e69d385 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py @@ -2634,8 +2634,8 @@ class FlexTemplateRuntimeEnvironment(_messages.Message): ipConfiguration: Configuration for VM IPs. kmsKeyName: Name for the Cloud KMS key for the job. Key format is: projects//locations//keyRings//cryptoKeys/ - launcherMachineType: The machine type to use for launching the job. The - default is n1-standard-1. + launcherMachineType: The machine type to use for launching the job. If not + set, Dataflow will select a default machine type. machineType: The machine type to use for the job. Defaults to the value from the template if not specified. maxWorkers: The maximum number of Google Compute Engine instances to be @@ -3209,6 +3209,7 @@ class Job(_messages.Message): attempts to create a job with the same name as an active job that already exists, the attempt returns the existing job. The name must match the regular expression `[a-z]([-a-z0-9]{0,1022}[a-z0-9])?` + pausable: Output only. Indicates whether the job can be paused. pipelineDescription: Preliminary field: The format of this data may change at any time. A description of the user pipeline and stages through which it is executed. Created by Cloud Dataflow service. Only retrieved with @@ -3498,22 +3499,23 @@ class AdditionalProperty(_messages.Message): labels = _messages.MessageField('LabelsValue', 10) location = _messages.StringField(11) name = _messages.StringField(12) - pipelineDescription = _messages.MessageField('PipelineDescription', 13) - projectId = _messages.StringField(14) - replaceJobId = _messages.StringField(15) - replacedByJobId = _messages.StringField(16) - requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 17) - runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 18) - satisfiesPzi = _messages.BooleanField(19) - satisfiesPzs = _messages.BooleanField(20) - serviceResources = _messages.MessageField('ServiceResources', 21) - stageStates = _messages.MessageField('ExecutionStageState', 22, repeated=True) - startTime = _messages.StringField(23) - steps = _messages.MessageField('Step', 24, repeated=True) - stepsLocation = _messages.StringField(25) - tempFiles = _messages.StringField(26, repeated=True) - transformNameMapping = _messages.MessageField('TransformNameMappingValue', 27) - type = _messages.EnumField('TypeValueValuesEnum', 28) + pausable = _messages.BooleanField(13) + pipelineDescription = _messages.MessageField('PipelineDescription', 14) + projectId = _messages.StringField(15) + replaceJobId = _messages.StringField(16) + replacedByJobId = _messages.StringField(17) + requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 18) + runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 19) + satisfiesPzi = _messages.BooleanField(20) + satisfiesPzs = _messages.BooleanField(21) + serviceResources = _messages.MessageField('ServiceResources', 22) + stageStates = _messages.MessageField('ExecutionStageState', 23, repeated=True) + startTime = _messages.StringField(24) + steps = _messages.MessageField('Step', 25, repeated=True) + stepsLocation = _messages.StringField(26) + tempFiles = _messages.StringField(27, repeated=True) + transformNameMapping = _messages.MessageField('TransformNameMappingValue', 28) + type = _messages.EnumField('TypeValueValuesEnum', 29) class JobExecutionDetails(_messages.Message): @@ -5342,8 +5344,14 @@ class RuntimeUpdatableParams(_messages.Message): during job creation. Fields: - acceptableBacklogDuration: Optional. The backlog threshold duration in - seconds for autoscaling. Value must be non-negative. + acceptableBacklogDuration: Optional. Deprecated: Use `latency_tier` + instead. The backlog threshold duration in seconds for autoscaling. + Value must be non-negative. + autoscalingTier: Optional. Deprecated: Use `latency_tier` instead. The + backlog threshold tier for autoscaling. Value must be one of "low- + latency", "medium-latency", or "high-latency". + latencyTier: Optional. The backlog threshold tier for autoscaling. Value + must be one of "low-latency", "medium-latency", or "high-latency". maxNumWorkers: The maximum number of workers to cap autoscaling at. This field is currently only supported for Streaming Engine jobs. minNumWorkers: The minimum number of workers to scale down to. This field @@ -5357,9 +5365,11 @@ class RuntimeUpdatableParams(_messages.Message): """ acceptableBacklogDuration = _messages.StringField(1) - maxNumWorkers = _messages.IntegerField(2, variant=_messages.Variant.INT32) - minNumWorkers = _messages.IntegerField(3, variant=_messages.Variant.INT32) - workerUtilizationHint = _messages.FloatField(4) + autoscalingTier = _messages.StringField(2) + latencyTier = _messages.StringField(3) + maxNumWorkers = _messages.IntegerField(4, variant=_messages.Variant.INT32) + minNumWorkers = _messages.IntegerField(5, variant=_messages.Variant.INT32) + workerUtilizationHint = _messages.FloatField(6) class SDKInfo(_messages.Message): @@ -7775,6 +7785,9 @@ class WorkerPool(_messages.Message): defaultPackageSet: The default package set to install. This allows the service to select a default set of packages which are useful to worker harnesses written in a particular language. + diskProvisionedIops: Optional. IOPS provisioned for the root disk for VMs. + diskProvisionedThroughputMibps: Optional. Throughput provisioned for the + root disk for VMs. diskSizeGb: Size of root disk for VMs, in GB. If zero or unspecified, the service will attempt to choose a reasonable default. diskSourceImage: Fully qualified source image for disks. @@ -7938,25 +7951,27 @@ class AdditionalProperty(_messages.Message): autoscalingSettings = _messages.MessageField('AutoscalingSettings', 1) dataDisks = _messages.MessageField('Disk', 2, repeated=True) defaultPackageSet = _messages.EnumField('DefaultPackageSetValueValuesEnum', 3) - diskSizeGb = _messages.IntegerField(4, variant=_messages.Variant.INT32) - diskSourceImage = _messages.StringField(5) - diskType = _messages.StringField(6) - ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 7) - kind = _messages.StringField(8) - machineType = _messages.StringField(9) - metadata = _messages.MessageField('MetadataValue', 10) - network = _messages.StringField(11) - numThreadsPerWorker = _messages.IntegerField(12, variant=_messages.Variant.INT32) - numWorkers = _messages.IntegerField(13, variant=_messages.Variant.INT32) - onHostMaintenance = _messages.StringField(14) - packages = _messages.MessageField('Package', 15, repeated=True) - poolArgs = _messages.MessageField('PoolArgsValue', 16) - sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 17, repeated=True) - subnetwork = _messages.StringField(18) - taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 19) - teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 20) - workerHarnessContainerImage = _messages.StringField(21) - zone = _messages.StringField(22) + diskProvisionedIops = _messages.IntegerField(4) + diskProvisionedThroughputMibps = _messages.IntegerField(5) + diskSizeGb = _messages.IntegerField(6, variant=_messages.Variant.INT32) + diskSourceImage = _messages.StringField(7) + diskType = _messages.StringField(8) + ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 9) + kind = _messages.StringField(10) + machineType = _messages.StringField(11) + metadata = _messages.MessageField('MetadataValue', 12) + network = _messages.StringField(13) + numThreadsPerWorker = _messages.IntegerField(14, variant=_messages.Variant.INT32) + numWorkers = _messages.IntegerField(15, variant=_messages.Variant.INT32) + onHostMaintenance = _messages.StringField(16) + packages = _messages.MessageField('Package', 17, repeated=True) + poolArgs = _messages.MessageField('PoolArgsValue', 18) + sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 19, repeated=True) + subnetwork = _messages.StringField(20) + taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 21) + teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 22) + workerHarnessContainerImage = _messages.StringField(23) + zone = _messages.StringField(24) class WorkerSettings(_messages.Message): @@ -8054,4 +8069,4 @@ class WriteInstruction(_messages.Message): encoding.AddCustomJsonFieldMapping( DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_gcsPath', 'dynamicTemplate.gcsPath') encoding.AddCustomJsonFieldMapping( - DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_stagingLocation', 'dynamicTemplate.stagingLocation') + DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_stagingLocation', 'dynamicTemplate.stagingLocation') \ No newline at end of file From c5c3e6bb54364ca5a8be5b9f64622d6047cb5303 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 29 Apr 2026 10:04:27 -0400 Subject: [PATCH 017/490] Revert "add auto gemini review comment workflow (#38309)" (#38318) This reverts commit febef3501f67f50f46517721a551fe4159b98a9b. --- .github/workflows/gemini-review-comment.yml | 49 --------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/gemini-review-comment.yml diff --git a/.github/workflows/gemini-review-comment.yml b/.github/workflows/gemini-review-comment.yml deleted file mode 100644 index 058a6e46b361..000000000000 --- a/.github/workflows/gemini-review-comment.yml +++ /dev/null @@ -1,49 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: Gemini Review Comment - -on: - workflow_dispatch: - inputs: - pr_number: - description: 'The Pull Request number to review' - required: true - type: string - pull_request_target: - types: [ opened ] - branches: [ master ] - -jobs: - add-comment: - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - uses: actions/github-script@v8 - with: - script: | - const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pr_number; - - if (!prNumber) { - throw new Error("Could not find a PR number from context or inputs."); - } - - await github.rest.issues.createComment({ - issue_number: parseInt(prNumber), - owner: context.repo.owner, - repo: context.repo.repo, - body: '/gemini review' - }); From e0544b696be49b93b4ba13e8ede8524eaeb119a8 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:12:01 -0400 Subject: [PATCH 018/490] Remove mypy configuration (#38266) --- sdks/python/mypy.ini | 110 ------------------------------------------- sdks/python/tox.ini | 1 - 2 files changed, 111 deletions(-) delete mode 100644 sdks/python/mypy.ini diff --git a/sdks/python/mypy.ini b/sdks/python/mypy.ini deleted file mode 100644 index f22258a13953..000000000000 --- a/sdks/python/mypy.ini +++ /dev/null @@ -1,110 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -[mypy] -python_version = 3.10 -ignore_missing_imports = true -follow_imports = normal -warn_no_return = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -show_error_codes = true -files = apache_beam -color_output = true -# uncomment this to see how close we are to being complete -# check_untyped_defs = true -disable_error_code = var-annotated, import-untyped, valid-type, truthy-function, attr-defined, annotation-unchecked - -[tool.mypy] -ignore_missing_imports = true - -[mypy-apache_beam.coders.proto2_coder_test_messages_pb2] -ignore_errors = true - -[mypy-apache_beam.dataframe.*] -ignore_errors = true - -[mypy-apache_beam.examples.*] -ignore_errors = true - -[mypy-apache_beam.io.gcp.gcsfilesystem_test] -# error: Cannot infer type of lambda [misc] -ignore_errors = true - -[mypy-apache_beam.io.gcp.internal.clients.bigquery.bigquery_v2_client] -ignore_errors = true - -[mypy-apache_beam.portability.api.*] -ignore_errors = true - -[mypy-apache_beam.runners.dataflow.internal.clients.dataflow.dataflow_v1b3_client] -ignore_errors = true - -[mypy-apache_beam.typehints.typed_pipeline_test_py3] -# error: Signature of "process" incompatible with supertype "DoFn" [override] -ignore_errors = true - -[mypy-apache_beam.typehints.typehints_test_py3] -# error: Signature of "process" incompatible with supertype "DoFn" [override] -ignore_errors = true - - -# TODO(https://github.com/apache/beam/issues/19737): Remove the lines below. - -[mypy-apache_beam.io.*] -ignore_errors = true - -[mypy-apache_beam.io.filesystem] -ignore_errors = false - -[mypy-apache_beam.io.iobase] -ignore_errors = false - -[mypy-apache_beam.ml.gcp.*] -ignore_errors = true - -[mypy-apache_beam.pipeline] -ignore_errors = true - -[mypy-apache_beam.pvalue] -ignore_errors = true - -[mypy-apache_beam.runners.common] -ignore_errors = true - -[mypy-apache_beam.runners.dataflow.dataflow_runner] -ignore_errors = true - -[mypy-apache_beam.runners.direct.*] -ignore_errors = true - -[mypy-apache_beam.runners.dask.*] -ignore_errors = true - -[mypy-apache_beam.runners.interactive.*] -ignore_errors = true - -[mypy-apache_beam.testing.synthetic_pipeline] -ignore_errors = true - -[mypy-apache_beam.testing.test_stream] -ignore_errors = true - - -[mypy-apache_beam.typehints.*] -ignore_errors = true diff --git a/sdks/python/tox.ini b/sdks/python/tox.ini index d832a54977d9..0f6183ae7715 100644 --- a/sdks/python/tox.ini +++ b/sdks/python/tox.ini @@ -209,7 +209,6 @@ deps = commands = time {toxinidir}/scripts/run_whitespacelint.sh - [testenv:docs] extras = test,gcp,docs,interactive,dataframe,dask deps = From 5c00f11e20f437f06b68dc37659044d44078a7bd Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 29 Apr 2026 10:41:56 -0400 Subject: [PATCH 019/490] Remove Samza runner support (#38263) --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/failing_test.yml | 2 +- .github/ISSUE_TEMPLATE/feature.yml | 2 +- .github/ISSUE_TEMPLATE/task.yml | 2 +- .github/autolabeler.yml | 1 - .github/issue-rules.yml | 4 +- ...PostCommit_Java_ValidatesRunner_Samza.json | 9 - .github/workflows/README.md | 5 - .../workflows/beam_PostCommit_Go_VR_Samza.yml | 85 -- .../beam_PostCommit_Java_PVR_Samza.yml | 104 -- ..._PostCommit_Java_ValidatesRunner_Samza.yml | 100 -- ...ostCommit_Python_ValidatesRunner_Samza.yml | 110 -- .../workflows/beam_PostCommit_XVR_Samza.yml | 107 -- .test-infra/BUILD_STATUS.md | 390 ------ .../src/main/resources/configuration.yaml | 2 +- CHANGES.md | 1 + build.gradle.kts | 3 - runners/samza/build.gradle | 194 --- runners/samza/job-server/build.gradle | 253 ---- .../runners/samza/SamzaExecutionContext.java | 72 -- .../samza/SamzaExecutionEnvironment.java | 44 - .../beam/runners/samza/SamzaJobInvoker.java | 82 -- .../runners/samza/SamzaJobServerDriver.java | 101 -- .../samza/SamzaPipelineExceptionContext.java | 37 - .../samza/SamzaPipelineLifeCycleListener.java | 44 - .../runners/samza/SamzaPipelineOptions.java | 182 --- .../samza/SamzaPipelineOptionsValidator.java | 54 - .../runners/samza/SamzaPipelineResult.java | 161 --- .../runners/samza/SamzaPipelineRunner.java | 89 -- .../samza/SamzaPortablePipelineOptions.java | 32 - .../samza/SamzaPortablePipelineResult.java | 48 - .../beam/runners/samza/SamzaRunner.java | 213 ---- .../samza/SamzaRunnerOverrideConfigs.java | 85 -- .../runners/samza/SamzaRunnerRegistrar.java | 53 - .../beam/runners/samza/TestSamzaRunner.java | 109 -- .../samza/adapter/BoundedSourceSystem.java | 449 ------- .../samza/adapter/UnboundedSourceSystem.java | 533 -------- .../runners/samza/adapter/package-info.java | 20 - .../samza/container/BeamContainerRunner.java | 89 -- .../container/BeamJobCoordinatorRunner.java | 78 -- .../samza/container/ContainerCfgLoader.java | 62 - .../container/ContainerCfgLoaderFactory.java | 30 - .../runners/samza/container/package-info.java | 20 - .../samza/metrics/DoFnRunnerWithMetrics.java | 108 -- .../samza/metrics/FnWithMetricsWrapper.java | 50 - .../samza/metrics/SamzaGBKMetricOp.java | 194 --- .../runners/samza/metrics/SamzaMetricOp.java | 173 --- .../samza/metrics/SamzaMetricOpFactory.java | 73 -- .../samza/metrics/SamzaMetricsContainer.java | 120 -- .../metrics/SamzaTransformMetricRegistry.java | 208 --- .../samza/metrics/SamzaTransformMetrics.java | 142 --- .../runners/samza/metrics/package-info.java | 20 - .../beam/runners/samza/package-info.java | 20 - .../samza/runtime/AsyncDoFnRunner.java | 188 --- .../runners/samza/runtime/BundleManager.java | 90 -- .../samza/runtime/ClassicBundleManager.java | 327 ----- .../beam/runners/samza/runtime/DoFnOp.java | 579 --------- .../runtime/DoFnRunnerWithKeyedInternals.java | 126 -- .../samza/runtime/FutureCollector.java | 67 - .../samza/runtime/FutureCollectorImpl.java | 99 -- .../runners/samza/runtime/GroupByKeyOp.java | 243 ---- .../runners/samza/runtime/KeyedInternals.java | 180 --- .../runners/samza/runtime/KeyedTimerData.java | 216 ---- .../samza/runtime/KvToKeyedWorkItemOp.java | 37 - .../apache/beam/runners/samza/runtime/Op.java | 64 - .../beam/runners/samza/runtime/OpAdapter.java | 237 ---- .../beam/runners/samza/runtime/OpEmitter.java | 41 - .../beam/runners/samza/runtime/OpMessage.java | 161 --- .../samza/runtime/OutputManagerFactory.java | 31 - .../samza/runtime/PortableBundleManager.java | 218 ---- .../runners/samza/runtime/PortableDoFnOp.java | 467 ------- .../samza/runtime/SamzaAssignContext.java | 56 - .../runtime/SamzaDoFnInvokerRegistrar.java | 35 - .../samza/runtime/SamzaDoFnRunners.java | 506 -------- .../SamzaExecutableStageContextFactory.java | 61 - .../SamzaMetricsBundleProgressHandler.java | 158 --- .../runtime/SamzaStateRequestHandlers.java | 179 --- .../runtime/SamzaStoreStateInternals.java | 1131 ----------------- .../runtime/SamzaTimerInternalsFactory.java | 733 ----------- .../samza/runtime/SingletonKeyedWorkItem.java | 49 - ...SplittableParDoProcessKeyedElementsOp.java | 248 ---- .../runners/samza/runtime/WindowAssignOp.java | 53 - .../runners/samza/runtime/package-info.java | 20 - .../runners/samza/state/SamzaMapState.java | 40 - .../runners/samza/state/SamzaSetState.java | 39 - .../runners/samza/state/package-info.java | 20 - .../transforms/GroupWithoutRepartition.java | 60 - .../samza/transforms/UpdatingCombineFn.java | 35 - .../samza/transforms/package-info.java | 20 - .../samza/translation/ConfigBuilder.java | 360 ------ .../samza/translation/ConfigContext.java | 79 -- .../FlattenPCollectionsTranslator.java | 111 -- .../translation/GroupByKeyTranslator.java | 272 ---- .../samza/translation/ImpulseTranslator.java | 80 -- .../samza/translation/PViewToIdMapper.java | 83 -- .../ParDoBoundMultiTranslator.java | 563 -------- .../PortableTranslationContext.java | 117 -- .../samza/translation/ReadTranslator.java | 91 -- .../RedistributeByKeyTranslator.java | 64 - .../translation/ReshuffleTranslator.java | 134 -- .../SamzaImpulseSystemFactory.java | 146 --- .../translation/SamzaPipelineTranslator.java | 211 --- .../SamzaPortablePipelineTranslator.java | 112 -- .../SamzaPortableTranslatorRegistrar.java | 25 - .../samza/translation/SamzaPublishView.java | 66 - .../SamzaPublishViewTransformOverride.java | 66 - .../SamzaPublishViewTranslator.java | 76 -- .../SamzaTestStreamSystemFactory.java | 179 --- .../SamzaTestStreamTranslator.java | 148 --- .../translation/SamzaTransformOverrides.java | 62 - .../translation/SamzaTranslatorRegistrar.java | 25 - .../SplittableParDoTranslators.java | 157 --- .../samza/translation/StateIdParser.java | 69 - .../translation/TransformConfigGenerator.java | 40 - .../translation/TransformTranslator.java | 45 - .../samza/translation/TranslationContext.java | 380 ------ .../translation/WindowAssignTranslator.java | 80 -- .../samza/translation/package-info.java | 20 - .../beam/runners/samza/util/ConfigUtils.java | 43 - .../beam/runners/samza/util/DoFnUtils.java | 75 -- .../beam/runners/samza/util/FutureUtils.java | 67 - .../runners/samza/util/HashIdGenerator.java | 66 - .../samza/util/PipelineJsonRenderer.java | 335 ----- .../samza/util/PortableConfigUtils.java | 43 - .../beam/runners/samza/util/SamzaCoders.java | 77 -- .../util/SamzaPipelineExceptionListener.java | 34 - .../util/SamzaPipelineTranslatorUtils.java | 61 - .../beam/runners/samza/util/StateUtils.java | 39 - .../runners/samza/util/StoreIdGenerator.java | 48 - .../beam/runners/samza/util/WindowUtils.java | 74 -- .../beam/runners/samza/util/package-info.java | 20 - .../samza/src/main/resources/log4j.properties | 23 - .../src/main/resources/samza-conf.properties | 37 - .../SamzaPipelineOptionsValidatorTest.java | 60 - .../adapter/BoundedSourceSystemTest.java | 309 ----- .../samza/adapter/TestBoundedSource.java | 188 --- .../samza/adapter/TestCheckpointMark.java | 40 - .../samza/adapter/TestSourceHelpers.java | 162 --- .../samza/adapter/TestUnboundedSource.java | 208 --- .../adapter/UnboundedSourceSystemTest.java | 405 ------ .../TestSamzaRunnerWithTransformMetrics.java | 323 ----- .../TestSamzaTransformMetricsRegistry.java | 191 --- .../samza/runtime/AsyncDoFnRunnerTest.java | 240 ---- .../runtime/ClassicBundleManagerTest.java | 457 ------- .../runtime/FutureCollectorImplTest.java | 92 -- .../samza/runtime/GroupByKeyOpTest.java | 133 -- .../samza/runtime/KeyedTimerDataTest.java | 65 - .../runtime/PortableBundleManagerTest.java | 178 --- ...SamzaMetricsBundleProgressHandlerTest.java | 187 --- .../runtime/SamzaStoreStateInternalsTest.java | 432 ------- .../SamzaTimerInternalsFactoryTest.java | 752 ----------- .../runtime/SdkHarnessDoFnRunnerTest.java | 48 - .../translation/ConfigGeneratorTest.java | 461 ------- .../translation/SamzaImpulseSystemTest.java | 65 - .../translation/TranslationContextTest.java | 98 -- .../runners/samza/util/DoFnUtilsTest.java | 84 -- .../runners/samza/util/FutureUtilsTest.java | 112 -- .../samza/util/InMemoryMetricsReporter.java | 49 - .../samza/util/PipelineJsonRendererTest.java | 146 --- .../samza/util/PortableConfigUtilsTest.java | 58 - .../samza/util/TestHashIdGenerator.java | 85 -- .../runners/samza/util/WindowUtilsTest.java | 87 -- .../samza/src/test/resources/ExpectedDag.json | 373 ------ .../src/test/resources/log4j-test.properties | 26 - .../large_wordcount/large_wordcount.go | 1 - sdks/go/pkg/beam/runners/samza/samza.go | 35 - sdks/go/pkg/beam/x/beamx/run.go | 1 - sdks/go/test/build.gradle | 29 - sdks/go/test/integration/expansions_test.go | 1 - sdks/go/test/integration/integration.go | 49 - .../io/mongodbio/mongodbio_test.go | 1 - .../io/xlang/debezium/debezium_test.go | 1 - .../integration/io/xlang/jdbc/jdbc_test.go | 1 - .../integration/io/xlang/kafka/kafka_test.go | 1 - .../integration/primitives/primitives_test.go | 1 - .../integration/synthetic/synthetic_test.go | 2 +- .../integration/wordcount/wordcount_test.go | 1 - sdks/go/test/integration/xlang/xlang_test.go | 1 - sdks/go/test/regression/lperror_test.go | 1 - sdks/go/test/regression/pardo_test.go | 1 - sdks/go/test/run_validatesrunner_tests.sh | 17 +- .../resources/archetype-resources/pom.xml | 11 - .../resources/archetype-resources/pom.xml | 10 - .../fn_api_runner/fn_runner_test.py | 3 - .../portability/portable_runner_test.py | 2 - .../runners/portability/samza_runner_test.py | 200 --- sdks/python/test-suites/gradle.properties | 3 - sdks/python/test-suites/portable/build.gradle | 6 - .../python/test-suites/portable/common.gradle | 27 - sdks/python/tox.ini | 6 - settings.gradle.kts | 2 - .../site/content/en/blog/capability-matrix.md | 2 +- .../postcommits-policies-details.md | 2 +- .../content/en/documentation/runners/samza.md | 4 +- .../content/en/get-started/beam-overview.md | 5 +- .../content/en/get-started/quickstart-java.md | 21 - .../en/get-started/wordcount-example.md | 43 - website/www/site/content/en/roadmap/_index.md | 2 +- website/www/site/content/en/roadmap/go-sdk.md | 2 +- .../site/content/en/roadmap/samza-runner.md | 2 +- website/www/site/data/capability_matrix.yaml | 34 - .../partials/section-menu/en/runners.html | 1 - 202 files changed, 23 insertions(+), 23511 deletions(-) delete mode 100644 .github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json delete mode 100644 .github/workflows/beam_PostCommit_Go_VR_Samza.yml delete mode 100644 .github/workflows/beam_PostCommit_Java_PVR_Samza.yml delete mode 100644 .github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml delete mode 100644 .github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml delete mode 100644 .github/workflows/beam_PostCommit_XVR_Samza.yml delete mode 100644 .test-infra/BUILD_STATUS.md delete mode 100644 runners/samza/build.gradle delete mode 100644 runners/samza/job-server/build.gradle delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/TestSamzaRunner.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystem.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystem.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamContainerRunner.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamJobCoordinatorRunner.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoader.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoaderFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/container/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/DoFnRunnerWithMetrics.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/FnWithMetricsWrapper.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaGBKMetricOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOpFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricsContainer.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetricRegistry.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetrics.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/BundleManager.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/ClassicBundleManager.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnRunnerWithKeyedInternals.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollector.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollectorImpl.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/GroupByKeyOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedInternals.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedTimerData.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KvToKeyedWorkItemOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/Op.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpEmitter.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpMessage.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OutputManagerFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableBundleManager.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableDoFnOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaAssignContext.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnInvokerRegistrar.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaExecutableStageContextFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandler.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStateRequestHandlers.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternals.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SingletonKeyedWorkItem.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SplittableParDoProcessKeyedElementsOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/WindowAssignOp.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaMapState.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaSetState.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/state/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/GroupWithoutRepartition.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigBuilder.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigContext.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/FlattenPCollectionsTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/GroupByKeyTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ImpulseTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PViewToIdMapper.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ParDoBoundMultiTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PortableTranslationContext.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReadTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/RedistributeByKeyTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReshuffleTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPipelineTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortablePipelineTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortableTranslatorRegistrar.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishView.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTransformOverride.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamSystemFactory.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTransformOverrides.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTranslatorRegistrar.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SplittableParDoTranslators.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/StateIdParser.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformConfigGenerator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TranslationContext.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/WindowAssignTranslator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/translation/package-info.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/ConfigUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/DoFnUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/FutureUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/HashIdGenerator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/PipelineJsonRenderer.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaCoders.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineExceptionListener.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineTranslatorUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/StateUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/StoreIdGenerator.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/WindowUtils.java delete mode 100644 runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java delete mode 100644 runners/samza/src/main/resources/log4j.properties delete mode 100644 runners/samza/src/main/resources/samza-conf.properties delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidatorTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystemTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestBoundedSource.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestCheckpointMark.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestSourceHelpers.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestUnboundedSource.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystemTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaRunnerWithTransformMetrics.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaTransformMetricsRegistry.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java delete mode 100644 runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java delete mode 100644 runners/samza/src/test/resources/ExpectedDag.json delete mode 100644 runners/samza/src/test/resources/log4j-test.properties delete mode 100644 sdks/go/pkg/beam/runners/samza/samza.go delete mode 100644 sdks/python/apache_beam/runners/portability/samza_runner_test.py diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 081c266e43c7..594dbbc59b22 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -72,7 +72,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/ISSUE_TEMPLATE/failing_test.yml b/.github/ISSUE_TEMPLATE/failing_test.yml index 0699621e2b16..bc461771d4dc 100644 --- a/.github/ISSUE_TEMPLATE/failing_test.yml +++ b/.github/ISSUE_TEMPLATE/failing_test.yml @@ -78,7 +78,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 049c0049d29a..abde43ea7302 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -66,7 +66,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml index 25d7e55e4e90..c02ff781ad44 100644 --- a/.github/ISSUE_TEMPLATE/task.yml +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -67,7 +67,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index 6cd7516e7440..0eb9a04b6d2c 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -89,6 +89,5 @@ io: ["sdks/go/pkg/beam/io/**/*", "sdks/java/io/**/*", "sdks/python/apache_beam/ "local": ["runners/local-java/**/*"] "portability": ["runners/portability/**/*"] "prism": ["runners/prism/**/*", "sdks/go/pkg/beam/runners/prism/**/*", "sdks/go/cmd/prism/**/*", "sdks/python/apache_beam/runners/portability/prism_runner.py","sdks/python/apache_beam/runners/portability/prism_runner_test.py"] -"samza": ["runners/samza/**/*"] "spark": ["runners/spark/**/*", "sdks/go/pkg/beam/runners/spark/**/*"] "twister2": ["runners/twister2/**/*"] diff --git a/.github/issue-rules.yml b/.github/issue-rules.yml index c4acb2945575..4fafb1c87f78 100644 --- a/.github/issue-rules.yml +++ b/.github/issue-rules.yml @@ -52,8 +52,8 @@ rules: addLabels: ['spark'] - contains: '[x] Component: Flink' addLabels: ['flink'] -- contains: '[x] Component: Samza' - addLabels: ['samza'] +- contains: '[x] Component: Prism' + addLabels: ['prism'] - contains: '[x] Component: Twister2' addLabels: ['twister2'] - contains: '[x] Component: Hazelcast' diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json deleted file mode 100644 index 6acc30e8280f..000000000000 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", - "https://github.com/apache/beam/pull/35177": "Introducing WindowedValueReceiver to runners", - "comment": "Modify this file in a trivial way to cause this test suite to run", - "https://github.com/apache/beam/pull/31156": "noting that PR #31156 should run this test", - "https://github.com/apache/beam/pull/31270": "re-add specialized Samza translation of Redistribute", - "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface", - "modification": 1 -} diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c1fc56355461..f8c77b7180db 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -325,7 +325,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Go ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go.yml) | N/A |`beam_PostCommit_Go.json`| [![.github/workflows/beam_PostCommit_Go.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go.yml?query=event%3Aschedule) | | [ PostCommit Go Dataflow ARM](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_Dataflow_ARM.yml) | N/A |`beam_PostCommit_Go_Dataflow_ARM.json`| [![.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_Dataflow_ARM.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_Dataflow_ARM.yml?query=event%3Aschedule) | | [ PostCommit Go VR Flink](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Flink.yml) | N/A |`beam_PostCommit_Go_VR_Flink.json`| [![.github/workflows/beam_PostCommit_Go_VR_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Go VR Samza](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Samza.yml) | N/A |`beam_PostCommit_Go_VR_Samza.json`| [![.github/workflows/beam_PostCommit_Go_VR_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Samza.yml?query=event%3Aschedule) | | [ PostCommit Go VR Spark](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Spark.yml) | N/A |`beam_PostCommit_Go_VR_Spark.json`| [![.github/workflows/beam_PostCommit_Go_VR_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java Avro Versions ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Avro_Versions.yml) | N/A |`beam_PostCommit_Java_Avro_Versions.json`| [![.github/workflows/beam_PostCommit_Java_Avro_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Avro_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Avro_Versions.yml?query=event%3Aschedule) | | [ PostCommit Java BigQueryEarlyRollout ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml) | N/A |`beam_PostCommit_Java_BigQueryEarlyRollout.json`| [![.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml?query=event%3Aschedule) | @@ -356,7 +355,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java Nexmark Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Nexmark_Spark.yml) | N/A |`beam_PostCommit_Java_Nexmark_Spark.json`| [![.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Nexmark_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Nexmark_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Flink Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Flink_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Flink Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Flink_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml?query=event%3Aschedule) | -| [ PostCommit Java PVR Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Samza.yml) | N/A |`beam_PostCommit_Java_PVR_Samza.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Samza.yml?query=event%3Aschedule) | | [ PostCommit Java SingleStoreIO IT ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml) | N/A |`beam_PostCommit_Java_SingleStoreIO_IT.json`| [![.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Spark3 Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Spark3_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Spark Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Spark_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml?query=event%3Aschedule) | @@ -372,7 +370,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Direct.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Flink Java8 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink_Java8.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Samza.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Spark Java8 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark_Java8.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner SparkStructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml?query=event%3Aschedule) | @@ -395,7 +392,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Python ValidatesContainer Dataflow With RC ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml) | ['3.9','3.10','3.11','3.12'] |`beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml?query=event%3Aschedule) | | [ PostCommit Python ValidatesRunner Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml) | ['3.9','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Python ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml) | ['3.9','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Python ValidatesRunner Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml) | ['3.9','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Samza.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml?query=event%3Aschedule) | | [ PostCommit Python ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml) | ['3.9','3.10','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang Gcp Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml) | N/A |`beam_PostCommit_Python_Xlang_Gcp_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang Gcp Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_Gcp_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml?query=event%3Aschedule) | @@ -409,7 +405,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit XVR JavaUsingPython Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml) | N/A |`beam_PostCommit_XVR_JavaUsingPython_Dataflow.json`| [![.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit XVR PythonUsingJava Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml) | N/A |`beam_PostCommit_XVR_PythonUsingJava_Dataflow.json`| [![.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit XVR PythonUsingJavaSQL Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml) | N/A |`beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.json`| [![.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml?query=event%3Aschedule) | -| [ PostCommit XVR Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Samza.yml) | N/A |`beam_PostCommit_XVR_Samza.json`| [![.github/workflows/beam_PostCommit_XVR_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Samza.yml?query=event%3Aschedule) | | [ PostCommit XVR Spark3 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Spark3.yml) | N/A |`beam_PostCommit_XVR_Spark3.json`| [![.github/workflows/beam_PostCommit_XVR_Spark3.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Spark3.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Spark3.yml?query=event%3Aschedule) | | [ PostCommit YAML Xlang Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml) | N/A |`beam_PostCommit_Yaml_Xlang_Direct.json`| [![.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml?query=event%3Aschedule) | | [ Python Validates Container Dataflow ARM ](https://github.com/apache/beam/actions/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml) | ['3.9','3.10','3.11','3.12'] |`beam_Python_ValidatesContainer_Dataflow_ARM.json`|[![.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml](https://github.com/apache/beam/actions/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_PostCommit_Go_VR_Samza.yml b/.github/workflows/beam_PostCommit_Go_VR_Samza.yml deleted file mode 100644 index ac6f2dd5ed85..000000000000 --- a/.github/workflows/beam_PostCommit_Go_VR_Samza.yml +++ /dev/null @@ -1,85 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: PostCommit Go VR Samza - -on: - schedule: - - cron: '30 3/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Go_VR_Samza.json'] - workflow_dispatch: - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: read - checks: read - contents: read - deployments: read - id-token: none - issues: read - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Go_VR_Samza: - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run Go Samza ValidatesRunner' - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - strategy: - matrix: - job_name: ["beam_PostCommit_Go_VR_Samza"] - job_phrase: ["Run Go Samza ValidatesRunner"] - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 - with: - java-version: | - 8 - 11 - - name: run Go Samza ValidatesRunner script - env: - CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :sdks:go:test:samzaValidatesRunner -Pjava8Home=$JAVA_HOME_8_X64 -PtestJavaVersion=8 diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Samza.yml b/.github/workflows/beam_PostCommit_Java_PVR_Samza.yml deleted file mode 100644 index dd28b4555e5f..000000000000 --- a/.github/workflows/beam_PostCommit_Java_PVR_Samza.yml +++ /dev/null @@ -1,104 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -name: PostCommit Java PVR Samza - -on: - schedule: - - cron: '15 4/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_PVR_Samza.json'] - workflow_dispatch: - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Java_PVR_Samza: - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 120 - strategy: - matrix: - job_name: [beam_PostCommit_Java_PVR_Samza] - job_phrase: [Run Java Samza PortableValidatesRunner] - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run Java Samza PortableValidatesRunner' - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 - with: - java-version: | - 8 - 11 - - name: run PostCommit Java Samza script - env: - CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:samza:job-server:validatesPortableRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml deleted file mode 100644 index 7f65b2ccee33..000000000000 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml +++ /dev/null @@ -1,100 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: PostCommit Java ValidatesRunner Samza - -on: - schedule: - - cron: '45 4/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json'] - workflow_dispatch: - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Java_ValidatesRunner_Samza: - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 - strategy: - matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Samza] - job_phrase: [Run Samza ValidatesRunner] - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run Samza ValidatesRunner' - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 - with: - java-version: | - 8 - 11 - - name: run validatesRunner script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:samza:validatesRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml deleted file mode 100644 index 144de32d2be6..000000000000 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml +++ /dev/null @@ -1,110 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: PostCommit Python ValidatesRunner Samza - -on: - schedule: - - cron: '15 5/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Samza.json'] - workflow_dispatch: - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Python_ValidatesRunner_Samza: - if: | - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - startsWith(github.event.comment.body, 'Run Python Samza ValidatesRunner') - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - strategy: - fail-fast: false - matrix: - job_name: ["beam_PostCommit_Python_ValidatesRunner_Samza"] - job_phrase: ["Run Python Samza ValidatesRunner"] - python_version: ['3.10', '3.14'] - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} ${{ matrix.python_version }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 - with: - java-version: | - 8 - 11 - python-version: ${{ matrix.python_version }} - - name: Set PY_VER_CLEAN - id: set_py_ver_clean - run: | - PY_VER=${{ matrix.python_version }} - PY_VER_CLEAN=${PY_VER//.} - echo "py_ver_clean=$PY_VER_CLEAN" >> $GITHUB_OUTPUT - - name: Run samzaValidatesRunner script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :sdks:python:test-suites:portable:py${{steps.set_py_ver_clean.outputs.py_ver_clean}}:samzaValidatesRunner - arguments: | - -PpythonVersion=${{ matrix.python_version }} \ - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - - name: Archive Python Test Results - uses: actions/upload-artifact@v4 - if: failure() - with: - name: Python Test Results - path: '**/pytest*.xml' - - name: Publish Python Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/pytest*.xml' - large_files: true \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_XVR_Samza.yml b/.github/workflows/beam_PostCommit_XVR_Samza.yml deleted file mode 100644 index 6b6905f6ce72..000000000000 --- a/.github/workflows/beam_PostCommit_XVR_Samza.yml +++ /dev/null @@ -1,107 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: PostCommit XVR Samza - -on: - schedule: - - cron: '45 5/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_XVR_Samza.json'] - workflow_dispatch: - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: read - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_XVR_Samza: - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run XVR_Samza PostCommit' - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - strategy: - matrix: - job_name: ["beam_PostCommit_XVR_Samza"] - job_phrase: ["Run XVR_Samza PostCommit"] - python_version: ['3.10','3.14'] - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} ${{ matrix.python_version }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 - with: - java-version: | - 8 - 11 - python-version: | - ${{ matrix.python_version }} - - name: run PostCommit XVR Samza script - env: - CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:samza:job-server:validatesCrossLanguageRunner - arguments: | - -PpythonVersion=${{ matrix.python_version }} \ - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file diff --git a/.test-infra/BUILD_STATUS.md b/.test-infra/BUILD_STATUS.md deleted file mode 100644 index 1972076df1e2..000000000000 --- a/.test-infra/BUILD_STATUS.md +++ /dev/null @@ -1,390 +0,0 @@ - - - -`ValidatesRunner` compliance status (on master branch) --------------------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LangULRDataflowFlinkSamzaSparkTwister2
Go--- - - Build Status - - - - Build Status - - - - Build Status - - - - Build Status - - ---
Java - - Build Status - - - - Build Status -
- - Build Status -
- - Build Status -
- - Build Status -
- - Build Status -
-
- - Build Status -
- - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status -
- - Build Status - -
- - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status - -
Python--- - - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status -
- - Build Status - -
- - Build Status - - - - Build Status - - ---
XLang - - Build Status - - - - Build Status -
- - Build Status -
- - Build Status -
-
- - Build Status - - - - Build Status - - - - Build Status - - ---
- -Examples testing status on various runners --------------------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LangULRDataflowFlinkSamzaSparkTwister2
Go---------------------
Java--- - - Build Status -
- - Build Status -
- - Build Status -
-
---------------
Python---------------------
XLang---------------------
- -Post-Commit SDK/Transform Integration Tests Status (on master branch) ------------------------------------------------------------------------------------------------- - - - - - - - - - - - - - - - - -
GoJavaPython
- - Build Status - - - - Build Status - - - - Build Status -
- - Build Status -
- - Build Status - -
- -Pre-Commit Tests Status (on master branch) ------------------------------------------------------------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
---JavaPythonGoWebsiteWhitespaceTypescript
Non-portable - - Build Status -
-
- - Build Status -
- - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status - - - - Build Status - - - - Build Status - - - - Build Status - -
Portable--- - - Build Status - - - - Build Status - - ---------
- -See [.test-infra/jenkins/README](https://github.com/apache/beam/blob/master/.test-infra/jenkins/README.md) for trigger phrase, status and link of all Jenkins jobs. - - -GitHub Actions Tests Status (on master branch) ------------------------------------------------------------------------------------------------- -[![Build python source distribution and wheels](https://github.com/apache/beam/workflows/Build%20python%20source%20distribution%20and%20wheels/badge.svg?branch=master&event=schedule)](https://github.com/apache/beam/actions?query=workflow%3A%22Build+python+source+distribution+and+wheels%22+branch%3Amaster+event%3Aschedule) -[![Python tests](https://github.com/apache/beam/workflows/Python%20tests/badge.svg?branch=master&event=schedule)](https://github.com/apache/beam/actions?query=workflow%3A%22Python+Tests%22+branch%3Amaster+event%3Aschedule) -[![Java tests](https://github.com/apache/beam/workflows/Java%20Tests/badge.svg?branch=master&event=schedule)](https://github.com/apache/beam/actions?query=workflow%3A%22Java+Tests%22+branch%3Amaster+event%3Aschedule) - -See [CI.md](https://github.com/apache/beam/blob/master/CI.md) for more information about GitHub Actions CI. diff --git a/.test-infra/validate-runner/src/main/resources/configuration.yaml b/.test-infra/validate-runner/src/main/resources/configuration.yaml index 31934d2146d7..4eb8cd018f99 100644 --- a/.test-infra/validate-runner/src/main/resources/configuration.yaml +++ b/.test-infra/validate-runner/src/main/resources/configuration.yaml @@ -19,6 +19,6 @@ batch: - dataflow: beam_PostCommit_Java_VR_Dataflow_V2 stream: - flink: beam_PostCommit_Java_PVR_Flink_Streaming - - samza: beam_PostCommit_Java_PVR_Samza + - spark: beam_PostCommit_Java_PVR_Spark3_Streaming server: https://ci-beam.apache.org/ jsonapi: testReport/api/json diff --git a/CHANGES.md b/CHANGES.md index 79700735c06a..28aa22f66811 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -85,6 +85,7 @@ ## Deprecations * X behavior is deprecated and will be removed in X versions ([#X](https://github.com/apache/beam/issues/X)). +* Removed Samza Runner support ([#35448](https://github.com/apache/beam/issues/35448)). ## Bugfixes diff --git a/build.gradle.kts b/build.gradle.kts index 805851c59696..4af8fa3f1ab4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -281,8 +281,6 @@ tasks.register("javaPreCommit") { dependsOn(":runners:local-java:build") dependsOn(":runners:portability:java:build") dependsOn(":runners:prism:java:build") - dependsOn(":runners:samza:build") - dependsOn(":runners:samza:job-server:build") dependsOn(":runners:spark:3:build") dependsOn(":runners:spark:3:job-server:build") dependsOn(":runners:twister2:build") @@ -410,7 +408,6 @@ tasks.register("javaPostCommit") { } tasks.register("javaPostCommitSickbay") { - dependsOn(":runners:samza:validatesRunnerSickbay") for (version in project.ext.get("allFlinkVersions") as Array<*>) { dependsOn(":runners:flink:${version}:validatesRunnerSickbay") } diff --git a/runners/samza/build.gradle b/runners/samza/build.gradle deleted file mode 100644 index 626588b79a5d..000000000000 --- a/runners/samza/build.gradle +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import groovy.json.JsonOutput - -plugins { id 'org.apache.beam.module' } -applyJavaNature( - exportJavadoc: false, - classesTriggerCheckerBugs: [ - 'GroupWithoutRepartition': 'https://github.com/typetools/checker-framework/issues/3791', - ], - automaticModuleName: 'org.apache.beam.runners.samza', -) - -description = "Apache Beam :: Runners :: Samza" - -/* - * We need to rely on manually specifying these evaluationDependsOn to ensure that - * the following projects are evaluated before we evaluate this project. This is because - * we are attempting to reference the "sourceSets.test.output" directly. - */ -evaluationDependsOn(":sdks:java:core") - -configurations { - validatesRunner -} - -def samza_version = "1.6.0" - -dependencies { - implementation library.java.vendored_guava_32_1_2_jre - implementation project(path: ":sdks:java:core", configuration: "shadow") - implementation project(":runners:core-java") - implementation project(":runners:java-fn-execution") - implementation project(":runners:java-job-service") - implementation library.java.jackson_annotations - implementation library.java.slf4j_api - implementation library.java.joda_time - implementation library.java.args4j - implementation library.java.commons_io - implementation library.java.commons_collections - runtimeOnly "org.rocksdb:rocksdbjni:6.15.2" - runtimeOnly "org.scala-lang:scala-library:2.11.8" - implementation "org.apache.samza:samza-api:$samza_version" - implementation "org.apache.samza:samza-core_2.11:$samza_version" - runtimeOnly "org.apache.samza:samza-kafka_2.11:$samza_version" - runtimeOnly "org.apache.samza:samza-kv_2.11:$samza_version" - implementation "org.apache.samza:samza-kv-rocksdb_2.11:$samza_version" - implementation "org.apache.samza:samza-kv-inmemory_2.11:$samza_version" - implementation "org.apache.samza:samza-yarn_2.11:$samza_version" - compileOnly library.java.error_prone_annotations - runtimeOnly "org.apache.kafka:kafka-clients:2.0.1" - implementation library.java.vendored_grpc_1_69_0 - implementation project(path: ":model:fn-execution", configuration: "shadow") - implementation project(path: ":model:job-management", configuration: "shadow") - implementation project(path: ":model:pipeline", configuration: "shadow") - testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") - testImplementation project(path: ":runners:core-java", configuration: "testRuntimeMigration") - testImplementation library.java.hamcrest - testImplementation library.java.junit - testImplementation library.java.mockito_core - testImplementation library.java.jackson_dataformat_yaml - testImplementation library.java.google_code_gson - validatesRunner project(path: ":sdks:java:core", configuration: "shadowTest") - validatesRunner project(path: ":runners:core-java", configuration: "testRuntimeMigration") - validatesRunner project(project.path) -} - -configurations.all { - exclude group: "org.slf4j", module: "slf4j-jdk14" -} - -def sickbayTests = [ - // TODO(https://github.com/apache/beam/issues/21033) - 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInGlobalWindowBatchSizeByteSizeFn', - 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInStreamingMode', - 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithShardedKeyInGlobalWindow', - // TODO(https://github.com/apache/beam/issues/21036) - 'org.apache.beam.sdk.transforms.MapElementsTest.testMapSimpleFunction', - // TODO(https://github.com/apache/beam/issues/21035) - 'org.apache.beam.sdk.transforms.ViewTest.testEmptySingletonSideInput', - 'org.apache.beam.sdk.transforms.ViewTest.testNonSingletonSideInput', - // TODO(https://github.com/apache/beam/issues/21037) - 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsBackwardsInTimeShouldThrow', - 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsWithNullTimestampShouldThrow', - // TODO(https://github.com/apache/beam/issues/21039) - 'org.apache.beam.sdk.io.FileIOTest*', - // TODO(https://github.com/apache/beam/issues/21038) - 'org.apache.beam.sdk.io.AvroIOTest*', - // TODO(https://github.com/apache/beam/issues/21040) - 'org.apache.beam.sdk.PipelineTest.testEmptyPipeline', - // TODO(https://github.com/apache/beam/issues/21041) - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingNPException', - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingIOException', - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingNPException', - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingIOException', - // https://github.com/apache/beam/issues/19344 - 'org.apache.beam.sdk.io.BoundedReadFromUnboundedSourceTest.testTimeBound', - // https://github.com/apache/beam/issues/31725 - 'org.apache.beam.sdk.io.TextIOWriteTest.testWriteUnboundedWithCustomBatchParameters', -] -tasks.register("validatesRunner", Test) { - group = "Verification" - description "Validates Samza runner" - systemProperty "beamTestPipelineOptions", JsonOutput.toJson([ - "--runner=TestSamzaRunner", - ]) - - classpath = configurations.validatesRunner - testClassesDirs = files(project(":sdks:java:core").sourceSets.test.output.classesDirs) - useJUnit { - includeCategories 'org.apache.beam.sdk.testing.NeedsRunner' - includeCategories 'org.apache.beam.sdk.testing.ValidatesRunner' - excludeCategories 'org.apache.beam.sdk.testing.UsesExternalService' - // Should be run only in a properly configured SDK harness environment - excludeCategories 'org.apache.beam.sdk.testing.UsesSdkHarnessEnvironment' - excludeCategories 'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo' - excludeCategories 'org.apache.beam.sdk.testing.UsesSchema' - excludeCategories 'org.apache.beam.sdk.testing.LargeKeys$Above100MB' - excludeCategories 'org.apache.beam.sdk.testing.UsesAttemptedMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesCommittedMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesTestStreamWithProcessingTime' - excludeCategories 'org.apache.beam.sdk.testing.UsesMetricsPusher' - excludeCategories 'org.apache.beam.sdk.testing.UsesParDoLifecycle' - excludeCategories 'org.apache.beam.sdk.testing.UsesProcessingTimeTimers' - excludeCategories 'org.apache.beam.sdk.testing.UsesStrictTimerOrdering' - excludeCategories 'org.apache.beam.sdk.testing.UsesOnWindowExpiration' - excludeCategories 'org.apache.beam.sdk.testing.UsesOrderedListState' - excludeCategories 'org.apache.beam.sdk.testing.UsesMultimapState' - excludeCategories 'org.apache.beam.sdk.testing.UsesBundleFinalizer' - excludeCategories 'org.apache.beam.sdk.testing.UsesLoopingTimer' - excludeCategories 'org.apache.beam.sdk.testing.UsesTriggeredSideInputs' - } - filter { - for (String test : sickbayTests) { - excludeTestsMatching test - } - // TODO(BEAM-10025) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestampDefaultUnbounded' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testRelativeTimerWithOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20847) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testFirstElementLate' - // TODO(https://github.com/apache/beam/issues/20846) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testLateDataAccumulating' - - // These tests fail since there is no support for side inputs in Samza's unbounded splittable DoFn integration - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testWindowedSideInputWithCheckpointsUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testSideInputUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testWindowedSideInputUnbounded' - // These tests produce the output but either the pipeline doesn't shutdown or PAssert fails - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testAdditionalOutputUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testPairWithIndexBasicUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testPairWithIndexWindowedTimestampedUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testOutputAfterCheckpointUnbounded' - } -} - -tasks.register("validatesRunnerSickbay", Test) { - group = "Verification" - description "Validates Samza runner (Sickbay Tests)" - systemProperty "beamTestPipelineOptions", JsonOutput.toJson([ - "--runner=TestSamzaRunner", - ]) - - classpath = configurations.validatesRunner - testClassesDirs = files(project(":sdks:java:core").sourceSets.test.output.classesDirs) - - filter { - for (String test : sickbayTests) { - includeTestsMatching test - } - } -} - -// Generates :runners:samza:runQuickstartJavaSamza -createJavaExamplesArchetypeValidationTask(type: 'Quickstart', runner:'Samza') diff --git a/runners/samza/job-server/build.gradle b/runners/samza/job-server/build.gradle deleted file mode 100644 index 7ffb2becd6d0..000000000000 --- a/runners/samza/job-server/build.gradle +++ /dev/null @@ -1,253 +0,0 @@ -import org.apache.beam.gradle.BeamModulePlugin - -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -apply plugin: 'org.apache.beam.module' -apply plugin: 'application' -// we need to set mainClassName before applying shadow plugin -mainClassName = "org.apache.beam.runners.samza.SamzaJobServerDriver" - -applyJavaNature( - automaticModuleName: 'org.apache.beam.runners.samza.jobserver', - archivesBaseName: project.hasProperty('archives_base_name') ? archives_base_name : archivesBaseName, - validateShadowJar: false, - exportJavadoc: false, - shadowClosure: { - append "reference.conf" - }, -) - -def samzaRunnerProject = project.parent.path - -description = "Apache Beam :: Runners :: Samza :: Job Server" - -configurations { - validatesPortableRunner -} - -dependencies { - implementation project(samzaRunnerProject) - permitUnusedDeclared project(samzaRunnerProject) - runtimeOnly group: "org.slf4j", name: "jcl-over-slf4j", version: dependencies.create(project.library.java.slf4j_api).getVersion() - validatesPortableRunner project(path: samzaRunnerProject, configuration: "testRuntimeMigration") - validatesPortableRunner project(path: ":sdks:java:core", configuration: "shadowTest") - validatesPortableRunner project(path: ":runners:core-java", configuration: "testRuntimeMigration") - validatesPortableRunner project(path: ":runners:portability:java", configuration: "testRuntimeMigration") - runtimeOnly library.java.slf4j_simple -} - -runShadow { - args = [] -} - -def portableValidatesRunnerTask(String name, boolean docker) { - def tempDir = File.createTempDir() - def pipelineOptions = [ - "--configOverride={\"job.non-logged.store.base.dir\":\"" + tempDir + "\"}" - ] - createPortableValidatesRunnerTask( - name: "validatesPortableRunner${name}", - jobServerDriver: "org.apache.beam.runners.samza.SamzaJobServerDriver", - jobServerConfig: "--job-host=localhost,--job-port=0,--artifact-port=0,--expansion-port=0", - testClasspathConfiguration: configurations.validatesPortableRunner, - numParallelTests: 1, - pipelineOpts: pipelineOptions, - environment: docker ? BeamModulePlugin.PortableValidatesRunnerConfiguration.Environment.DOCKER : BeamModulePlugin.PortableValidatesRunnerConfiguration.Environment.EMBEDDED, - testCategories: { - if (docker) { - includeCategories 'org.apache.beam.sdk.testing.UsesSdkHarnessEnvironment' - return - } - // TODO(https://github.com/apache/beam/issues/22657) - // includeCategories 'org.apache.beam.sdk.testing.NeedsRunner' - includeCategories 'org.apache.beam.sdk.testing.ValidatesRunner' - // Should be run only in a properly configured SDK harness environment - excludeCategories 'org.apache.beam.sdk.testing.UsesSdkHarnessEnvironment' - // TODO: BEAM-12350 - excludeCategories 'org.apache.beam.sdk.testing.UsesAttemptedMetrics' - // TODO: BEAM-12681 - excludeCategories 'org.apache.beam.sdk.testing.FlattenWithHeterogeneousCoders' - // Larger keys are possible, but they require more memory. - excludeCategories 'org.apache.beam.sdk.testing.LargeKeys$Above10MB' - excludeCategories 'org.apache.beam.sdk.testing.UsesCommittedMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesExternalService' - excludeCategories 'org.apache.beam.sdk.testing.UsesCustomWindowMerging' - excludeCategories 'org.apache.beam.sdk.testing.UsesFailureMessage' - excludeCategories 'org.apache.beam.sdk.testing.UsesGaugeMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesMapState' - excludeCategories 'org.apache.beam.sdk.testing.UsesMultimapState' - excludeCategories 'org.apache.beam.sdk.testing.UsesSetState' - excludeCategories 'org.apache.beam.sdk.testing.UsesOrderedListState' - excludeCategories 'org.apache.beam.sdk.testing.UsesStrictTimerOrdering' - excludeCategories 'org.apache.beam.sdk.testing.UsesOnWindowExpiration' - excludeCategories 'org.apache.beam.sdk.testing.UsesBundleFinalizer' - excludeCategories 'org.apache.beam.sdk.testing.UsesOrderedListState' - excludeCategories 'org.apache.beam.sdk.testing.UsesBoundedSplittableParDo' - excludeCategories 'org.apache.beam.sdk.testing.UsesTestStreamWithProcessingTime' - // TODO(https://github.com/apache/beam/issues/21023) - excludeCategories 'org.apache.beam.sdk.testing.UsesTestStreamWithMultipleStages' - excludeCategories 'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo' - excludeCategories 'org.apache.beam.sdk.testing.UsesLoopingTimer' - excludeCategories 'org.apache.beam.sdk.testing.UsesTriggeredSideInputs' - }, - testFilter: { - // TODO(https://github.com/apache/beam/issues/21042) - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testFlattenWithDifferentInputAndOutputCoders2" - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testEmptyFlattenAsSideInput" - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testFlattenPCollectionsEmptyThenParDo" - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testFlattenPCollectionsEmpty" - // TODO(BEAM-10025) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestampDefaultUnbounded' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20847) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testFirstElementLate' - // TODO(https://github.com/apache/beam/issues/20846) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testLateDataAccumulating' - // TODO(https://github.com/apache/beam/issues/21142) - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupByKeyTest$WindowTests.testWindowFnPostMerging' - // TODO(https://github.com/apache/beam/issues/21143) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testParDoShiftTimestampInvalid' - // TODO(https://github.com/apache/beam/issues/21144) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testParDoShiftTimestampInvalidZeroAllowed' - // TODO(https://github.com/apache/beam/issues/32520) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionIn*Stateful' - // TODO(https://github.com/apache/beam/issues/21145) - excludeTestsMatching 'org.apache.beam.sdk.transforms.DeduplicateTest.testEventTime' - // TODO(https://github.com/apache/beam/issues/21146) - excludeTestsMatching 'org.apache.beam.sdk.io.TFRecordIOTest.testReadInvalidRecord' - // TODO(https://github.com/apache/beam/issues/21147) - excludeTestsMatching 'org.apache.beam.sdk.io.TFRecordIOTest.testReadInvalidDataMask' - // TODO(https://github.com/apache/beam/issues/21148) - excludeTestsMatching 'org.apache.beam.sdk.io.TFRecordIOTest.testReadInvalidLengthMask' - // TODO(https://github.com/apache/beam/issues/21149) - excludeTestsMatching 'org.apache.beam.sdk.io.TextIOReadTest$CompressedReadTest.testCompressedReadWithoutExtension' - // TODO(https://github.com/apache/beam/issues/21150) - excludeTestsMatching 'org.apache.beam.sdk.io.WriteFilesTest.testWithRunnerDeterminedShardingUnbounded' - // TODO(https://github.com/apache/beam/issues/211505) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$MultipleInputsAndOutputTests.testParDoWritingToUndeclaredTag' - // TODO(https://github.com/apache/beam/issues/21152) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$MultipleInputsAndOutputTests.testParDoReadingFromUnknownSideInput' - // TODO(https://github.com/apache/beam/issues/21153) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ViewTest.testMapSideInputWithNullValuesCatchesDuplicates' - - // TODO(https://github.com/apache/beam/issues/21041) - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingNPException' - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingIOException' - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingNPException' - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingIOException' - // TODO(https://github.com/apache/beam/issues/21040) - excludeTestsMatching 'org.apache.beam.sdk.PipelineTest.testEmptyPipeline' - // TODO(https://github.com/apache/beam/issues/21038) - excludeTestsMatching 'org.apache.beam.sdk.io.AvroIOTest*' - // TODO(https://github.com/apache/beam/issues/21039) - excludeTestsMatching 'org.apache.beam.sdk.io.FileIOTest*' - // TODO(https://github.com/apache/beam/issues/21037) - excludeTestsMatching 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsBackwardsInTimeShouldThrow' - excludeTestsMatching 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsWithNullTimestampShouldThrow' - // TODO(https://github.com/apache/beam/issues/21035) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ViewTest.testEmptySingletonSideInput' - excludeTestsMatching 'org.apache.beam.sdk.transforms.ViewTest.testNonSingletonSideInput' - // TODO(https://github.com/apache/beam/issues/21036) - excludeTestsMatching 'org.apache.beam.sdk.transforms.MapElementsTest.testMapSimpleFunction' - // TODO(https://github.com/apache/beam/issues/21033) - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInGlobalWindowBatchSizeByteSizeFn' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInStreamingMode' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithShardedKeyInGlobalWindow' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithUnevenBatches' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInGlobalWindowBatchSizeByteSize' - // TODO(BEAM-10025) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestampDefaultUnbounded' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testRelativeTimerWithOutputTimestamp' - // TODO(BEAM-13498) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testProcessElementSkew' - // TODO(https://github.com/apache/beam/issues/22650) - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupByKeyTest$BasicTests.testAfterProcessingTimeContinuationTriggerUsingState' - // TODO(https://github.com/apache/beam/issues/29973) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ReshuffleTest.testReshufflePreservesMetadata' - // TODO(https://github.com/apache/beam/issues/31231) - excludeTestsMatching 'org.apache.beam.sdk.transforms.RedistributeTest.testRedistributePreservesMetadata' - } - ) -} - -project.ext.validatesPortableRunnerDocker = portableValidatesRunnerTask("Docker", true) -project.ext.validatesPortableRunnerEmbedded = portableValidatesRunnerTask("Embedded", false) - -tasks.register("validatesPortableRunner") { - dependsOn validatesPortableRunnerDocker - dependsOn validatesPortableRunnerEmbedded -} - -def testJavaVersion = project.findProperty('testJavaVersion') -String testJavaHome = null -if (testJavaVersion) { - testJavaHome = project.findProperty("java${testJavaVersion}Home") -} - -def jobPort = BeamModulePlugin.getRandomPort() -def artifactPort = BeamModulePlugin.getRandomPort() - -def setupTask = project.tasks.register("samzaJobServerSetup", Exec) { - dependsOn shadowJar - def pythonDir = project.project(":sdks:python").projectDir - def samzaJobServerJar = shadowJar.archivePath - if (testJavaHome) { - environment "JAVA_HOME", testJavaHome - } - executable 'sh' - args '-c', "$pythonDir/scripts/run_job_server.sh stop --group_id ${project.name} && $pythonDir/scripts/run_job_server.sh start --group_id ${project.name} --job_port ${jobPort} --artifact_port ${artifactPort} --job_server_jar ${samzaJobServerJar}" -} - -def cleanupTask = project.tasks.register("samzaJobServerCleanup", Exec) { - def pythonDir = project.project(":sdks:python").projectDir - if (testJavaHome) { - environment "JAVA_HOME", testJavaHome - } - executable 'sh' - args '-c', "$pythonDir/scripts/run_job_server.sh stop --group_id ${project.name}" -} - -createCrossLanguageValidatesRunnerTask( - startJobServer: setupTask, - cleanupJobServer: cleanupTask, - classpath: configurations.validatesPortableRunner, - numParallelTests: 1, - pythonPipelineOptions: [ - "--runner=PortableRunner", - "--job_endpoint=localhost:${jobPort}", - "--environment_cache_millis=10000", - "--experiments=beam_fn_api", - ], - javaPipelineOptions: [ - "--runner=PortableRunner", - "--jobEndpoint=localhost:${jobPort}", - "--environmentCacheMillis=10000", - "--experiments=beam_fn_api", - "--customBeamRequirement=${project.project(":sdks:python").projectDir}/build/apache-beam.tar.gz", - ], - goScriptOptions: [ - "--runner samza", - "--tests \"./test/integration/xlang ./test/integration/io/xlang/...\"", - "--endpoint localhost:${jobPort}", - ], -) diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java deleted file mode 100644 index 18bb098bf2cc..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.samza.context.ApplicationContainerContext; -import org.apache.samza.context.ApplicationContainerContextFactory; -import org.apache.samza.context.ContainerContext; -import org.apache.samza.context.ExternalContext; -import org.apache.samza.context.JobContext; -import org.apache.samza.metrics.MetricsRegistryMap; - -/** Runtime context for the Samza runner. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaExecutionContext implements ApplicationContainerContext { - - private final SamzaPipelineOptions options; - private SamzaMetricsContainer metricsContainer; - - public SamzaExecutionContext(SamzaPipelineOptions options) { - this.options = options; - } - - public SamzaPipelineOptions getPipelineOptions() { - return options; - } - - public SamzaMetricsContainer getMetricsContainer() { - return this.metricsContainer; - } - - void setMetricsContainer(SamzaMetricsContainer metricsContainer) { - this.metricsContainer = metricsContainer; - } - - @Override - public void start() {} - - @Override - public void stop() {} - - /** The factory to return this {@link SamzaExecutionContext}. */ - public class Factory implements ApplicationContainerContextFactory { - - @Override - public SamzaExecutionContext create( - ExternalContext externalContext, JobContext jobContext, ContainerContext containerContext) { - - final MetricsRegistryMap metricsRegistry = - (MetricsRegistryMap) containerContext.getContainerMetricsRegistry(); - SamzaExecutionContext.this.setMetricsContainer(new SamzaMetricsContainer(metricsRegistry)); - return SamzaExecutionContext.this; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java deleted file mode 100644 index 02c31edfd1dd..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -/** Different Samza execution environments that defines how the Samza job will be deployed. */ -public enum SamzaExecutionEnvironment { - /** - * Runs the Samza job on the local machine with only one container. There is no coordination - * required since there is only one container deployed in a single JVM. This setting is generally - * used for development and testing. - */ - LOCAL, - - /** - * Submits and runs the Samza job on YARN, a remote clustered resource manager. Samza works with - * the YARN to provision and coordinate resources for your application and run it across a cluster - * of machines. It also handles failures of individual instances and automatically restarts them. - */ - YARN, - - /** - * Runs Samza job as a stand alone embedded library mode which can be imported into your Java - * application. You can increase your application's capacity by spinning up multiple instances. - * These instances will then dynamically coordinate with each other and distribute work among - * themselves. If an instance fails, the tasks running on it will be re-assigned to the remaining - * ones. By default, Samza uses Zookeeper for coordination across individual instances. - */ - STANDALONE -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java deleted file mode 100644 index 6638b35f377d..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import java.util.UUID; -import javax.annotation.Nullable; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.jobsubmission.JobInvocation; -import org.apache.beam.runners.jobsubmission.JobInvoker; -import org.apache.beam.runners.jobsubmission.PortablePipelineJarCreator; -import org.apache.beam.runners.jobsubmission.PortablePipelineRunner; -import org.apache.beam.sdk.util.construction.PipelineOptionsTranslation; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.Struct; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ListeningExecutorService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaJobInvoker extends JobInvoker { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaJobInvoker.class); - - public static SamzaJobInvoker create( - SamzaJobServerDriver.SamzaServerConfiguration configuration) { - return new SamzaJobInvoker(); - } - - private SamzaJobInvoker() { - this("samza-runner-job-invoker-%d"); - } - - protected SamzaJobInvoker(String name) { - super(name); - } - - @Override - protected JobInvocation invokeWithExecutor( - RunnerApi.Pipeline pipeline, - Struct options, - @Nullable String retrievalToken, - ListeningExecutorService executorService) { - LOG.trace("Parsing pipeline options"); - final SamzaPortablePipelineOptions samzaOptions = - PipelineOptionsTranslation.fromProto(options).as(SamzaPortablePipelineOptions.class); - - final PortablePipelineRunner pipelineRunner; - if (Strings.isNullOrEmpty(samzaOptions.getOutputExecutablePath())) { - pipelineRunner = new SamzaPipelineRunner(samzaOptions); - } else { - /* - * To support --output_executable_path where bundles the input pipeline along with all - * artifacts, etc. required to run the pipeline into a jar that can be executed later. - */ - pipelineRunner = new PortablePipelineJarCreator(SamzaPipelineRunner.class); - } - - final String invocationId = - String.format("%s_%s", samzaOptions.getJobName(), UUID.randomUUID().toString()); - final JobInfo jobInfo = - JobInfo.create(invocationId, samzaOptions.getJobName(), retrievalToken, options); - return new JobInvocation(jobInfo, executorService, pipeline, pipelineRunner); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java deleted file mode 100644 index f8139c0d26f2..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.runners.jobsubmission.JobServerDriver; -import org.apache.beam.sdk.fn.server.ServerFactory; -import org.apache.beam.sdk.io.FileSystems; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Driver program that starts a job server for the Samza runner. */ -public class SamzaJobServerDriver extends JobServerDriver { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaJobServerDriver.class); - - /** Samza runner-specific Configuration for the jobServer. */ - public static class SamzaServerConfiguration extends ServerConfiguration {} - - public static void main(String[] args) { - // TODO: Expose the fileSystem related options. - PipelineOptions options = PipelineOptionsFactory.create(); - // Register standard file systems. - FileSystems.setDefaultPipelineOptions(options); - fromParams(args).run(); - } - - private static SamzaJobServerDriver fromParams(String[] args) { - return fromConfig(parseArgs(args)); - } - - private static void printUsage(CmdLineParser parser) { - System.err.printf("Usage: java %s arguments...%n", SamzaJobServerDriver.class.getSimpleName()); - parser.printUsage(System.err); - System.err.println(); - } - - private static SamzaJobServerDriver fromConfig(SamzaServerConfiguration configuration) { - return create( - configuration, - createJobServerFactory(configuration), - createArtifactServerFactory(configuration)); - } - - public static SamzaServerConfiguration parseArgs(String[] args) { - SamzaServerConfiguration configuration = new SamzaServerConfiguration(); - CmdLineParser parser = new CmdLineParser(configuration); - try { - parser.parseArgument(args); - } catch (CmdLineException e) { - LOG.error("Unable to parse command line arguments.", e); - printUsage(parser); - throw new IllegalArgumentException("Unable to parse command line arguments.", e); - } - return configuration; - } - - private static SamzaJobServerDriver create( - SamzaServerConfiguration configuration, - ServerFactory jobServerFactory, - ServerFactory artifactServerFactory) { - return new SamzaJobServerDriver(configuration, jobServerFactory, artifactServerFactory); - } - - private SamzaJobServerDriver( - SamzaServerConfiguration configuration, - ServerFactory jobServerFactory, - ServerFactory artifactServerFactory) { - this( - configuration, - jobServerFactory, - artifactServerFactory, - () -> SamzaJobInvoker.create(configuration)); - } - - protected SamzaJobServerDriver( - ServerConfiguration configuration, - ServerFactory jobServerFactory, - ServerFactory artifactServerFactory, - JobInvokerFactory jobInvokerFactory) { - super(configuration, jobServerFactory, artifactServerFactory, jobInvokerFactory); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java deleted file mode 100644 index 5bd02b3fc737..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -/** Helper that is used to metadata associated with an exception thrown by Samza Runner. */ -public class SamzaPipelineExceptionContext { - private final String transformFullName; - private final Exception exception; - - public SamzaPipelineExceptionContext(String transformFullName, Exception exception) { - this.transformFullName = transformFullName; - this.exception = exception; - } - - public String getTransformFullName() { - return transformFullName; - } - - public Exception getException() { - return exception; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java deleted file mode 100644 index 47f36a229ac1..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.samza.config.Config; -import org.apache.samza.context.ExternalContext; - -/** Life cycle listener for a Samza pipeline during runtime. */ -public interface SamzaPipelineLifeCycleListener { - /** Callback when the pipeline options is created. */ - void onInit(Config config, SamzaPipelineOptions options); - - /** Callback when the pipeline is started. */ - ExternalContext onStart(); - - /** - * Callback after the pipeline is submmitted. This will be invoked only for Samza jobs submitted - * to a cluster. - */ - void onSubmit(); - - /** Callback after the pipeline is finished. */ - void onFinish(); - - /** A registrar for {@link SamzaPipelineLifeCycleListener}. */ - interface Registrar { - SamzaPipelineLifeCycleListener getLifeCycleListener(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java deleted file mode 100644 index a34303d92552..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.apache.beam.sdk.options.Default; -import org.apache.beam.sdk.options.DefaultValueFactory; -import org.apache.beam.sdk.options.Description; -import org.apache.beam.sdk.options.Hidden; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; -import org.apache.samza.config.ConfigLoaderFactory; -import org.apache.samza.config.loaders.PropertiesConfigLoaderFactory; -import org.apache.samza.metrics.MetricsReporter; - -/** Options which can be used to configure a Samza PortablePipelineRunner. */ -public interface SamzaPipelineOptions extends PipelineOptions { - - @Description( - "The config file for Samza. It is *optional*. By default Samza supports properties config." - + "Without a config file, Samza uses a default config for local execution.") - String getConfigFilePath(); - - void setConfigFilePath(String filePath); - - @Description("The factory to read config file from config file path.") - @Default.Class(PropertiesConfigLoaderFactory.class) - Class getConfigLoaderFactory(); - - void setConfigLoaderFactory(Class configLoaderFactory); - - @Description( - "The config override to set programmatically. It will be applied on " - + "top of config file if it exits, otherwise used directly as the config.") - Map getConfigOverride(); - - void setConfigOverride(Map configs); - - @Description("The instance name of the job") - @Default.String("1") - String getJobInstance(); - - void setJobInstance(String instance); - - @Description( - "Samza application execution environment." - + "See {@link org.apache.beam.runners.samza.SamzaExecutionEnvironment} for detailed environment descriptions.") - @Default.Enum("LOCAL") - SamzaExecutionEnvironment getSamzaExecutionEnvironment(); - - void setSamzaExecutionEnvironment(SamzaExecutionEnvironment environment); - - @Description("The interval to check for watermarks in milliseconds.") - @Default.Long(1000) - long getWatermarkInterval(); - - void setWatermarkInterval(long interval); - - @Description("The maximum number of messages to buffer for a given system.") - @Default.Integer(5000) - int getSystemBufferSize(); - - void setSystemBufferSize(int consumerBufferSize); - - @Description("The maximum number of event-time timers to buffer in memory for a PTransform") - @Default.Integer(50000) - int getEventTimerBufferSize(); - - void setEventTimerBufferSize(int eventTimerBufferSize); - - @Description("The maximum number of ready timers to process at once per watermark.") - @Default.Integer(Integer.MAX_VALUE) - int getMaxReadyTimersToProcessOnce(); - - void setMaxReadyTimersToProcessOnce(int maxReadyTimersToProcessOnce); - - @Description("The maximum parallelism allowed for any data source.") - @Default.Integer(1) - int getMaxSourceParallelism(); - - void setMaxSourceParallelism(int maxSourceParallelism); - - @Description("The batch get size limit for the state store.") - @Default.Integer(10000) - int getStoreBatchGetSize(); - - void setStoreBatchGetSize(int storeBatchGetSize); - - @Description("Enable/disable Beam metrics in Samza Runner") - @Default.Boolean(true) - Boolean getEnableMetrics(); - - void setEnableMetrics(Boolean enableMetrics); - - @Description("Enable/disable Beam Transform throughput, latency metrics in Samza Runner") - @Default.Boolean(false) - Boolean getEnableTransformMetrics(); - - void setEnableTransformMetrics(Boolean enableMetrics); - - @Description("The config for state to be durable") - @Default.Boolean(false) - Boolean getStateDurable(); - - void setStateDurable(Boolean stateDurable); - - @JsonIgnore - @Description("The metrics reporters that will be used to emit metrics.") - List getMetricsReporters(); - - void setMetricsReporters(List reporters); - - @Description("The maximum number of elements in a bundle.") - @Default.Long(1) - long getMaxBundleSize(); - - void setMaxBundleSize(long maxBundleSize); - - @Description("The maximum time to wait before finalising a bundle (in milliseconds).") - @Default.Long(1000) - long getMaxBundleTimeMs(); - - void setMaxBundleTimeMs(long maxBundleTimeMs); - - @Description( - "Wait if necessary for completing a remote bundle processing for at most the given time (in milliseconds). if the value of timeout is negative, wait forever until the bundle processing is completed. Used only in portable mode for now.") - @Default.Long(-1) - long getBundleProcessingTimeout(); - - void setBundleProcessingTimeout(long timeoutMs); - - @Description( - "The number of threads to run DoFn.processElements in parallel within a bundle. Used only in non-portable mode.") - @Default.Integer(1) - int getNumThreadsForProcessElement(); - - void setNumThreadsForProcessElement(int numThreads); - - @JsonIgnore - @Description( - "The ExecutorService instance to run DoFN.processElements in parallel within a bundle. Used only in non-portable mode.") - @Default.InstanceFactory(ProcessElementExecutorServiceFactory.class) - @Hidden - ExecutorService getExecutorServiceForProcessElement(); - - void setExecutorServiceForProcessElement(ExecutorService executorService); - - class ProcessElementExecutorServiceFactory implements DefaultValueFactory { - - @Override - public ExecutorService create(PipelineOptions options) { - return Executors.newFixedThreadPool( - options.as(SamzaPipelineOptions.class).getNumThreadsForProcessElement(), - new ThreadFactoryBuilder().setNameFormat("Process Element Thread-%d").build()); - } - } - - @Description("Enable/disable late data dropping in GroupByKey/Combine transforms") - @Default.Boolean(false) - boolean getDropLateData(); - - void setDropLateData(boolean dropLateData); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java deleted file mode 100644 index 1db0974b5d30..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import static org.apache.samza.config.JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE; - -import java.util.HashMap; -import java.util.Map; -import org.apache.samza.config.JobConfig; -import org.apache.samza.config.MapConfig; - -/** Validates that the {@link SamzaPipelineOptions} conforms to all the criteria. */ -public class SamzaPipelineOptionsValidator { - public static void validate(SamzaPipelineOptions opts) { - checkArgument(opts.getMaxSourceParallelism() >= 1); - validateBundlingRelatedOptions(opts); - } - - /* - * Perform some bundling related validation for pipeline option. - * Visible for testing. - */ - static void validateBundlingRelatedOptions(SamzaPipelineOptions pipelineOptions) { - if (pipelineOptions.getMaxBundleSize() > 1) { - final Map configs = - pipelineOptions.getConfigOverride() == null - ? new HashMap<>() - : pipelineOptions.getConfigOverride(); - final JobConfig jobConfig = new JobConfig(new MapConfig(configs)); - - // Validate that the threadPoolSize is not override in the code - checkArgument( - jobConfig.getThreadPoolSize() <= 1, - JOB_CONTAINER_THREAD_POOL_SIZE - + " config should be replaced with SamzaPipelineOptions.numThreadsForProcessElement"); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java deleted file mode 100644 index e84cf086edc9..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import static org.apache.beam.runners.core.metrics.MetricsContainerStepMap.asAttemptedOnlyMetricResults; -import static org.apache.samza.config.TaskConfig.TASK_SHUTDOWN_MS; - -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.PipelineResult; -import org.apache.beam.sdk.metrics.MetricResults; -import org.apache.beam.sdk.util.UserCodeException; -import org.apache.samza.config.Config; -import org.apache.samza.job.ApplicationStatus; -import org.apache.samza.runtime.ApplicationRunner; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Duration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** The result from executing a Samza Pipeline. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaPipelineResult implements PipelineResult { - private static final Logger LOG = LoggerFactory.getLogger(SamzaPipelineResult.class); - // allow some buffer on top of samza's own shutdown timeout - private static final long SHUTDOWN_TIMEOUT_BUFFER = 5000L; - private static final long DEFAULT_TASK_SHUTDOWN_MS = 30000L; - - private final SamzaExecutionContext executionContext; - private final ApplicationRunner runner; - private final SamzaPipelineLifeCycleListener listener; - private final long shutdownTiemoutMs; - - public SamzaPipelineResult( - ApplicationRunner runner, - SamzaExecutionContext executionContext, - SamzaPipelineLifeCycleListener listener, - Config config) { - this.executionContext = executionContext; - this.runner = runner; - this.listener = listener; - this.shutdownTiemoutMs = - config.getLong(TASK_SHUTDOWN_MS, DEFAULT_TASK_SHUTDOWN_MS) + SHUTDOWN_TIMEOUT_BUFFER; - } - - @Override - public State getState() { - return getStateInfo().state; - } - - @Override - public State cancel() { - LOG.info("Start to cancel samza pipeline..."); - runner.kill(); - LOG.info("Start awaiting finish for {} ms.", shutdownTiemoutMs); - return waitUntilFinish(Duration.millis(shutdownTiemoutMs)); - } - - @Override - public State waitUntilFinish(@Nullable Duration duration) { - try { - if (duration == null) { - runner.waitForFinish(); - } else { - runner.waitForFinish(java.time.Duration.ofMillis(duration.getMillis())); - } - } catch (Exception e) { - throw new Pipeline.PipelineExecutionException(e); - } - - final StateInfo stateInfo = getStateInfo(); - - if (listener != null && (stateInfo.state == State.DONE || stateInfo.state == State.FAILED)) { - listener.onFinish(); - } - - if (stateInfo.state == State.FAILED) { - throw stateInfo.error; - } - - LOG.info("Pipeline finished. Final state: {}", stateInfo.state); - return stateInfo.state; - } - - @Override - public State waitUntilFinish() { - return waitUntilFinish(null); - } - - @Override - public MetricResults metrics() { - return asAttemptedOnlyMetricResults(executionContext.getMetricsContainer().getContainers()); - } - - @SuppressWarnings("Slf4jDoNotLogMessageOfExceptionExplicitly") - private StateInfo getStateInfo() { - final ApplicationStatus status = runner.status(); - switch (status.getStatusCode()) { - case New: - return new StateInfo(State.STOPPED); - case Running: - return new StateInfo(State.RUNNING); - case SuccessfulFinish: - return new StateInfo(State.DONE); - case UnsuccessfulFinish: - LOG.error("Pipeline execution failed", status.getThrowable()); - return new StateInfo( - State.FAILED, - new Pipeline.PipelineExecutionException(getUserCodeException(status.getThrowable()))); - default: - return new StateInfo(State.UNKNOWN); - } - } - - private static class StateInfo { - private final State state; - private final Pipeline.PipelineExecutionException error; - - private StateInfo(State state) { - this(state, null); - } - - private StateInfo(State state, Pipeline.PipelineExecutionException error) { - this.state = state; - this.error = error; - } - } - - /** - * Some of the Beam unit tests relying on the exception message to do assertion. This function - * will find the original UserCodeException so the message will be exposed directly. - */ - private static Throwable getUserCodeException(Throwable throwable) { - Throwable t = throwable; - while (t != null) { - if (t instanceof UserCodeException) { - return t; - } - - t = t.getCause(); - } - - return throwable; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java deleted file mode 100644 index 897b78cf9e47..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.jobsubmission.PortablePipelineResult; -import org.apache.beam.runners.jobsubmission.PortablePipelineRunner; -import org.apache.beam.runners.samza.translation.SamzaPortablePipelineTranslator; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.GreedyPipelineFuser; -import org.apache.beam.sdk.util.construction.graph.ProtoOverrides; -import org.apache.beam.sdk.util.construction.graph.SplittableParDoExpander; -import org.apache.beam.sdk.util.construction.graph.TrivialNativeTransformExpander; -import org.apache.beam.sdk.util.construction.renderer.PipelineDotRenderer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Runs a Samza job via {@link SamzaRunner}. */ -public class SamzaPipelineRunner implements PortablePipelineRunner { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaPipelineRunner.class); - - private final SamzaPipelineOptions options; - - @Override - public PortablePipelineResult run(final RunnerApi.Pipeline pipeline, JobInfo jobInfo) { - // Expand any splittable DoFns within the graph to enable sizing and splitting of bundles. - RunnerApi.Pipeline pipelineWithSdfExpanded = - ProtoOverrides.updateTransform( - PTransformTranslation.PAR_DO_TRANSFORM_URN, - pipeline, - SplittableParDoExpander.createSizedReplacement()); - - // Don't let the fuser fuse any subcomponents of native transforms. - RunnerApi.Pipeline trimmedPipeline = - TrivialNativeTransformExpander.forKnownUrns( - pipelineWithSdfExpanded, SamzaPortablePipelineTranslator.knownUrns()); - - // Fused pipeline proto. - // TODO: Consider supporting partially-fused graphs. - RunnerApi.Pipeline fusedPipeline = - trimmedPipeline.getComponents().getTransformsMap().values().stream() - .anyMatch(proto -> ExecutableStage.URN.equals(proto.getSpec().getUrn())) - ? trimmedPipeline - : GreedyPipelineFuser.fuse(trimmedPipeline).toPipeline(); - - LOG.info("Portable pipeline to run:"); - LOG.info("{}", PipelineDotRenderer.toDotString(fusedPipeline)); - // the pipeline option coming from sdk will set the sdk specific runner which will break - // serialization - // so we need to reset the runner here to a valid Java runner - options.setRunner(SamzaRunner.class); - try { - final SamzaRunner runner = SamzaRunner.fromOptions(options); - final PortablePipelineResult result = runner.runPortablePipeline(fusedPipeline, jobInfo); - - final SamzaExecutionEnvironment exeEnv = options.getSamzaExecutionEnvironment(); - if (exeEnv == SamzaExecutionEnvironment.LOCAL - || exeEnv == SamzaExecutionEnvironment.STANDALONE) { - // Make run() sync for local mode - result.waitUntilFinish(); - } - return result; - } catch (Exception e) { - throw new RuntimeException("Failed to invoke samza job", e); - } - } - - public SamzaPipelineRunner(SamzaPipelineOptions options) { - this.options = options; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java deleted file mode 100644 index aa8e7ceb71d7..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.sdk.options.Description; -import org.apache.beam.sdk.options.PortablePipelineOptions; - -/** Samza pipeline option that contains portability specific logic. For internal usage only. */ -public interface SamzaPortablePipelineOptions - extends SamzaPipelineOptions, PortablePipelineOptions { - @Description( - "The file path for the local file system token. If not set (by default), then the runner would" - + " not use secure server factory.") - String getFsTokenPath(); - - void setFsTokenPath(String path); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java deleted file mode 100644 index a3452097f511..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.model.jobmanagement.v1.JobApi; -import org.apache.beam.runners.jobsubmission.PortablePipelineResult; -import org.apache.samza.application.StreamApplication; -import org.apache.samza.config.Config; -import org.apache.samza.runtime.ApplicationRunner; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** The result from executing a Samza Portable Pipeline. */ -public class SamzaPortablePipelineResult extends SamzaPipelineResult - implements PortablePipelineResult { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaPortablePipelineResult.class); - - SamzaPortablePipelineResult( - StreamApplication app, - ApplicationRunner runner, - SamzaExecutionContext executionContext, - SamzaPipelineLifeCycleListener listener, - Config config) { - super(runner, executionContext, listener, config); - } - - @Override - public JobApi.MetricResults portableMetrics() throws UnsupportedOperationException { - LOG.warn("Collecting monitoring infos is not implemented yet in Samza portable runner."); - return JobApi.MetricResults.newBuilder().build(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java deleted file mode 100644 index eb16faa41ac0..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.jobsubmission.PortablePipelineResult; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.runners.samza.translation.ConfigContext; -import org.apache.beam.runners.samza.translation.PViewToIdMapper; -import org.apache.beam.runners.samza.translation.PortableTranslationContext; -import org.apache.beam.runners.samza.translation.SamzaPipelineTranslator; -import org.apache.beam.runners.samza.translation.SamzaPortablePipelineTranslator; -import org.apache.beam.runners.samza.translation.SamzaTransformOverrides; -import org.apache.beam.runners.samza.translation.StateIdParser; -import org.apache.beam.runners.samza.translation.TranslationContext; -import org.apache.beam.runners.samza.util.PipelineJsonRenderer; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.PipelineRunner; -import org.apache.beam.sdk.metrics.MetricsEnvironment; -import org.apache.beam.sdk.options.ExperimentalOptions; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsValidator; -import org.apache.beam.sdk.util.construction.SplittableParDo; -import org.apache.beam.sdk.util.construction.renderer.PipelineDotRenderer; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.samza.application.StreamApplication; -import org.apache.samza.config.Config; -import org.apache.samza.context.ExternalContext; -import org.apache.samza.metrics.MetricsReporter; -import org.apache.samza.metrics.MetricsReporterFactory; -import org.apache.samza.runtime.ApplicationRunner; -import org.apache.samza.runtime.ApplicationRunners; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A {@link PipelineRunner} that executes the operations in the {@link Pipeline} into an equivalent - * Samza plan. - * - * @deprecated The support for Samza is scheduled for removal in Beam 3.0. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -@Deprecated -public class SamzaRunner extends PipelineRunner { - private static final Logger LOG = LoggerFactory.getLogger(SamzaRunner.class); - private static final String BEAM_DOT_GRAPH = "beamDotGraph"; - public static final String BEAM_JSON_GRAPH = "beamJsonGraph"; - - public static SamzaRunner fromOptions(PipelineOptions opts) { - final SamzaPipelineOptions samzaOptions = - PipelineOptionsValidator.validate(SamzaPipelineOptions.class, opts); - return new SamzaRunner(samzaOptions); - } - - private final SamzaPipelineOptions options; - private final SamzaPipelineLifeCycleListener listener; - - private SamzaRunner(SamzaPipelineOptions options) { - this.options = options; - final Iterator listenerReg = - ServiceLoader.load(SamzaPipelineLifeCycleListener.Registrar.class).iterator(); - this.listener = - listenerReg.hasNext() ? Iterators.getOnlyElement(listenerReg).getLifeCycleListener() : null; - } - - public PortablePipelineResult runPortablePipeline(RunnerApi.Pipeline pipeline, JobInfo jobInfo) { - final String dotGraph = PipelineDotRenderer.toDotString(pipeline); - LOG.info("Portable pipeline to run DOT graph:\n{}", dotGraph); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPortablePipelineTranslator.createConfig(pipeline, configBuilder, options); - configBuilder.put(BEAM_DOT_GRAPH, dotGraph); - - final Config config = configBuilder.build(); - options.setConfigOverride(config); - - if (listener != null) { - listener.onInit(config, options); - } - - final SamzaExecutionContext executionContext = new SamzaExecutionContext(options); - final Map reporterFactories = getMetricsReporters(); - final StreamApplication app = - appDescriptor -> { - appDescriptor - .withApplicationContainerContextFactory(executionContext.new Factory()) - .withMetricsReporterFactories(reporterFactories); - SamzaPortablePipelineTranslator.translate( - pipeline, new PortableTranslationContext(appDescriptor, options, jobInfo)); - }; - - ApplicationRunner runner = runSamzaApp(app, config); - return new SamzaPortablePipelineResult(app, runner, executionContext, listener, config); - } - - @Override - public SamzaPipelineResult run(Pipeline pipeline) { - // TODO(https://github.com/apache/beam/issues/20530): Use SDF read as default for non-portable - // execution when we address performance issue. - if (!ExperimentalOptions.hasExperiment(pipeline.getOptions(), "beam_fn_api")) { - SplittableParDo.convertReadBasedSplittableDoFnsToPrimitiveReadsIfNecessary(pipeline); - } - - MetricsEnvironment.setMetricsSupported(true); - - if (LOG.isDebugEnabled()) { - LOG.debug( - "Pre-processed Beam pipeline in dot format:\n{}", - PipelineDotRenderer.toDotString(pipeline)); - } - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - - final String dotGraph = PipelineDotRenderer.toDotString(pipeline); - LOG.info("Beam pipeline DOT graph:\n{}", dotGraph); - - final String jsonGraph = PipelineJsonRenderer.toJsonString(pipeline, configCtx); - LOG.info("Beam pipeline JSON graph:\n{}", jsonGraph); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - configBuilder.put(BEAM_DOT_GRAPH, dotGraph); - configBuilder.put(BEAM_JSON_GRAPH, jsonGraph); - - final Config config = configBuilder.build(); - options.setConfigOverride(config); - - if (listener != null) { - listener.onInit(config, options); - } - - final SamzaExecutionContext executionContext = new SamzaExecutionContext(options); - final Map reporterFactories = getMetricsReporters(); - - final StreamApplication app = - appDescriptor -> { - appDescriptor.withApplicationContainerContextFactory(executionContext.new Factory()); - appDescriptor.withMetricsReporterFactories(reporterFactories); - - SamzaPipelineTranslator.translate( - pipeline, new TranslationContext(appDescriptor, idMap, nonUniqueStateIds, options)); - }; - - // perform a final round of validation for the pipeline options now that all configs are - // generated - SamzaPipelineOptionsValidator.validate(options); - ApplicationRunner runner = runSamzaApp(app, config); - return new SamzaPipelineResult(runner, executionContext, listener, config); - } - - private Map getMetricsReporters() { - if (options.getMetricsReporters() != null) { - final Map reporters = new HashMap<>(); - for (int i = 0; i < options.getMetricsReporters().size(); i++) { - final String name = "beam-metrics-reporter-" + i; - final MetricsReporter reporter = options.getMetricsReporters().get(i); - - reporters.put(name, (MetricsReporterFactory) (nm, processorId, config) -> reporter); - LOG.info("{}: {}", name, reporter.getClass().getName()); - } - return reporters; - } else { - return Collections.emptyMap(); - } - } - - private ApplicationRunner runSamzaApp(StreamApplication app, Config config) { - - final ApplicationRunner runner = ApplicationRunners.getApplicationRunner(app, config); - - ExternalContext externalContext = null; - if (listener != null) { - externalContext = listener.onStart(); - } - - runner.run(externalContext); - - if (listener != null - && options.getSamzaExecutionEnvironment() == SamzaExecutionEnvironment.YARN) { - listener.onSubmit(); - } - - return runner; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java deleted file mode 100644 index 4c5fa432ca82..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import java.time.Duration; - -// TODO: can we get rid of this class? Right now the SamzaPipelineOptionsValidator would force -// the pipeline option to be the type SamzaPipelineOption. Ideally, we should be able to keep -// passing SamzaPortablePipelineOption. Alternative, we could merge portable and non-portable -// pipeline option. -/** A helper class for holding all the beam runner specific samza configs. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaRunnerOverrideConfigs { - public static final String BEAM_RUNNER_CONFIG_PREFIX = "beam.override."; - // whether the job is in portable mode - public static final String IS_PORTABLE_MODE = BEAM_RUNNER_CONFIG_PREFIX + "portable"; - // for portable mode only: port number for fn control api - public static final String FN_CONTROL_PORT = BEAM_RUNNER_CONFIG_PREFIX + "control.port"; - // timeout for waiting for control client to connect - public static final String CONTROL_CLIENT_MAX_WAIT_TIME_MS = "controL.wait.time.ms"; - public static final long DEFAULT_CONTROL_CLIENT_MAX_WAIT_TIME_MS = - Duration.ofMinutes(2).toMillis(); - public static final String FS_TOKEN_PATH = BEAM_RUNNER_CONFIG_PREFIX + "fs.token.path"; - public static final String DEFAULT_FS_TOKEN_PATH = null; - - private static boolean containsKey(SamzaPipelineOptions options, String configKey) { - if (options == null || options.getConfigOverride() == null) { - return false; - } - return options.getConfigOverride().containsKey(configKey); - } - - /** Whether the job is in portable mode based on the config override in the pipeline options. */ - public static boolean isPortableMode(SamzaPipelineOptions options) { - if (containsKey(options, IS_PORTABLE_MODE)) { - return options.getConfigOverride().get(IS_PORTABLE_MODE).equals(String.valueOf(true)); - } else { - return false; - } - } - - /** Get fn control port number based on the config override in the pipeline options. */ - public static int getFnControlPort(SamzaPipelineOptions options) { - if (containsKey(options, FN_CONTROL_PORT)) { - return Integer.parseInt(options.getConfigOverride().get(FN_CONTROL_PORT)); - } else { - return -1; - } - } - - /** Get max wait time for control client connection. */ - public static long getControlClientWaitTimeoutMs(SamzaPipelineOptions options) { - if (containsKey(options, CONTROL_CLIENT_MAX_WAIT_TIME_MS)) { - return Long.parseLong(options.getConfigOverride().get(CONTROL_CLIENT_MAX_WAIT_TIME_MS)); - } else { - return DEFAULT_CONTROL_CLIENT_MAX_WAIT_TIME_MS; - } - } - - /** Get fs token path for portable mode. */ - public static String getFsTokenPath(SamzaPipelineOptions options) { - if (containsKey(options, FS_TOKEN_PATH)) { - return options.getConfigOverride().get(FS_TOKEN_PATH); - } else { - return DEFAULT_FS_TOKEN_PATH; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java deleted file mode 100644 index 102838975d13..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import com.google.auto.service.AutoService; -import org.apache.beam.sdk.PipelineRunner; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsRegistrar; -import org.apache.beam.sdk.runners.PipelineRunnerRegistrar; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; - -/** - * AutoService registrar - will register SamzaRunner and SamzaOptions as possible pipeline runner - * services. - * - *

It ends up in META-INF/services and gets picked up by Beam. - */ -public class SamzaRunnerRegistrar { - private SamzaRunnerRegistrar() {} - - /** Pipeline runner registrar. */ - @AutoService(PipelineRunnerRegistrar.class) - public static class Runner implements PipelineRunnerRegistrar { - @Override - public Iterable>> getPipelineRunners() { - return ImmutableList.of(SamzaRunner.class, TestSamzaRunner.class); - } - } - - /** Pipeline options registrar. */ - @AutoService(PipelineOptionsRegistrar.class) - public static class Options implements PipelineOptionsRegistrar { - @Override - public Iterable> getPipelineOptions() { - return ImmutableList.of(SamzaPipelineOptions.class); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/TestSamzaRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/TestSamzaRunner.java deleted file mode 100644 index 810fc0c983f7..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/TestSamzaRunner.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import static org.apache.samza.config.JobConfig.JOB_JMX_ENABLED; -import static org.apache.samza.config.JobConfig.JOB_LOGGED_STORE_BASE_DIR; -import static org.apache.samza.config.JobConfig.JOB_NON_LOGGED_STORE_BASE_DIR; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.File; -import java.nio.file.Files; -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.PipelineResult; -import org.apache.beam.sdk.PipelineRunner; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsValidator; -import org.apache.commons.io.FileUtils; - -/** Test {@link SamzaRunner}. */ -public class TestSamzaRunner extends PipelineRunner { - - private final SamzaRunner delegate; - private final File storeDir; - - public static TestSamzaRunner fromOptions(PipelineOptions options) { - return new TestSamzaRunner(options); - } - - public static SamzaPipelineOptions createSamzaPipelineOptions( - PipelineOptions options, File storeDir) { - try { - final SamzaPipelineOptions samzaOptions = - PipelineOptionsValidator.validate(SamzaPipelineOptions.class, options); - final Map config = new HashMap<>(ConfigBuilder.localRunConfig()); - config.put(JOB_LOGGED_STORE_BASE_DIR, storeDir.getAbsolutePath()); - config.put(JOB_NON_LOGGED_STORE_BASE_DIR, storeDir.getAbsolutePath()); - config.put(JOB_JMX_ENABLED, "false"); - - if (samzaOptions.getConfigOverride() != null) { - config.putAll(samzaOptions.getConfigOverride()); - } - samzaOptions.setConfigOverride(config); - return samzaOptions; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static File createStoreDir() { - try { - return Files.createTempDirectory("beam-samza-test").toFile(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public TestSamzaRunner(PipelineOptions options) { - this.storeDir = createStoreDir(); - this.delegate = SamzaRunner.fromOptions(createSamzaPipelineOptions(options, storeDir)); - } - - @Override - @SuppressFBWarnings(value = "DE_MIGHT_IGNORE") - public PipelineResult run(Pipeline pipeline) { - try { - final PipelineResult result = delegate.run(pipeline); - result.waitUntilFinish(); - - return result; - } catch (Throwable t) { - // Search for AssertionError. If present use it as the cause of the pipeline failure. - Throwable current = t; - - while (current != null) { - if (current instanceof AssertionError) { - throw (AssertionError) current; - } - current = current.getCause(); - } - - throw t; - } finally { - try { - // delete the store folder - FileUtils.deleteDirectory(storeDir); - } catch (Exception ignore) { - // Ignore - } - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystem.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystem.java deleted file mode 100644 index 92c9eea4293a..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystem.java +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.apache.beam.runners.core.construction.SerializablePipelineOptions; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.metrics.FnWithMetricsWrapper; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.io.BoundedSource; -import org.apache.beam.sdk.io.BoundedSource.BoundedReader; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.samza.Partition; -import org.apache.samza.SamzaException; -import org.apache.samza.config.Config; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemAdmin; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemFactory; -import org.apache.samza.system.SystemProducer; -import org.apache.samza.system.SystemStreamMetadata; -import org.apache.samza.system.SystemStreamMetadata.SystemStreamPartitionMetadata; -import org.apache.samza.system.SystemStreamPartition; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A Samza system that supports reading from a Beam {@link BoundedSource}. The source is treated as - * though it has a single partition and does not support checkpointing via a changelog stream. If - * the job is restarted the bounded source will be consumed from the beginning. - */ -// TODO: instrumentation for the consumer -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class BoundedSourceSystem { - private static final Logger LOG = LoggerFactory.getLogger(BoundedSourceSystem.class); - - private static List> split( - BoundedSource source, SamzaPipelineOptions pipelineOptions) throws Exception { - final int numSplits = pipelineOptions.getMaxSourceParallelism(); - if (numSplits > 1) { - final long estimatedSize = source.getEstimatedSizeBytes(pipelineOptions); - // calculate the size of each split, rounded up to the ceiling. - final long bundleSize = (estimatedSize + numSplits - 1) / numSplits; - @SuppressWarnings("unchecked") - final List> splits = - (List>) source.split(bundleSize, pipelineOptions); - // Need the empty check here because Samza doesn't handle empty partition well - if (!splits.isEmpty()) { - return splits; - } - } - return Collections.singletonList(source); - } - - /** A {@link SystemAdmin} for {@link BoundedSourceSystem}. */ - public static class Admin implements SystemAdmin { - private final BoundedSource source; - private final SamzaPipelineOptions pipelineOptions; - - public Admin(BoundedSource source, SamzaPipelineOptions pipelineOptions) { - this.source = source; - this.pipelineOptions = pipelineOptions; - } - - @Override - public Map getOffsetsAfter( - Map offsets) { - // BEAM checkpoints the next offset so here we just need to return the map itself - return offsets; - } - - @Override - public Map getSystemStreamMetadata(Set streamNames) { - return streamNames.stream() - .collect( - Collectors.toMap( - Function.identity(), - streamName -> { - try { - List> splits = split(source, pipelineOptions); - final Map partitionMetaData = - new HashMap<>(); - // we assume that the generated splits are stable, - // this is necessary so that the mapping of partition to source is correct - // in each container. - for (int i = 0; i < splits.size(); i++) { - partitionMetaData.put( - new Partition(i), new SystemStreamPartitionMetadata(null, null, null)); - } - return new SystemStreamMetadata(streamName, partitionMetaData); - } catch (Exception e) { - throw new SamzaException("Fail to read stream metadata", e); - } - })); - } - - @Override - public Integer offsetComparator(String offset1, String offset2) { - if (offset1 == null) { - return offset2 == null ? 0 : -1; - } - - if (offset2 == null) { - return 1; - } - - return Long.valueOf(offset1).compareTo(Long.valueOf(offset2)); - } - } - - /** - * A {@link SystemConsumer} for a {@link BoundedSource}. See {@link BoundedSourceSystem} for more - * details. - */ - public static class Consumer implements SystemConsumer { - private static final Logger LOG = LoggerFactory.getLogger(Consumer.class); - private static final AtomicInteger NEXT_ID = new AtomicInteger(); - - private final List> splits; - private final SamzaPipelineOptions pipelineOptions; - private final Map, SystemStreamPartition> readerToSsp = new HashMap<>(); - private final SamzaMetricsContainer metricsContainer; - private final String stepName; - - private ReaderTask readerTask; - - Consumer( - BoundedSource source, - SamzaPipelineOptions pipelineOptions, - SamzaMetricsContainer metricsContainer, - String stepName) { - try { - splits = split(source, pipelineOptions); - } catch (Exception e) { - throw new SamzaException("Fail to split source", e); - } - this.pipelineOptions = pipelineOptions; - this.metricsContainer = metricsContainer; - this.stepName = stepName; - } - - @Override - public void start() { - if (this.readerToSsp.isEmpty()) { - throw new IllegalArgumentException( - "Attempted to call start without assigned system stream partitions"); - } - - final int capacity = pipelineOptions.getSystemBufferSize(); - final FnWithMetricsWrapper metricsWrapper = - pipelineOptions.getEnableMetrics() - ? new FnWithMetricsWrapper(metricsContainer, stepName) - : null; - readerTask = new ReaderTask<>(readerToSsp, capacity, metricsWrapper); - final Thread thread = - new Thread(readerTask, "bounded-source-system-consumer-" + NEXT_ID.getAndIncrement()); - thread.start(); - } - - @Override - public void stop() { - // NOTE: this is not a blocking shutdown - if (readerTask != null) { - readerTask.stop(); - } - } - - @Override - public void register(SystemStreamPartition ssp, String offset) { - final int partitionId = ssp.getPartition().getPartitionId(); - try { - final BoundedReader reader = splits.get(partitionId).createReader(pipelineOptions); - readerToSsp.put(reader, ssp); - } catch (Exception e) { - throw new SamzaException("Error while creating source reader for ssp: " + ssp, e); - } - } - - @Override - public Map> poll( - Set systemStreamPartitions, long timeout) - throws InterruptedException { - assert !readerToSsp.isEmpty(); // start should be called before poll - - final Map> envelopes = new HashMap<>(); - for (SystemStreamPartition ssp : systemStreamPartitions) { - envelopes.put(ssp, readerTask.getNextMessages(ssp, timeout)); - } - return envelopes; - } - - private static class ReaderTask implements Runnable { - private final Map, SystemStreamPartition> readerToSsp; - private final Map> queues; - private final Semaphore available; - private final FnWithMetricsWrapper metricsWrapper; - - // NOTE: we do not support recovery with a bounded source (we restart from the beginning), - // so we do not need to have a way to tie an offset to a position in the bounded source. - private long offset; - private volatile Thread readerThread; - private volatile boolean stopInvoked = false; - private volatile Exception lastException; - - private ReaderTask( - Map, SystemStreamPartition> readerToSsp, - int capacity, - FnWithMetricsWrapper metricsWrapper) { - this.readerToSsp = readerToSsp; - this.available = new Semaphore(capacity); - this.metricsWrapper = metricsWrapper; - - final Map> qs = - new HashMap<>(); - readerToSsp.values().forEach(ssp -> qs.put(ssp, new LinkedBlockingQueue<>())); - this.queues = ImmutableMap.copyOf(qs); - } - - @Override - public void run() { - readerThread = Thread.currentThread(); - - final Set> availableReaders = new HashSet<>(readerToSsp.keySet()); - try { - for (BoundedReader reader : readerToSsp.keySet()) { - boolean hasData = invoke(reader::start); - if (hasData) { - enqueueMessage(reader); - } else { - enqueueMaxWatermarkAndEndOfStream(reader); - reader.close(); - availableReaders.remove(reader); - } - } - - while (!stopInvoked && !availableReaders.isEmpty()) { - final Iterator> iter = availableReaders.iterator(); - while (iter.hasNext()) { - final BoundedReader reader = iter.next(); - final boolean hasData = invoke(reader::advance); - if (hasData) { - enqueueMessage(reader); - } else { - enqueueMaxWatermarkAndEndOfStream(reader); - reader.close(); - iter.remove(); - } - } - } - } catch (InterruptedException e) { - // We use an interrupt to wake the reader from a blocking read under normal termination, - // so ignore it here. - } catch (Exception e) { - setError(e); - } finally { - availableReaders.forEach( - reader -> { - try { - reader.close(); - } catch (IOException e) { - LOG.error( - "Reader task failed to close reader for ssp {}", readerToSsp.get(reader), e); - } - }); - } - } - - private X invoke(FnWithMetricsWrapper.SupplierWithException fn) throws Exception { - if (metricsWrapper != null) { - return metricsWrapper.wrap(fn, true); - } else { - return fn.get(); - } - } - - private void enqueueMessage(BoundedReader reader) throws InterruptedException { - final T value = reader.getCurrent(); - final WindowedValue windowedValue = - WindowedValues.timestampedValueInGlobalWindow(value, reader.getCurrentTimestamp()); - final SystemStreamPartition ssp = readerToSsp.get(reader); - final IncomingMessageEnvelope envelope = - new IncomingMessageEnvelope( - ssp, Long.toString(offset++), null, OpMessage.ofElement(windowedValue)); - - available.acquire(); - queues.get(ssp).put(envelope); - } - - private void enqueueMaxWatermarkAndEndOfStream(BoundedReader reader) { - final SystemStreamPartition ssp = readerToSsp.get(reader); - // Send the max watermark to force completion of any open windows. - final IncomingMessageEnvelope watermarkEnvelope = - IncomingMessageEnvelope.buildWatermarkEnvelope( - ssp, BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()); - enqueueUninterruptibly(watermarkEnvelope); - - final IncomingMessageEnvelope endOfStreamEnvelope = - IncomingMessageEnvelope.buildEndOfStreamEnvelope(ssp); - enqueueUninterruptibly(endOfStreamEnvelope); - } - - private void stop() { - stopInvoked = true; - - final Thread readerThread = this.readerThread; - if (readerThread != null) { - readerThread.interrupt(); - } - } - - private List getNextMessages( - SystemStreamPartition ssp, long timeoutMillis) throws InterruptedException { - if (lastException != null) { - throw new RuntimeException(lastException); - } - - final List envelopes = new ArrayList<>(); - final BlockingQueue queue = queues.get(ssp); - final IncomingMessageEnvelope envelope = queue.poll(timeoutMillis, TimeUnit.MILLISECONDS); - - if (envelope != null) { - envelopes.add(envelope); - queue.drainTo(envelopes); - } - - available.release(envelopes.size()); - - if (lastException != null) { - throw new RuntimeException(lastException); - } - - return envelopes; - } - - private void setError(Exception exception) { - this.lastException = exception; - // A dummy message used to force the consumer to wake up immediately and check the - // lastException field, which will be populated. - readerToSsp - .values() - .forEach( - ssp -> { - final IncomingMessageEnvelope checkLastExceptionEvelope = - new IncomingMessageEnvelope(ssp, null, null, null); - enqueueUninterruptibly(checkLastExceptionEvelope); - }); - } - - private void enqueueUninterruptibly(IncomingMessageEnvelope envelope) { - final BlockingQueue queue = - queues.get(envelope.getSystemStreamPartition()); - while (true) { - try { - queue.put(envelope); - return; - } catch (InterruptedException e) { - // Some events require that we post an envelope to the queue even if the interrupt - // flag was set (i.e. during a call to stop) to ensure that the consumer properly - // shuts down. Consequently, if we receive an interrupt here we ignore it and retry - // the put operation. - } - } - } - } - } - - /** - * A {@link SystemFactory} that produces a {@link BoundedSourceSystem} for a particular {@link - * BoundedSource} registered in {@link Config}. - */ - public static class Factory implements SystemFactory { - @Override - public SystemConsumer getConsumer(String systemName, Config config, MetricsRegistry registry) { - final String streamPrefix = "systems." + systemName; - final Config scopedConfig = config.subset(streamPrefix + ".", true); - - return new Consumer( - getBoundedSource(scopedConfig), - getPipelineOptions(config), - new SamzaMetricsContainer((MetricsRegistryMap) registry), - scopedConfig.get("stepName")); - } - - @Override - public SystemProducer getProducer(String systemName, Config config, MetricsRegistry registry) { - LOG.info("System {} does not have producer.", systemName); - return null; - } - - @Override - public SystemAdmin getAdmin(String systemName, Config config) { - final Config scopedConfig = config.subset("systems." + systemName + ".", true); - return new Admin(getBoundedSource(scopedConfig), getPipelineOptions(config)); - } - - private static BoundedSource getBoundedSource(Config config) { - @SuppressWarnings("unchecked") - final BoundedSource source = - Base64Serializer.deserializeUnchecked(config.get("source"), BoundedSource.class); - return source; - } - - private static SamzaPipelineOptions getPipelineOptions(Config config) { - return Base64Serializer.deserializeUnchecked( - config.get("beamPipelineOptions"), SerializablePipelineOptions.class) - .get() - .as(SamzaPipelineOptions.class); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystem.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystem.java deleted file mode 100644 index ffab2ff59ce5..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystem.java +++ /dev/null @@ -1,533 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.apache.beam.repackaged.core.org.apache.commons.lang3.StringUtils; -import org.apache.beam.runners.core.construction.SerializablePipelineOptions; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.metrics.FnWithMetricsWrapper; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.io.UnboundedSource; -import org.apache.beam.sdk.io.UnboundedSource.CheckpointMark; -import org.apache.beam.sdk.io.UnboundedSource.UnboundedReader; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.samza.Partition; -import org.apache.samza.SamzaException; -import org.apache.samza.config.Config; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemAdmin; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemFactory; -import org.apache.samza.system.SystemProducer; -import org.apache.samza.system.SystemStreamMetadata; -import org.apache.samza.system.SystemStreamMetadata.SystemStreamPartitionMetadata; -import org.apache.samza.system.SystemStreamPartition; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A Samza system that supports reading from a Beam {@link UnboundedSource}. The source is split - * into partitions. Samza creates the job model by assigning partitions to Samza tasks. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class UnboundedSourceSystem { - private static final Logger LOG = LoggerFactory.getLogger(UnboundedSourceSystem.class); - - // A dummy message used to force the consumer to wake up immediately and check the - // lastException field, which will be populated. - private static final IncomingMessageEnvelope CHECK_LAST_EXCEPTION_ENVELOPE = - new IncomingMessageEnvelope(null, null, null, null); - - /** - * For better parallelism in Samza, we need to configure a large split number for {@link - * UnboundedSource} like Kafka. This will most likely make each split contain a single partition, - * and be assigned to a Samza task. A large split number is safe since the actual split is bounded - * by the number of source partitions. - */ - private static - List> split( - UnboundedSource source, SamzaPipelineOptions pipelineOptions) - throws Exception { - final int numSplits = pipelineOptions.getMaxSourceParallelism(); - if (numSplits > 1) { - @SuppressWarnings("unchecked") - final List> splits = - (List>) source.split(numSplits, pipelineOptions); - // Need the empty check here because Samza doesn't handle empty partition well - if (!splits.isEmpty()) { - return splits; - } - } - return Collections.singletonList(source); - } - - /** A {@link SystemAdmin} for {@link UnboundedSourceSystem}. */ - public static class Admin implements SystemAdmin { - private final UnboundedSource source; - private final SamzaPipelineOptions pipelineOptions; - - public Admin(UnboundedSource source, SamzaPipelineOptions pipelineOptions) { - this.source = source; - this.pipelineOptions = pipelineOptions; - } - - @Override - public Map getOffsetsAfter( - Map offsets) { - // BEAM checkpoints the next offset so here we just need to return the map itself - return offsets; - } - - @Override - public Map getSystemStreamMetadata(Set streamNames) { - return streamNames.stream() - .collect( - Collectors.toMap( - Function.identity(), - streamName -> { - try { - final List> splits = - split(source, pipelineOptions); - final Map partitionMetaData = - new HashMap<>(); - // we assume that the generated splits are stable, - // this is necessary so that the mapping of partition to source is correct - // in each container. - for (int i = 0; i < splits.size(); i++) { - partitionMetaData.put( - new Partition(i), new SystemStreamPartitionMetadata(null, null, null)); - } - return new SystemStreamMetadata(streamName, partitionMetaData); - } catch (Exception e) { - throw new SamzaException("Fail to read stream metadata", e); - } - })); - } - - @Override - public Integer offsetComparator(String offset1, String offset2) { - // BEAM will fetch the exact offset. So we don't need to compare them. - // Return null indicating it's caught up. - return null; - } - } - - /** - * A {@link SystemConsumer} for a {@link UnboundedSource}. See {@link UnboundedSourceSystem} for - * more details. - */ - public static class Consumer - implements SystemConsumer { - private static final Logger LOG = LoggerFactory.getLogger(Consumer.class); - - private static final AtomicInteger NEXT_ID = new AtomicInteger(); - - private final Coder checkpointMarkCoder; - private final List> splits; - private final SamzaPipelineOptions pipelineOptions; - private final Map readerToSsp = new HashMap<>(); - private final SamzaMetricsContainer metricsContainer; - private final String stepName; - - private ReaderTask readerTask; - - Consumer( - UnboundedSource source, - SamzaPipelineOptions pipelineOptions, - SamzaMetricsContainer metricsContainer, - String stepName) { - try { - this.splits = split(source, pipelineOptions); - } catch (Exception e) { - throw new SamzaException("Fail to split source", e); - } - this.checkpointMarkCoder = source.getCheckpointMarkCoder(); - this.pipelineOptions = pipelineOptions; - this.metricsContainer = metricsContainer; - this.stepName = stepName; - } - - @Override - public void start() { - if (this.readerToSsp.isEmpty()) { - throw new IllegalArgumentException( - "Attempted to call start without assigned system stream partitions"); - } - - final FnWithMetricsWrapper metricsWrapper = - pipelineOptions.getEnableMetrics() - ? new FnWithMetricsWrapper(metricsContainer, stepName) - : null; - readerTask = - new ReaderTask<>( - readerToSsp, - checkpointMarkCoder, - pipelineOptions.getSystemBufferSize(), - pipelineOptions.getWatermarkInterval(), - metricsWrapper); - final Thread thread = - new Thread(readerTask, "unbounded-source-system-consumer-" + NEXT_ID.getAndIncrement()); - thread.start(); - } - - @Override - public void stop() { - // NOTE: this is not a blocking shutdown - readerTask.stop(); - } - - @Override - public void register(SystemStreamPartition ssp, String offset) { - CheckpointMarkT checkpoint = null; - if (StringUtils.isNoneEmpty(offset)) { - final byte[] offsetBytes = Base64.getDecoder().decode(offset); - final ByteArrayInputStream bais = new ByteArrayInputStream(offsetBytes); - try { - checkpoint = checkpointMarkCoder.decode(bais); - } catch (Exception e) { - throw new SamzaException("Error in decode offset", e); - } - } - - // Create unbounded reader with checkpoint - final int partitionId = ssp.getPartition().getPartitionId(); - try { - final UnboundedReader reader = - splits.get(partitionId).createReader(pipelineOptions, checkpoint); - readerToSsp.put(reader, ssp); - } catch (Exception e) { - throw new SamzaException("Error while creating source reader for ssp: " + ssp, e); - } - } - - @Override - public Map> poll( - Set systemStreamPartitions, long timeout) - throws InterruptedException { - assert !readerToSsp.isEmpty(); // start should be called before poll - - final Map> envelopes = new HashMap<>(); - for (SystemStreamPartition ssp : systemStreamPartitions) { - envelopes.put(ssp, readerTask.getNextMessages(ssp, timeout)); - } - return envelopes; - } - - private static class ReaderTask implements Runnable { - private final Map readerToSsp; - private final List readers; - private final Coder checkpointMarkCoder; - private final Map currentWatermarks = new HashMap<>(); - private final Map> queues; - private final long watermarkInterval; - private final Semaphore available; - private final FnWithMetricsWrapper metricsWrapper; - - private volatile boolean running; - private volatile Exception lastException; - private long lastWatermarkTime = 0L; - - private ReaderTask( - Map readerToSsp, - Coder checkpointMarkCoder, - int capacity, - long watermarkInterval, - FnWithMetricsWrapper metricsWrapper) { - this.readerToSsp = readerToSsp; - this.checkpointMarkCoder = checkpointMarkCoder; - this.readers = ImmutableList.copyOf(readerToSsp.keySet()); - this.watermarkInterval = watermarkInterval; - this.available = new Semaphore(capacity); - this.metricsWrapper = metricsWrapper; - - final Map> qs = - new HashMap<>(); - readerToSsp.values().forEach(ssp -> qs.put(ssp, new LinkedBlockingQueue<>())); - this.queues = ImmutableMap.copyOf(qs); - } - - @Override - public void run() { - this.running = true; - - try { - for (UnboundedReader reader : readers) { - final boolean hasData = invoke(reader::start); - if (hasData) { - available.acquire(); - enqueueMessage(reader); - } - } - - while (running) { - boolean elementAvailable = false; - for (UnboundedReader reader : readers) { - final boolean hasData = invoke(reader::advance); - if (hasData) { - while (!available.tryAcquire( - 1, - Math.max(lastWatermarkTime + watermarkInterval - System.currentTimeMillis(), 1), - TimeUnit.MILLISECONDS)) { - updateWatermark(); - } - enqueueMessage(reader); - elementAvailable = true; - } - } - - updateWatermark(); - - if (!elementAvailable) { - // TODO: make poll interval configurable - Thread.sleep(50); - } - } - } catch (Exception e) { - lastException = e; - running = false; - } finally { - readers.forEach( - reader -> { - try { - reader.close(); - } catch (IOException e) { - LOG.error("Reader task failed to close reader", e); - } - }); - } - - if (lastException != null) { - // Force any pollers to wake up - queues - .values() - .forEach( - queue -> { - queue.clear(); - queue.add(CHECK_LAST_EXCEPTION_ENVELOPE); - }); - } - } - - private X invoke(FnWithMetricsWrapper.SupplierWithException fn) throws Exception { - if (metricsWrapper != null) { - return metricsWrapper.wrap(fn, true); - } else { - return fn.get(); - } - } - - private void updateWatermark() throws InterruptedException { - final long time = System.currentTimeMillis(); - if (time - lastWatermarkTime > watermarkInterval) { - for (UnboundedReader reader : readers) { - final SystemStreamPartition ssp = readerToSsp.get(reader); - final Instant currentWatermark = - currentWatermarks.containsKey(ssp) - ? currentWatermarks.get(ssp) - : BoundedWindow.TIMESTAMP_MIN_VALUE; - final Instant nextWatermark = reader.getWatermark(); - if (currentWatermark.isBefore(nextWatermark)) { - currentWatermarks.put(ssp, nextWatermark); - if (BoundedWindow.TIMESTAMP_MAX_VALUE.isAfter(nextWatermark)) { - enqueueWatermark(reader); - } else { - // Max watermark has been reached for this reader. - enqueueMaxWatermarkAndEndOfStream(reader); - running = false; - } - } - } - - lastWatermarkTime = time; - } - } - - private void enqueueWatermark(UnboundedReader reader) throws InterruptedException { - final SystemStreamPartition ssp = readerToSsp.get(reader); - final IncomingMessageEnvelope envelope = - IncomingMessageEnvelope.buildWatermarkEnvelope(ssp, reader.getWatermark().getMillis()); - - queues.get(ssp).put(envelope); - } - - private void enqueueMessage(UnboundedReader reader) throws InterruptedException { - @SuppressWarnings("unchecked") - final T value = (T) reader.getCurrent(); - final Instant time = reader.getCurrentTimestamp(); - final SystemStreamPartition ssp = readerToSsp.get(reader); - final WindowedValue windowedValue = - WindowedValues.timestampedValueInGlobalWindow(value, time); - - final OpMessage opMessage = OpMessage.ofElement(windowedValue); - final IncomingMessageEnvelope envelope = - new IncomingMessageEnvelope(ssp, getOffset(reader), null, opMessage); - - queues.get(ssp).put(envelope); - } - - // Send an max watermark message and an end of stream message to the corresponding ssp to - // close windows and finish the task. - private void enqueueMaxWatermarkAndEndOfStream(UnboundedReader reader) { - final SystemStreamPartition ssp = readerToSsp.get(reader); - // Send the max watermark to force completion of any open windows. - final IncomingMessageEnvelope watermarkEnvelope = - IncomingMessageEnvelope.buildWatermarkEnvelope( - ssp, BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()); - enqueueUninterruptibly(watermarkEnvelope); - - final IncomingMessageEnvelope endOfStreamEnvelope = - IncomingMessageEnvelope.buildEndOfStreamEnvelope(ssp); - enqueueUninterruptibly(endOfStreamEnvelope); - } - - private void enqueueUninterruptibly(IncomingMessageEnvelope envelope) { - final BlockingQueue queue = - queues.get(envelope.getSystemStreamPartition()); - while (true) { - try { - queue.put(envelope); - return; - } catch (InterruptedException e) { - // Some events require that we post an envelope to the queue even if the interrupt - // flag was set (i.e. during a call to stop) to ensure that the consumer properly - // shuts down. Consequently, if we receive an interrupt here we ignore it and retry - // the put operation. - } - } - } - - void stop() { - running = false; - } - - List getNextMessages(SystemStreamPartition ssp, long timeoutMillis) - throws InterruptedException { - if (lastException != null) { - throw new RuntimeException(lastException); - } - - final List envelopes = new ArrayList<>(); - final BlockingQueue queue = queues.get(ssp); - final IncomingMessageEnvelope envelope = queue.poll(timeoutMillis, TimeUnit.MILLISECONDS); - - if (envelope != null) { - envelopes.add(envelope); - queue.drainTo(envelopes); - } - - final int numElements = - (int) envelopes.stream().filter(ev -> (ev.getMessage() instanceof OpMessage)).count(); - available.release(numElements); - - if (lastException != null) { - throw new RuntimeException(lastException); - } - - return envelopes; - } - - private String getOffset(UnboundedReader reader) { - try { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - @SuppressWarnings("unchecked") - final CheckpointMarkT checkpointMark = - (CheckpointMarkT) invoke(reader::getCheckpointMark); - checkpointMarkCoder.encode(checkpointMark, baos); - return Base64.getEncoder().encodeToString(baos.toByteArray()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - } - - /** - * A {@link SystemFactory} that produces a {@link UnboundedSourceSystem} for a particular {@link - * UnboundedSource} registered in {@link Config}. - */ - public static class Factory implements SystemFactory { - @Override - public SystemConsumer getConsumer(String systemName, Config config, MetricsRegistry registry) { - final String streamPrefix = "systems." + systemName; - final Config scopedConfig = config.subset(streamPrefix + ".", true); - return new Consumer( - getUnboundedSource(scopedConfig), - getPipelineOptions(config), - new SamzaMetricsContainer((MetricsRegistryMap) registry), - scopedConfig.get("stepName")); - } - - @Override - public SystemProducer getProducer(String systemName, Config config, MetricsRegistry registry) { - LOG.info("System {} does not have producer.", systemName); - return null; - } - - @Override - public SystemAdmin getAdmin(String systemName, Config config) { - final Config scopedConfig = config.subset("systems." + systemName + ".", true); - return new Admin( - getUnboundedSource(scopedConfig), getPipelineOptions(config)); - } - - private static - UnboundedSource getUnboundedSource(Config config) { - @SuppressWarnings("unchecked") - final UnboundedSource source = - Base64Serializer.deserializeUnchecked(config.get("source"), UnboundedSource.class); - return source; - } - - private static SamzaPipelineOptions getPipelineOptions(Config config) { - return Base64Serializer.deserializeUnchecked( - config.get("beamPipelineOptions"), SerializablePipelineOptions.class) - .get() - .as(SamzaPipelineOptions.class); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java deleted file mode 100644 index 582194440c9b..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.adapter; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamContainerRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamContainerRunner.java deleted file mode 100644 index 0f4a1c7a6905..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamContainerRunner.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.container; - -import java.time.Duration; -import org.apache.samza.application.SamzaApplication; -import org.apache.samza.application.descriptors.ApplicationDescriptor; -import org.apache.samza.application.descriptors.ApplicationDescriptorImpl; -import org.apache.samza.application.descriptors.ApplicationDescriptorUtil; -import org.apache.samza.config.Config; -import org.apache.samza.config.ShellCommandConfig; -import org.apache.samza.context.ExternalContext; -import org.apache.samza.job.ApplicationStatus; -import org.apache.samza.runtime.ApplicationRunner; -import org.apache.samza.runtime.ContainerLaunchUtil; -import org.apache.samza.util.SamzaUncaughtExceptionHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Runs the beam Yarn container, using the static global job model. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class BeamContainerRunner implements ApplicationRunner { - private static final Logger LOG = LoggerFactory.getLogger(BeamContainerRunner.class); - - @SuppressWarnings("rawtypes") - private final ApplicationDescriptorImpl appDesc; - - @SuppressWarnings("rawtypes") - public BeamContainerRunner(SamzaApplication app, Config config) { - this.appDesc = ApplicationDescriptorUtil.getAppDescriptor(app, config); - } - - @Override - public void run(ExternalContext externalContext) { - Thread.setDefaultUncaughtExceptionHandler( - new SamzaUncaughtExceptionHandler( - () -> { - LOG.info("Exiting process now."); - System.exit(1); - })); - - ContainerLaunchUtil.run( - appDesc, System.getenv(ShellCommandConfig.ENV_CONTAINER_ID), ContainerCfgLoader.jobModel); - } - - @Override - public void kill() { - // Do nothing. Yarn will kill the container. - } - - @Override - public ApplicationStatus status() { - // The container is running during the life span of this object. - return ApplicationStatus.Running; - } - - @Override - public void waitForFinish() { - // Container run is synchronous - // so calling waitForFinish() after run() should return immediately - LOG.info("Container has stopped"); - } - - @Override - public boolean waitForFinish(Duration timeout) { - // Container run is synchronous - // so calling waitForFinish() after run() should return immediately - LOG.info("Container has stopped"); - return true; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamJobCoordinatorRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamJobCoordinatorRunner.java deleted file mode 100644 index fb00a018fb29..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/BeamJobCoordinatorRunner.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.container; - -import java.time.Duration; -import org.apache.samza.application.SamzaApplication; -import org.apache.samza.application.descriptors.ApplicationDescriptor; -import org.apache.samza.clustermanager.JobCoordinatorLaunchUtil; -import org.apache.samza.config.Config; -import org.apache.samza.context.ExternalContext; -import org.apache.samza.job.ApplicationStatus; -import org.apache.samza.runtime.ApplicationRunner; - -/** Runs on Yarn AM, execute planning and launches JobCoordinator. */ -public class BeamJobCoordinatorRunner implements ApplicationRunner { - - @SuppressWarnings("rawtypes") - private final SamzaApplication app; - - private final Config config; - - /** - * Constructors a {@link BeamJobCoordinatorRunner} to run the {@code app} with the {@code config}. - * - * @param app application to run - * @param config configuration for the application - */ - @SuppressWarnings("rawtypes") - public BeamJobCoordinatorRunner( - SamzaApplication app, Config config) { - this.app = app; - this.config = config; - } - - @Override - public void run(ExternalContext externalContext) { - JobCoordinatorLaunchUtil.run(app, config); - } - - @Override - public void kill() { - throw new UnsupportedOperationException( - "BeamJobCoordinatorRunner#kill should never be invoked."); - } - - @Override - public ApplicationStatus status() { - throw new UnsupportedOperationException( - "BeamJobCoordinatorRunner#status should never be invoked."); - } - - @Override - public void waitForFinish() { - throw new UnsupportedOperationException( - "BeamJobCoordinatorRunner#waitForFinish should never be invoked."); - } - - @Override - public boolean waitForFinish(Duration timeout) { - throw new UnsupportedOperationException( - "BeamJobCoordinatorRunner#waitForFinish should never be invoked."); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoader.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoader.java deleted file mode 100644 index 9437aea56561..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoader.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.container; - -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import org.apache.samza.config.Config; -import org.apache.samza.config.ConfigLoader; -import org.apache.samza.config.MapConfig; -import org.apache.samza.config.ShellCommandConfig; -import org.apache.samza.container.SamzaContainer; -import org.apache.samza.job.model.JobModel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Loader for the Beam yarn container to load job model. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class ContainerCfgLoader implements ConfigLoader { - private static final Logger LOG = LoggerFactory.getLogger(ContainerCfgLoader.class); - - private static final Object LOCK = new Object(); - static volatile JobModel jobModel; - private static final Random RANDOM = new Random(); - - @Override - public Config getConfig() { - if (jobModel == null) { - synchronized (LOCK) { - if (jobModel == null) { - final String containerId = System.getenv(ShellCommandConfig.ENV_CONTAINER_ID); - LOG.info("Got container ID: {}", containerId); - final String coordinatorUrl = System.getenv(ShellCommandConfig.ENV_COORDINATOR_URL); - LOG.info("Got coordinator URL: {}", coordinatorUrl); - final int delay = RANDOM.nextInt(SamzaContainer.DEFAULT_READ_JOBMODEL_DELAY_MS()) + 1; - jobModel = SamzaContainer.readJobModel(coordinatorUrl, delay); - } - } - } - - final Map config = new HashMap<>(jobModel.getConfig()); - config.put("app.runner.class", BeamContainerRunner.class.getName()); - return new MapConfig(config); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoaderFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoaderFactory.java deleted file mode 100644 index d3b090d6e20a..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/ContainerCfgLoaderFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.container; - -import org.apache.samza.config.Config; -import org.apache.samza.config.ConfigLoader; -import org.apache.samza.config.ConfigLoaderFactory; - -/** Factory for the Beam yarn container to get loader to load job model. */ -public class ContainerCfgLoaderFactory implements ConfigLoaderFactory { - @Override - public ConfigLoader getLoader(Config config) { - return new ContainerCfgLoader(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/container/package-info.java deleted file mode 100644 index 58a09e6023de..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.container; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/DoFnRunnerWithMetrics.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/DoFnRunnerWithMetrics.java deleted file mode 100644 index 7bec91abb34d..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/DoFnRunnerWithMetrics.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.WindowedValue; -import org.joda.time.Instant; - -/** - * {@link DoFnRunner} wrapper with metrics. The class uses {@link SamzaMetricsContainer} to keep - * BEAM metrics results and update Samza metrics. - */ -public class DoFnRunnerWithMetrics implements DoFnRunner { - private final DoFnRunner underlying; - private final FnWithMetricsWrapper metricsWrapper; - - private DoFnRunnerWithMetrics( - DoFnRunner underlying, SamzaMetricsContainer metricsContainer, String stepName) { - this.underlying = underlying; - this.metricsWrapper = new FnWithMetricsWrapper(metricsContainer, stepName); - } - - public static DoFnRunner wrap( - DoFnRunner doFnRunner, SamzaMetricsContainer metricsContainer, String stepName) { - return new DoFnRunnerWithMetrics<>(doFnRunner, metricsContainer, stepName); - } - - @Override - public void startBundle() { - withMetrics(underlying::startBundle, false); - } - - @Override - public void processElement(WindowedValue elem) { - withMetrics(() -> underlying.processElement(elem), false); - } - - @Override - public void onTimer( - String timerId, - String timerFamilyId, - KeyT key, - BoundedWindow window, - Instant timestamp, - Instant outputTimestamp, - TimeDomain timeDomain, - CausedByDrain causedByDrain) { - withMetrics( - () -> - underlying.onTimer( - timerId, - timerFamilyId, - key, - window, - timestamp, - outputTimestamp, - timeDomain, - causedByDrain), - false); - } - - @Override - public void finishBundle() { - withMetrics(underlying::finishBundle, true); - } - - @Override - public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { - underlying.onWindowExpiration(window, timestamp, key); - } - - @Override - public DoFn getFn() { - return underlying.getFn(); - } - - private void withMetrics(Runnable runnable, boolean shouldUpdateMetrics) { - try { - metricsWrapper.wrap( - () -> { - runnable.run(); - return (Void) null; - }, - shouldUpdateMetrics); - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/FnWithMetricsWrapper.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/FnWithMetricsWrapper.java deleted file mode 100644 index 8052e0d66146..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/FnWithMetricsWrapper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import java.io.Closeable; -import org.apache.beam.sdk.metrics.MetricsEnvironment; - -/** This class wraps a {@link java.util.function.Supplier} function call with BEAM metrics. */ -public class FnWithMetricsWrapper { - - /** Interface for functions to be wrapped with metrics. */ - public interface SupplierWithException { - T get() throws Exception; - } - - private final SamzaMetricsContainer metricsContainer; - private final String stepName; - - public FnWithMetricsWrapper(SamzaMetricsContainer metricsContainer, String stepName) { - this.metricsContainer = metricsContainer; - this.stepName = stepName; - } - - public T wrap(SupplierWithException fn, boolean shouldUpdateMetrics) throws Exception { - try (Closeable closeable = - MetricsEnvironment.scopedMetricsContainer(metricsContainer.getContainer(stepName))) { - T result = fn.get(); - // Skip updating metrics if not necessary to improve performance - if (shouldUpdateMetrics) { - metricsContainer.updateMetrics(stepName); - } - return result; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaGBKMetricOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaGBKMetricOp.java deleted file mode 100644 index 9b6375171c23..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaGBKMetricOp.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import org.apache.beam.runners.samza.runtime.KeyedTimerData; -import org.apache.beam.runners.samza.runtime.Op; -import org.apache.beam.runners.samza.runtime.OpEmitter; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * SamzaGBKMetricOp is a {@link Op} that emits & maintains default metrics for input or output - * PCollection for GroupByKey. - * - *

For Input PCollection: It emits the input throughput and maintains avg input time for input - * PCollection per windowId. - * - *

For Output PCollection: It emits the output throughput and maintains avg output time for - * output PCollection per windowId. It is also responsible for emitting latency metric per windowId - * once the watermark passes the end of window timestamp. - * - *

Assumes that {@code SamzaGBKMetricOp#processWatermark(Instant, OpEmitter)} is exclusive of - * {@code SamzaGBKMetricOp#processElement(Instant, OpEmitter)}. Specifically, the processWatermark - * method assumes that no calls to processElement will be made during its execution, and vice versa. - * - * @param The type of the elements in the input PCollection. - */ -class SamzaGBKMetricOp implements Op { - private static final Logger LOG = LoggerFactory.getLogger(SamzaGBKMetricOp.class); - // Unique name of the PTransform this MetricOp is associated with - private final String transformFullName; - private final SamzaTransformMetricRegistry samzaTransformMetricRegistry; - // Type of the processing operation - private final SamzaMetricOpFactory.OpType opType; - - private final String pValue; - // Counters for keeping sum of arrival time and count of elements per windowId - @SuppressFBWarnings("SE_BAD_FIELD") - private final ConcurrentHashMap sumOfTimestampsPerWindowId; - - @SuppressFBWarnings("SE_BAD_FIELD") - private final ConcurrentHashMap sumOfCountPerWindowId; - // Name of the task, for logging purpose - private transient String task; - - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter emitter) { - // for logging / debugging purposes - this.task = context.getTaskContext().getTaskModel().getTaskName().getTaskName(); - // Register the transform with SamzaTransformMetricRegistry - samzaTransformMetricRegistry.register(transformFullName, pValue, context); - } - - // Some fields are initialized in open() method, which is called after the constructor. - @SuppressWarnings("initialization.fields.uninitialized") - public SamzaGBKMetricOp( - String pValue, - String transformFullName, - SamzaMetricOpFactory.OpType opType, - SamzaTransformMetricRegistry samzaTransformMetricRegistry) { - this.pValue = pValue; - this.transformFullName = transformFullName; - this.opType = opType; - this.samzaTransformMetricRegistry = samzaTransformMetricRegistry; - this.sumOfTimestampsPerWindowId = new ConcurrentHashMap<>(); - this.sumOfCountPerWindowId = new ConcurrentHashMap<>(); - } - - @Override - public void processElement(WindowedValue inputElement, OpEmitter emitter) { - // one element can belong to multiple windows - for (BoundedWindow windowId : inputElement.getWindows()) { - // Atomic updates to counts - sumOfCountPerWindowId.compute( - windowId, - (key, value) -> { - value = value == null ? Long.valueOf(0) : value; - return ++value; - }); - // Atomic updates to sum of arrival timestamps - sumOfTimestampsPerWindowId.compute( - windowId, - (key, value) -> { - value = value == null ? BigInteger.ZERO : value; - return value.add(BigInteger.valueOf(System.nanoTime())); - }); - } - - switch (opType) { - case INPUT: - samzaTransformMetricRegistry - .getTransformMetrics() - .getTransformInputThroughput(transformFullName) - .inc(); - break; - case OUTPUT: - samzaTransformMetricRegistry - .getTransformMetrics() - .getTransformOutputThroughput(transformFullName) - .inc(); - break; - } - emitter.emitElement(inputElement); - } - - @Override - public void processWatermark(Instant watermark, OpEmitter emitter) { - final List closedWindows = new ArrayList<>(); - sumOfCountPerWindowId.keySet().stream() - .filter(windowId -> watermark.isAfter(windowId.maxTimestamp())) // window is closed - .forEach( - windowId -> { - // In case if BigInteger overflows for long we only retain the last 64 bits of the sum - long sumOfTimestamps = - sumOfTimestampsPerWindowId.get(windowId) != null - ? sumOfTimestampsPerWindowId.get(windowId).longValue() - : 0L; - long count = sumOfCountPerWindowId.get(windowId); - closedWindows.add(windowId); - - if (LOG.isDebugEnabled()) { - LOG.debug( - "Processing {} Watermark for Transform: {}, WindowId:{}, count: {}, sumOfTimestamps: {}, task: {}", - opType, - transformFullName, - windowId, - count, - sumOfTimestamps, - task); - } - - // if the window is closed and there is some data - if (sumOfTimestamps > 0 && count > 0) { - switch (opType) { - case INPUT: - // Update the arrival time for the window - samzaTransformMetricRegistry.updateArrivalTimeMap( - transformFullName, windowId, Math.floorDiv(sumOfTimestamps, count)); - break; - case OUTPUT: - // Compute the latency if there is some data for the window - samzaTransformMetricRegistry.emitLatencyMetric( - transformFullName, windowId, Math.floorDiv(sumOfTimestamps, count), task); - break; - } - } - }); - - // remove the closed windows - sumOfCountPerWindowId.keySet().removeAll(closedWindows); - sumOfTimestampsPerWindowId.keySet().removeAll(closedWindows); - - // Update the watermark progress for the transform output - if (opType == SamzaMetricOpFactory.OpType.OUTPUT) { - samzaTransformMetricRegistry - .getTransformMetrics() - .getTransformWatermarkProgress(transformFullName) - .set(watermark.getMillis()); - } - - emitter.emitWatermark(watermark); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOp.java deleted file mode 100644 index 0f5334546c7c..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOp.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import org.apache.beam.runners.samza.runtime.KeyedTimerData; -import org.apache.beam.runners.samza.runtime.Op; -import org.apache.beam.runners.samza.runtime.OpEmitter; -import org.apache.beam.runners.samza.util.PipelineJsonRenderer; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * SamzaMetricOp is a metric Op that emits & maintains default transform metrics for inputs & - * outputs PCollection to the non data-shuffle transform. It emits the output throughput and - * maintains avg arrival time for input & output PCollection per watermark. - * - *

Assumes that {@code SamzaMetricOp#processWatermark(Instant, OpEmitter)} is exclusive of {@code - * SamzaMetricOp#processElement(Instant, OpEmitter)}. Specifically, the processWatermark method - * assumes that no calls to processElement will be made during its execution, and vice versa. - * - * @param The type of the elements in the output PCollection. - */ -class SamzaMetricOp implements Op { - // Unique name of the PTransform this MetricOp is associated with - private final String transformFullName; - private final SamzaTransformMetricRegistry samzaTransformMetricRegistry; - // Name or identifier of the PCollection which PTransform is processing - private final String pValue; - // Counters for output throughput - private final AtomicLong count; - private final AtomicReference sumOfTimestamps; - // Type of the PTransform input or output - private final SamzaMetricOpFactory.OpType opType; - // List of input PValue(s) for all PCollections processing the PTransform - private transient List transformInputs; - // List of output PValue(s) for all PCollections processing the PTransform - private transient List transformOutputs; - // Name of the task, for logging purpose - private transient String task; - - private static final Logger LOG = LoggerFactory.getLogger(SamzaMetricOp.class); - - // Some fields are initialized in open() method, which is called after the constructor. - @SuppressWarnings("initialization.fields.uninitialized") - public SamzaMetricOp( - @NonNull String pValue, - @NonNull String transformFullName, - SamzaMetricOpFactory.OpType opType, - @NonNull SamzaTransformMetricRegistry samzaTransformMetricRegistry) { - this.transformFullName = transformFullName; - this.samzaTransformMetricRegistry = samzaTransformMetricRegistry; - this.pValue = pValue; - this.opType = opType; - this.count = new AtomicLong(0L); - this.sumOfTimestamps = new AtomicReference<>(BigInteger.ZERO); - } - - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter emitter) { - final Map.Entry, List> transformInputOutput = - PipelineJsonRenderer.getTransformIOMap(config).get(transformFullName); - this.transformInputs = - transformInputOutput != null ? transformInputOutput.getKey() : new ArrayList(); - this.transformOutputs = - transformInputOutput != null ? transformInputOutput.getValue() : new ArrayList(); - // for logging / debugging purposes - this.task = context.getTaskContext().getTaskModel().getTaskName().getTaskName(); - // Register the transform with SamzaTransformMetricRegistry - samzaTransformMetricRegistry.register(transformFullName, pValue, context); - } - - @Override - public void processElement(WindowedValue inputElement, OpEmitter emitter) { - // update counters for timestamps - count.incrementAndGet(); - sumOfTimestamps.updateAndGet(sum -> sum.add(BigInteger.valueOf(System.nanoTime()))); - switch (opType) { - case INPUT: - samzaTransformMetricRegistry - .getTransformMetrics() - .getTransformInputThroughput(transformFullName) - .inc(); - break; - case OUTPUT: - samzaTransformMetricRegistry - .getTransformMetrics() - .getTransformOutputThroughput(transformFullName) - .inc(); - break; - } - emitter.emitElement(inputElement); - } - - @Override - @SuppressWarnings({"CompareToZero"}) - public void processWatermark(Instant watermark, OpEmitter emitter) { - if (LOG.isDebugEnabled()) { - LOG.debug( - "Processing Output Watermark for Transform: {} Count: {} SumOfTimestamps: {} for Watermark: {} for Task: {}", - transformFullName, - count.get(), - sumOfTimestamps.get().longValue(), - watermark.getMillis(), - task); - } - - // if there is no input data then counters will be zero and only watermark will progress - if (count.get() > 0) { - // if BigInt.longValue is out of range for long then only the low-order 64 bits are retained - long avg = Math.floorDiv(sumOfTimestamps.get().longValue(), count.get()); - // Update MetricOp Registry with avg arrival for the pValue - samzaTransformMetricRegistry.updateArrivalTimeMap( - transformFullName, pValue, watermark.getMillis(), avg); - if (opType == SamzaMetricOpFactory.OpType.OUTPUT) { - // compute & emit the latency metric if the opType is OUTPUT - samzaTransformMetricRegistry.emitLatencyMetric( - transformFullName, transformInputs, transformOutputs, watermark.getMillis(), task); - } - } - - if (opType == SamzaMetricOpFactory.OpType.OUTPUT) { - // update output watermark progress metric - samzaTransformMetricRegistry - .getTransformMetrics() - .getTransformWatermarkProgress(transformFullName) - .set(watermark.getMillis()); - } - - // reset all counters - count.set(0L); - this.sumOfTimestamps.set(BigInteger.ZERO); - emitter.emitWatermark(watermark); - } - - @VisibleForTesting - void init(List transformInputs, List transformOutputs) { - this.transformInputs = transformInputs; - this.transformOutputs = transformOutputs; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOpFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOpFactory.java deleted file mode 100644 index a4112a510459..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricOpFactory.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import org.apache.beam.runners.samza.runtime.Op; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.checkerframework.checker.nullness.qual.NonNull; - -/** - * Factory class to create {@link Op} for default transform metric computation. - * - *

Each metric Op computes and emits default throughput, latency & watermark progress metric per - * transform for Beam Samza Runner. A metric Op can be either attached to Input PCollection or - * Output PCollection of a PTransform. - * - *

Each concrete metric OP is responsible for following metrics computation: 1. Throughput: Emit - * the number of elements processed in the PCollection 2. Watermark Progress: Emit the output - * watermark progress of the PCollection 3. Latency: Maintain the avg arrival time per watermark - * across elements it processes, compute & emit the latency - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://issues.apache.org/jira/browse/BEAM-10556) - "nullness" -}) // TODO(https://issues.apache.org/jira/browse/BEAM-10402) -public class SamzaMetricOpFactory { - public enum OpType { - INPUT, - OUTPUT - } - - /** - * Create a {@link Op} for default transform metric computation. - * - * @param urn URN of the PCollection metric Op is processing - * @param pValue name of the PCollection metric Op is processing - * @param transformName name of the PTransform for which metric Op is created - * @param opType type of the metric - * @param samzaTransformMetricRegistry metric registry - * @param type of the message - * @return a {@link Op} for default transform metric computation - */ - public static @NonNull Op createMetricOp( - @NonNull String urn, - @NonNull String pValue, - @NonNull String transformName, - @NonNull OpType opType, - @NonNull SamzaTransformMetricRegistry samzaTransformMetricRegistry) { - if (isDataShuffleTransform(urn)) { - return new SamzaGBKMetricOp<>(pValue, transformName, opType, samzaTransformMetricRegistry); - } - return new SamzaMetricOp<>(pValue, transformName, opType, samzaTransformMetricRegistry); - } - - private static boolean isDataShuffleTransform(String urn) { - return urn.equals(PTransformTranslation.GROUP_BY_KEY_TRANSFORM_URN) - || urn.equals(PTransformTranslation.COMBINE_PER_KEY_TRANSFORM_URN); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricsContainer.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricsContainer.java deleted file mode 100644 index 1679b748b10b..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaMetricsContainer.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import static org.apache.beam.runners.core.metrics.MetricsContainerStepMap.asAttemptedOnlyMetricResults; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import org.apache.beam.runners.core.metrics.MetricsContainerStepMap; -import org.apache.beam.sdk.metrics.GaugeResult; -import org.apache.beam.sdk.metrics.MetricQueryResults; -import org.apache.beam.sdk.metrics.MetricResult; -import org.apache.beam.sdk.metrics.MetricResults; -import org.apache.beam.sdk.metrics.MetricsContainer; -import org.apache.beam.sdk.metrics.MetricsFilter; -import org.apache.samza.metrics.Counter; -import org.apache.samza.metrics.Gauge; -import org.apache.samza.metrics.Metric; -import org.apache.samza.metrics.MetricsRegistryMap; - -/** - * This class holds the {@link MetricsContainer}s for BEAM metrics, and update the results to Samza - * metrics. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaMetricsContainer { - private static final String BEAM_METRICS_GROUP = "BeamMetrics"; - - private final MetricsContainerStepMap metricsContainers = new MetricsContainerStepMap(); - private final MetricsRegistryMap metricsRegistry; - - public SamzaMetricsContainer(MetricsRegistryMap metricsRegistry) { - this.metricsRegistry = metricsRegistry; - this.metricsRegistry.metrics().put(BEAM_METRICS_GROUP, new ConcurrentHashMap<>()); - } - - public MetricsContainer getContainer(String stepName) { - return this.metricsContainers.getContainer(stepName); - } - - public MetricsContainerStepMap getContainers() { - return this.metricsContainers; - } - - public void updateMetrics(String stepName) { - assert metricsRegistry != null; - - final MetricResults metricResults = asAttemptedOnlyMetricResults(metricsContainers); - final MetricQueryResults results = - metricResults.queryMetrics(MetricsFilter.builder().addStep(stepName).build()); - - final CounterUpdater updateCounter = new CounterUpdater(); - results.getCounters().forEach(updateCounter); - - final GaugeUpdater updateGauge = new GaugeUpdater(); - results.getGauges().forEach(updateGauge); - - // TODO(https://github.com/apache/beam/issues/21043): add distribution metrics to Samza - } - - public void updateExecutableStageBundleMetric(String metricName, long time) { - @SuppressWarnings("unchecked") - Gauge gauge = (Gauge) getSamzaMetricFor(metricName); - if (gauge == null) { - gauge = metricsRegistry.newGauge(BEAM_METRICS_GROUP, metricName, 0L); - } - gauge.set(time); - } - - private class CounterUpdater implements Consumer> { - @Override - public void accept(MetricResult metricResult) { - final String metricName = getMetricName(metricResult); - Counter counter = (Counter) getSamzaMetricFor(metricName); - if (counter == null) { - counter = metricsRegistry.newCounter(BEAM_METRICS_GROUP, metricName); - } - counter.dec(counter.getCount()); - counter.inc(metricResult.getAttempted()); - } - } - - private class GaugeUpdater implements Consumer> { - @Override - public void accept(MetricResult metricResult) { - final String metricName = getMetricName(metricResult); - @SuppressWarnings("unchecked") - Gauge gauge = (Gauge) getSamzaMetricFor(metricName); - if (gauge == null) { - gauge = metricsRegistry.newGauge(BEAM_METRICS_GROUP, metricName, 0L); - } - gauge.set(metricResult.getAttempted().getValue()); - } - } - - private Metric getSamzaMetricFor(String metricName) { - return metricsRegistry.getGroup(BEAM_METRICS_GROUP).get(metricName); - } - - private static String getMetricName(MetricResult metricResult) { - return metricResult.getKey().toString(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetricRegistry.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetricRegistry.java deleted file mode 100644 index add207752f06..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetricRegistry.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.Serializable; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; -import org.apache.samza.context.Context; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * SamzaTransformMetricRegistry is a registry that maintains the metrics for each transform. It - * maintains the average arrival time for each PCollection for a primitive transform. - * - *

For a non-data shuffling primitive transform, the average arrival time is calculated per - * watermark, per PCollection {@link org.apache.beam.sdk.values.PValue} and updated in - * avgArrivalTimeMap - */ -public class SamzaTransformMetricRegistry implements Serializable { - private static final Logger LOG = LoggerFactory.getLogger(SamzaTransformMetricRegistry.class); - - // TransformName -> PValue for pCollection -> Map - private final ConcurrentHashMap>> - avgArrivalTimeMap; - // TransformName -> Map - @SuppressFBWarnings("SE_BAD_FIELD") - private final ConcurrentHashMap> - avgArrivalTimeMapForGbk; - - // Per Transform Metrics for each primitive transform - private final SamzaTransformMetrics transformMetrics; - - public SamzaTransformMetricRegistry() { - this.avgArrivalTimeMap = new ConcurrentHashMap<>(); - this.avgArrivalTimeMapForGbk = new ConcurrentHashMap<>(); - this.transformMetrics = new SamzaTransformMetrics(); - } - - @VisibleForTesting - SamzaTransformMetricRegistry(SamzaTransformMetrics samzaTransformMetrics) { - this.transformMetrics = samzaTransformMetrics; - this.avgArrivalTimeMap = new ConcurrentHashMap<>(); - this.avgArrivalTimeMapForGbk = new ConcurrentHashMap<>(); - } - - public void register(String transformFullName, String pValue, Context ctx) { - transformMetrics.register(transformFullName, ctx); - // initialize the map for the transform - avgArrivalTimeMap.putIfAbsent(transformFullName, new ConcurrentHashMap<>()); - avgArrivalTimeMap.get(transformFullName).putIfAbsent(pValue, new ConcurrentHashMap<>()); - avgArrivalTimeMapForGbk.putIfAbsent(transformFullName, new ConcurrentHashMap<>()); - } - - public SamzaTransformMetrics getTransformMetrics() { - return transformMetrics; - } - - public void updateArrivalTimeMap(String transformName, String pValue, long watermark, long avg) { - if (avgArrivalTimeMap.get(transformName) != null - && avgArrivalTimeMap.get(transformName).get(pValue) != null) { - ConcurrentHashMap avgArrivalTimeMapForPValue = - avgArrivalTimeMap.get(transformName).get(pValue); - // update the average arrival time for the latest watermark - avgArrivalTimeMapForPValue.put(watermark, avg); - // remove any stale entries which are lesser than the watermark - avgArrivalTimeMapForPValue.entrySet().removeIf(entry -> entry.getKey() < watermark); - } - } - - public void updateArrivalTimeMap(String transformName, BoundedWindow windowId, long avg) { - ConcurrentHashMap avgArrivalTimeMapForTransform = - avgArrivalTimeMapForGbk.get(transformName); - if (avgArrivalTimeMapForTransform != null) { - avgArrivalTimeMapForTransform.put(windowId, avg); - } - } - - @SuppressWarnings("nullness") - public void emitLatencyMetric( - String transformName, BoundedWindow windowId, long avgArrivalEndTime, String taskName) { - Long avgArrivalStartTime = - avgArrivalTimeMapForGbk.get(transformName) != null - ? avgArrivalTimeMapForGbk.get(transformName).remove(windowId) - : null; - - if (avgArrivalStartTime == null || avgArrivalStartTime == 0 || avgArrivalEndTime == 0) { - LOG.debug( - "Failure to Emit Metric for Transform: {}, Start-Time: {} or End-Time: {} found is 0/null for windowId: {}, task: {}", - transformName, - avgArrivalStartTime, - avgArrivalEndTime, - windowId, - taskName); - return; - } - - if (LOG.isDebugEnabled()) { - LOG.debug( - "Success Emit Metric for Transform: {}, window: {} for task: {}", - transformName, - windowId, - taskName); - } - transformMetrics - .getTransformLatencyMetric(transformName) - .update(avgArrivalEndTime - avgArrivalStartTime); - - transformMetrics - .getTransformCacheSize(transformName) - .set((long) avgArrivalTimeMapForGbk.get(transformName).size()); - } - - // Checker framework bug: https://github.com/typetools/checker-framework/issues/979 - @SuppressWarnings("return") - public void emitLatencyMetric( - String transformName, - List inputs, - List outputs, - Long watermark, - String taskName) { - final ConcurrentHashMap> avgArrivalTimeMapForTransform = - avgArrivalTimeMap.get(transformName); - - if (avgArrivalTimeMapForTransform == null || inputs.isEmpty() || outputs.isEmpty()) { - return; - } - - // get the avg arrival times for all the input PValues - final List inputPValuesAvgArrivalTimes = - inputs.stream() - .map(avgArrivalTimeMapForTransform::get) - .map(map -> map == null ? null : map.remove(watermark)) - .filter(avgArrivalTime -> avgArrivalTime != null) - .collect(Collectors.toList()); - - // get the avg arrival times for all the output PValues - final List outputPValuesAvgArrivalTimes = - outputs.stream() - .map(avgArrivalTimeMapForTransform::get) - .map(map -> map == null ? null : map.remove(watermark)) - .filter(avgArrivalTime -> avgArrivalTime != null) - .collect(Collectors.toList()); - - if (inputPValuesAvgArrivalTimes.isEmpty() || outputPValuesAvgArrivalTimes.isEmpty()) { - LOG.debug( - "Failure to Emit Metric for Transform: {} inputArrivalTime: {} or outputArrivalTime: {} not found for Watermark: {} Task: {}", - transformName, - inputPValuesAvgArrivalTimes, - inputPValuesAvgArrivalTimes, - watermark, - taskName); - return; - } - - final long startTime = Collections.min(inputPValuesAvgArrivalTimes); - final long endTime = Collections.max(outputPValuesAvgArrivalTimes); - final long latency = endTime - startTime; - transformMetrics.getTransformLatencyMetric(transformName).update(latency); - - transformMetrics - .getTransformCacheSize(transformName) - .set( - avgArrivalTimeMapForTransform.values().stream() - .mapToLong(ConcurrentHashMap::size) - .sum()); - - LOG.debug( - "Success Emit Metric Transform: {} for watermark: {} for task: {}", - transformName, - watermark, - taskName); - } - - @VisibleForTesting - @Nullable - ConcurrentHashMap> getAverageArrivalTimeMap( - String transformName) { - return avgArrivalTimeMap.get(transformName); - } - - @VisibleForTesting - @Nullable - ConcurrentHashMap getAverageArrivalTimeMapForGBK(String transformName) { - return avgArrivalTimeMapForGbk.get(transformName); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetrics.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetrics.java deleted file mode 100644 index 229b6da4e7c0..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/SamzaTransformMetrics.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.Serializable; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.apache.samza.context.Context; -import org.apache.samza.metrics.Counter; -import org.apache.samza.metrics.Gauge; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.metrics.SlidingTimeWindowReservoir; -import org.apache.samza.metrics.Timer; - -/** - * Metrics like throughput, latency and watermark progress for each Beam transform for Samza Runner. - */ -@SuppressWarnings("return") -public class SamzaTransformMetrics implements Serializable { - private static final String ENABLE_TASK_METRICS = "runner.samza.transform.enable.task.metrics"; - - private static final int DEFAULT_LOOKBACK_TIMER_WINDOW_SIZE_MS = 180000; - private static final String GROUP = "SamzaBeamTransformMetrics"; - private static final String TRANSFORM_LATENCY_METRIC = "handle-message-ns"; - private static final String TRANSFORM_WATERMARK_PROGRESS = "output-watermark-ms"; - private static final String TRANSFORM_IP_THROUGHPUT = "num-input-messages"; - private static final String TRANSFORM_OP_THROUGHPUT = "num-output-messages"; - - private static final String TRANSFORM_ARRIVAL_TIME_CACHE_SIZE = "in-mem-cache-size"; - - // Transform name to metric maps - @SuppressFBWarnings("SE_BAD_FIELD") - private final Map transformLatency; - - @SuppressFBWarnings("SE_BAD_FIELD") - private final Map> transformWatermarkProgress; - - @SuppressFBWarnings("SE_BAD_FIELD") - private final Map transformInputThroughput; - - @SuppressFBWarnings("SE_BAD_FIELD") - private final Map transformOutputThroughPut; - - @SuppressFBWarnings("SE_BAD_FIELD") - private final Map> transformCacheSize; - - public SamzaTransformMetrics() { - this.transformLatency = new ConcurrentHashMap<>(); - this.transformOutputThroughPut = new ConcurrentHashMap<>(); - this.transformWatermarkProgress = new ConcurrentHashMap<>(); - this.transformInputThroughput = new ConcurrentHashMap<>(); - this.transformCacheSize = new ConcurrentHashMap<>(); - } - - public void register(String transformName, Context ctx) { - // Output Watermark metric per transform will always be per transform, per task, since per - // container output watermark is not useful for debugging - transformWatermarkProgress.putIfAbsent( - transformName, - ctx.getTaskContext() - .getTaskMetricsRegistry() - .newGauge( - GROUP, getMetricNameWithPrefix(TRANSFORM_WATERMARK_PROGRESS, transformName), 0L)); - - // Latency, throughput metrics can be per container (default) or per task - final boolean enablePerTaskMetrics = - ctx.getJobContext().getConfig().getBoolean(ENABLE_TASK_METRICS, false); - final MetricsRegistry metricsRegistry = - enablePerTaskMetrics - ? ctx.getTaskContext().getTaskMetricsRegistry() - : ctx.getContainerContext().getContainerMetricsRegistry(); - transformLatency.putIfAbsent( - transformName, - metricsRegistry.newTimer(GROUP, getTimerWithCustomizedLookBackWindow(transformName))); - transformOutputThroughPut.putIfAbsent( - transformName, - metricsRegistry.newCounter( - GROUP, getMetricNameWithPrefix(TRANSFORM_OP_THROUGHPUT, transformName))); - transformInputThroughput.putIfAbsent( - transformName, - metricsRegistry.newCounter( - GROUP, getMetricNameWithPrefix(TRANSFORM_IP_THROUGHPUT, transformName))); - transformCacheSize.putIfAbsent( - transformName, - ctx.getTaskContext() - .getTaskMetricsRegistry() - .newGauge( - GROUP, - getMetricNameWithPrefix(TRANSFORM_ARRIVAL_TIME_CACHE_SIZE, transformName), - 0L)); - } - - public Timer getTransformLatencyMetric(String transformName) { - return transformLatency.get(transformName); - } - - public Counter getTransformInputThroughput(String transformName) { - return transformInputThroughput.get(transformName); - } - - public Counter getTransformOutputThroughput(String transformName) { - return transformOutputThroughPut.get(transformName); - } - - public Gauge getTransformCacheSize(String transformName) { - return transformCacheSize.get(transformName); - } - - public Gauge getTransformWatermarkProgress(String transformName) { - return transformWatermarkProgress.get(transformName); - } - - // Customize in-memory window size for timer, default from samza is 5 mins which causes memory - // pressure if a lot of timers are registered - private static Timer getTimerWithCustomizedLookBackWindow(String transformName) { - return new Timer( - getMetricNameWithPrefix(TRANSFORM_LATENCY_METRIC, transformName), - new SlidingTimeWindowReservoir(DEFAULT_LOOKBACK_TIMER_WINDOW_SIZE_MS)); - } - - private static String getMetricNameWithPrefix(String metricName, String transformName) { - // Replace all non-alphanumeric characters with underscore - final String samzaSafeMetricName = transformName.replaceAll("[^A-Za-z0-9_]", "_"); - return String.format("%s-%s", samzaSafeMetricName, metricName); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/package-info.java deleted file mode 100644 index 97415846e310..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.metrics; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/package-info.java deleted file mode 100644 index 549a4d81c2de..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java deleted file mode 100644 index e1bc9251a304..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.stream.Collectors; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.WindowedValue; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This {@link DoFnRunner} adds the capability of executing the {@link - * org.apache.beam.sdk.transforms.DoFn.ProcessElement} in the thread pool, and returns the future to - * the collector for the underlying async execution. - */ -public class AsyncDoFnRunner implements DoFnRunner { - private static final Logger LOG = LoggerFactory.getLogger(AsyncDoFnRunner.class); - - // A dummy key to represent null keys - private static final Object NULL_KEY = new Object(); - - private final DoFnRunner underlying; - private final ExecutorService executor; - private final OpEmitter emitter; - private final FutureCollector futureCollector; - private final boolean isStateful; - - /** - * This map keeps track of the last outputFutures for a certain key. When the next element of the - * key comes in, its outputFutures will be chained from the last outputFutures in the map. When - * all futures of a key have been complete, the key entry will be removed. The map is bounded by - * (bundle size * 2). - */ - private final Map>>> keyedOutputFutures; - - public static AsyncDoFnRunner create( - DoFnRunner runner, - OpEmitter emitter, - FutureCollector futureCollector, - boolean isStateful, - SamzaPipelineOptions options) { - - LOG.info("Run DoFn with {}", AsyncDoFnRunner.class.getName()); - return new AsyncDoFnRunner<>(runner, emitter, futureCollector, isStateful, options); - } - - private AsyncDoFnRunner( - DoFnRunner runner, - OpEmitter emitter, - FutureCollector futureCollector, - boolean isStateful, - SamzaPipelineOptions options) { - this.underlying = runner; - this.executor = options.getExecutorServiceForProcessElement(); - this.emitter = emitter; - this.futureCollector = futureCollector; - this.isStateful = isStateful; - this.keyedOutputFutures = new ConcurrentHashMap<>(); - } - - @Override - public void startBundle() { - underlying.startBundle(); - } - - @Override - public void processElement(WindowedValue elem) { - final CompletableFuture>> outputFutures = - isStateful ? processStateful(elem) : processElement(elem, null); - - futureCollector.addAll(outputFutures); - } - - private CompletableFuture>> processElement( - WindowedValue elem, - @Nullable CompletableFuture>> prevOutputFuture) { - - final CompletableFuture>> prevFuture = - prevOutputFuture == null - ? CompletableFuture.completedFuture(Collections.emptyList()) - : prevOutputFuture; - - // For ordering by key, we chain the processing of the elem to the completion of - // the previous output of the same key - return prevFuture.thenApplyAsync( - x -> { - underlying.processElement(elem); - - return emitter.collectOutput().stream() - .map(OpMessage::getElement) - .collect(Collectors.toList()); - }, - executor); - } - - private CompletableFuture>> processStateful( - WindowedValue elem) { - final Object key = getKey(elem); - - final CompletableFuture>> outputFutures = - processElement(elem, keyedOutputFutures.get(key)); - - // Update the latest outputFuture for key - keyedOutputFutures.put(key, outputFutures); - - // Remove the outputFuture from the map once it's complete. - // This ensures the map will be cleaned up immediately. - return outputFutures.thenApply( - output -> { - // Under the condition that the outputFutures has not been updated - keyedOutputFutures.remove(key, outputFutures); - return output; - }); - } - - /** Package private for testing. */ - boolean hasOutputFuturesForKey(Object key) { - return keyedOutputFutures.containsKey(key); - } - - @Override - public void onTimer( - String timerId, - String timerFamilyId, - KeyT key, - BoundedWindow window, - Instant timestamp, - Instant outputTimestamp, - TimeDomain timeDomain, - CausedByDrain causedByDrain) { - underlying.onTimer( - timerId, timerFamilyId, key, window, timestamp, outputTimestamp, timeDomain, causedByDrain); - } - - @Override - public void finishBundle() { - underlying.finishBundle(); - } - - @Override - public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { - underlying.onWindowExpiration(window, timestamp, key); - } - - @Override - public DoFn getFn() { - return underlying.getFn(); - } - - private Object getKey(WindowedValue elem) { - KV kv = (KV) elem.getValue(); - if (kv == null) { - return NULL_KEY; - } else { - Object key = kv.getKey(); - return key == null ? NULL_KEY : key; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/BundleManager.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/BundleManager.java deleted file mode 100644 index 36ba19d7da3c..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/BundleManager.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import org.joda.time.Instant; - -/** - * Bundle management for the {@link DoFnOp} that handles lifecycle of a bundle. It also serves as a - * proxy for the {@link DoFnOp} to process watermark and decides to 1. Hold watermark if there is at - * least one bundle in progress. 2. Propagates the watermark to downstream DAG, if all the previous - * bundles have completed. - * - *

A bundle is considered complete only when the outputs corresponding to each element in the - * bundle have been resolved and the watermark associated with the bundle(if any) is propagated - * downstream. The output of an element is considered resolved based on the nature of the ParDoFn 1. - * In case of synchronous ParDo, outputs of the element is resolved immediately after the - * processElement returns. 2. In case of asynchronous ParDo, outputs of the element is resolved when - * all the future emitted by the processElement is resolved. - * - * @param output type of the {@link DoFnOp} - */ -public interface BundleManager { - /** Starts a new bundle if not already started, then adds an element to the existing bundle. */ - void tryStartBundle(); - - /** - * Signals a watermark event arrived. The BundleManager will decide if the watermark needs to be - * processed, and notify the listener if needed. - * - * @param watermark - * @param emitter - */ - void processWatermark(Instant watermark, OpEmitter emitter); - - /** - * Signals the BundleManager that a timer is up. - * - * @param keyedTimerData - * @param emitter - */ - void processTimer(KeyedTimerData keyedTimerData, OpEmitter emitter); - - /** - * Fails the current bundle, throws away the pending output, and resets the bundle to an empty - * state. - * - * @param t the throwable that caused the failure. - */ - void signalFailure(Throwable t); - - /** - * Tries to close the bundle, and reset the bundle to an empty state. - * - * @param emitter - */ - void tryFinishBundle(OpEmitter emitter); - - /** - * A listener used to track the lifecycle of a bundle. Typically, the lifecycle of a bundle - * consists of 1. Start bundle - Invoked when the bundle is started 2. Finish bundle - Invoked - * when the bundle is complete. Refer to the docs under {@link BundleManager} for definition on - * when a bundle is considered complete. 3. onWatermark - Invoked when watermark is ready to be - * propagated to downstream DAG. Refer to the docs under {@link BundleManager} on when watermark - * is held vs propagated. - * - * @param - */ - interface BundleProgressListener { - void onBundleStarted(); - - void onBundleFinished(OpEmitter emitter); - - void onWatermark(Instant watermark, OpEmitter emitter); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/ClassicBundleManager.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/ClassicBundleManager.java deleted file mode 100644 index 53b6968e1119..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/ClassicBundleManager.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import javax.annotation.Nullable; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@inheritDoc} Implementation of BundleManager for non-portable mode. Keeps track of the async - * function completions. - * - *

This class is not thread safe and the current implementation relies on the assumption that - * messages are dispatched to BundleManager in a single threaded mode. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class ClassicBundleManager implements BundleManager { - private static final Logger LOG = LoggerFactory.getLogger(ClassicBundleManager.class); - private static final long MIN_BUNDLE_CHECK_TIME_MS = 10L; - - private final long maxBundleSize; - private final long maxBundleTimeMs; - private final BundleProgressListener bundleProgressListener; - private final FutureCollector futureCollector; - private final Scheduler> bundleTimerScheduler; - private final String bundleCheckTimerId; - - // Number elements belonging to the current active bundle - private transient AtomicLong currentBundleElementCount; - // Number of bundles that are in progress but not yet finished - private transient AtomicLong pendingBundleCount; - // Denotes the start time of the current active bundle - private transient AtomicLong bundleStartTime; - // Denotes if there is an active in progress bundle. Note at a given time, we can have multiple - // bundle in progress. - // This flag denotes if there is a bundle that is current and hasn't been closed. - private transient AtomicBoolean isBundleStarted; - // Holder for watermark which gets propagated when the bundle is finished. - private transient Instant bundleWatermarkHold; - // A future that is completed once all futures belonging to the current active bundle are - // completed. The value is null if there are no futures in the current active bundle. - private transient AtomicReference> currentActiveBundleDoneFutureReference; - private transient CompletionStage watermarkFuture; - - public ClassicBundleManager( - BundleProgressListener bundleProgressListener, - FutureCollector futureCollector, - long maxBundleSize, - long maxBundleTimeMs, - Scheduler> bundleTimerScheduler, - String bundleCheckTimerId) { - this.maxBundleSize = maxBundleSize; - this.maxBundleTimeMs = maxBundleTimeMs; - this.bundleProgressListener = bundleProgressListener; - this.bundleTimerScheduler = bundleTimerScheduler; - this.bundleCheckTimerId = bundleCheckTimerId; - this.futureCollector = futureCollector; - - if (maxBundleSize > 1) { - scheduleNextBundleCheck(); - } - - // instance variable initialization for bundle tracking - this.bundleStartTime = new AtomicLong(Long.MAX_VALUE); - this.currentActiveBundleDoneFutureReference = new AtomicReference<>(); - this.currentBundleElementCount = new AtomicLong(0L); - this.isBundleStarted = new AtomicBoolean(false); - this.pendingBundleCount = new AtomicLong(0L); - this.watermarkFuture = CompletableFuture.completedFuture(null); - } - - /* - * Schedule in processing time to check whether the current bundle should be closed. Note that - * we only approximately achieve max bundle time by checking as frequent as half of the max bundle - * time set by users. This would violate the max bundle time by up to half of it but should - * acceptable in most cases (and cheaper than scheduling a timer at the beginning of every bundle). - */ - private void scheduleNextBundleCheck() { - final Instant nextBundleCheckTime = - Instant.now().plus(Duration.millis(maxBundleTimeMs / 2 + MIN_BUNDLE_CHECK_TIME_MS)); - final TimerInternals.TimerData timerData = - TimerInternals.TimerData.of( - this.bundleCheckTimerId, - StateNamespaces.global(), - nextBundleCheckTime, - nextBundleCheckTime, - TimeDomain.PROCESSING_TIME, - CausedByDrain.NORMAL); - bundleTimerScheduler.schedule( - new KeyedTimerData<>(new byte[0], null, timerData), nextBundleCheckTime.getMillis()); - } - - @Override - public void tryStartBundle() { - futureCollector.prepare(); - - if (isBundleStarted.compareAndSet(false, true)) { - LOG.debug("Starting a new bundle."); - // make sure the previous bundle is sealed and futures are cleared - Preconditions.checkArgument( - currentActiveBundleDoneFutureReference.get() == null, - "Current active bundle done future should be null before starting a new bundle."); - bundleStartTime.set(System.currentTimeMillis()); - pendingBundleCount.incrementAndGet(); - bundleProgressListener.onBundleStarted(); - } - - currentBundleElementCount.incrementAndGet(); - } - - @Override - public void processWatermark(Instant watermark, OpEmitter emitter) { - // propagate watermark immediately if no bundle is in progress and all the previous bundles have - // completed. - if (!isBundleStarted() && pendingBundleCount.get() == 0) { - LOG.debug("Propagating watermark: {} directly since no bundle in progress.", watermark); - bundleProgressListener.onWatermark(watermark, emitter); - return; - } - - // hold back the watermark since there is either a bundle in progress or previously closed - // bundles are unfinished. - this.bundleWatermarkHold = watermark; - - // for batch mode, the max watermark should force the bundle to close - if (BoundedWindow.TIMESTAMP_MAX_VALUE.equals(watermark)) { - /* - * Due to lack of async watermark function, we block on the previous watermark futures before propagating the watermark - * downstream. If a bundle is in progress tryFinishBundle() fill force the bundle to close and emit watermark. - * If no bundle in progress, we progress watermark explicitly after the completion of previous watermark futures. - */ - if (isBundleStarted()) { - LOG.info( - "Received max watermark. Triggering finish bundle before flushing the watermark downstream."); - tryFinishBundle(emitter); - watermarkFuture.toCompletableFuture().join(); - } else { - LOG.info( - "Received max watermark. Waiting for previous bundles to complete before flushing the watermark downstream."); - watermarkFuture.toCompletableFuture().join(); - bundleProgressListener.onWatermark(watermark, emitter); - } - } - } - - @Override - public void processTimer(KeyedTimerData keyedTimerData, OpEmitter emitter) { - // this is internal timer in processing time to check whether a bundle should be closed - if (bundleCheckTimerId.equals(keyedTimerData.getTimerData().getTimerId())) { - tryFinishBundle(emitter); - scheduleNextBundleCheck(); - } - } - - /** - * Signal the bundle manager to handle failure. We discard the output collected as part of - * processing the current element and reset the bundle count. - * - * @param t failure cause - */ - @Override - public void signalFailure(Throwable t) { - LOG.error("Encountered error during processing the message. Discarding the output due to: ", t); - futureCollector.discard(); - // reset the bundle start flag only if the bundle has started - isBundleStarted.compareAndSet(true, false); - - // bundle start may not necessarily mean we have actually started the bundle since some of the - // invariant check conditions within bundle start could throw exceptions. so rely on bundle - // start time - if (bundleStartTime.get() != Long.MAX_VALUE) { - currentBundleElementCount.set(0L); - bundleStartTime.set(Long.MAX_VALUE); - pendingBundleCount.decrementAndGet(); - currentActiveBundleDoneFutureReference.set(null); - } - } - - @Override - public void tryFinishBundle(OpEmitter emitter) { - - // we need to seal the output for each element within a bundle irrespective of the whether we - // decide to finish the - // bundle or not - CompletionStage>> outputFuture = futureCollector.finish(); - - if (shouldFinishBundle() && isBundleStarted.compareAndSet(true, false)) { - LOG.debug("Finishing the current bundle."); - - // reset the bundle count - // seal the bundle and emit the result future (collection of results) - // chain the finish bundle invocation on the finish bundle - currentBundleElementCount.set(0L); - bundleStartTime.set(Long.MAX_VALUE); - Instant watermarkHold = bundleWatermarkHold; - bundleWatermarkHold = null; - - CompletionStage currentActiveBundleDoneFuture = - currentActiveBundleDoneFutureReference.get(); - outputFuture = - outputFuture.thenCombine( - currentActiveBundleDoneFuture != null - ? currentActiveBundleDoneFuture - : CompletableFuture.completedFuture(null), - (res, ignored) -> { - bundleProgressListener.onBundleFinished(emitter); - return res; - }); - - BiConsumer>, Void> watermarkPropagationFn; - if (watermarkHold == null) { - watermarkPropagationFn = (ignored, res) -> pendingBundleCount.decrementAndGet(); - } else { - watermarkPropagationFn = - (ignored, res) -> { - LOG.debug("Propagating watermark: {} to downstream.", watermarkHold); - bundleProgressListener.onWatermark(watermarkHold, emitter); - pendingBundleCount.decrementAndGet(); - }; - } - - // We chain the current watermark emission with previous watermark and the output futures - // since bundles can finish out of order but we still want the watermark to be emitted in - // order. - watermarkFuture = outputFuture.thenAcceptBoth(watermarkFuture, watermarkPropagationFn); - currentActiveBundleDoneFutureReference.set(null); - } else if (isBundleStarted.get()) { - final CompletableFuture>> finalOutputFuture = - outputFuture.toCompletableFuture(); - currentActiveBundleDoneFutureReference.updateAndGet( - maybePrevFuture -> { - CompletableFuture prevFuture = - maybePrevFuture != null ? maybePrevFuture : CompletableFuture.completedFuture(null); - - return CompletableFuture.allOf(prevFuture, finalOutputFuture); - }); - } - - // emit the future to the propagate it to rest of the DAG - emitter.emitFuture(outputFuture); - } - - @VisibleForTesting - long getCurrentBundleElementCount() { - return currentBundleElementCount.longValue(); - } - - @VisibleForTesting - @Nullable - CompletionStage getCurrentBundleDoneFuture() { - return currentActiveBundleDoneFutureReference.get(); - } - - @VisibleForTesting - void setCurrentBundleDoneFuture(CompletableFuture currentBundleResultFuture) { - this.currentActiveBundleDoneFutureReference.set(currentBundleResultFuture); - } - - @VisibleForTesting - long getPendingBundleCount() { - return pendingBundleCount.longValue(); - } - - @VisibleForTesting - void setPendingBundleCount(long value) { - pendingBundleCount.set(value); - } - - @VisibleForTesting - boolean isBundleStarted() { - return isBundleStarted.get(); - } - - @VisibleForTesting - void setBundleWatermarkHold(Instant watermark) { - this.bundleWatermarkHold = watermark; - } - - /** - * We close the current bundle in progress if one of the following criteria is met 1. The bundle - * count ≥ maxBundleSize 2. Time elapsed since the bundle started is ≥ maxBundleTimeMs 3. - * Watermark hold equals to TIMESTAMP_MAX_VALUE which usually is the case for bounded jobs - * - * @return true - if one of the criteria above is satisfied; false - otherwise - */ - private boolean shouldFinishBundle() { - return isBundleStarted.get() - && (currentBundleElementCount.get() >= maxBundleSize - || System.currentTimeMillis() - bundleStartTime.get() >= maxBundleTimeMs - || BoundedWindow.TIMESTAMP_MAX_VALUE.equals(bundleWatermarkHold)); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnOp.java deleted file mode 100644 index bc87e2460ec4..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnOp.java +++ /dev/null @@ -1,579 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.concurrent.CompletionStage; -import java.util.function.Function; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.core.PushbackSideInputDoFnRunner; -import org.apache.beam.runners.core.SideInputHandler; -import org.apache.beam.runners.core.SimplePushbackSideInputDoFnRunner; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.fnexecution.control.ExecutableStageContext; -import org.apache.beam.runners.fnexecution.control.StageBundleFactory; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.samza.SamzaExecutionContext; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.util.DoFnUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.join.RawUnionValue; -import org.apache.beam.sdk.transforms.reflect.DoFnInvoker; -import org.apache.beam.sdk.transforms.reflect.DoFnInvokers; -import org.apache.beam.sdk.transforms.reflect.DoFnSignature; -import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.WindowedValueMultiReceiver; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Samza operator for {@link DoFn}. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class DoFnOp implements Op { - private static final Logger LOG = LoggerFactory.getLogger(DoFnOp.class); - - private final TupleTag mainOutputTag; - private final DoFn doFn; - private final Coder keyCoder; - private final Collection> sideInputs; - private final List> sideOutputTags; - private final WindowingStrategy windowingStrategy; - private final OutputManagerFactory outputManagerFactory; - // NOTE: we use HashMap here to guarantee Serializability - // Mapping from view id to a view - private final HashMap> idToViewMap; - private final String transformFullName; - private final String transformId; - private final Coder inputCoder; - private final Coder> windowedValueCoder; - private final HashMap, Coder> outputCoders; - private final PCollection.IsBounded isBounded; - private final String bundleCheckTimerId; - private final String bundleStateId; - - // portable api related - private final boolean isPortable; - private final RunnerApi.ExecutableStagePayload stagePayload; - private final JobInfo jobInfo; - private final HashMap> idToTupleTagMap; - - private transient SamzaTimerInternalsFactory timerInternalsFactory; - private transient DoFnRunner fnRunner; - private transient PushbackSideInputDoFnRunner pushbackFnRunner; - private transient SideInputHandler sideInputHandler; - private transient DoFnInvoker doFnInvoker; - private transient SamzaPipelineOptions samzaPipelineOptions; - - // This is derivable from pushbackValues which is persisted to a store. - // TODO: eagerly initialize the hold in init - @edu.umd.cs.findbugs.annotations.SuppressWarnings( - justification = "No bug", - value = "SE_TRANSIENT_FIELD_NOT_RESTORED") - private transient Instant pushbackWatermarkHold; - - // TODO: add this to checkpointable state - private transient Instant inputWatermark; - private transient BundleManager bundleManager; - private transient Instant sideInputWatermark; - private transient List> pushbackValues; - private transient ExecutableStageContext stageContext; - private transient StageBundleFactory stageBundleFactory; - private transient boolean bundleDisabled; - - private final DoFnSchemaInformation doFnSchemaInformation; - private final Map> sideInputMapping; - private final Map stateIdToStoreMapping; - - public DoFnOp( - TupleTag mainOutputTag, - DoFn doFn, - Coder keyCoder, - Coder inputCoder, - Coder> windowedValueCoder, - Map, Coder> outputCoders, - Collection> sideInputs, - List> sideOutputTags, - WindowingStrategy windowingStrategy, - Map> idToViewMap, - OutputManagerFactory outputManagerFactory, - String transformFullName, - String transformId, - PCollection.IsBounded isBounded, - boolean isPortable, - RunnerApi.ExecutableStagePayload stagePayload, - JobInfo jobInfo, - Map> idToTupleTagMap, - DoFnSchemaInformation doFnSchemaInformation, - Map> sideInputMapping, - Map stateIdToStoreMapping) { - this.mainOutputTag = mainOutputTag; - this.doFn = doFn; - this.sideInputs = sideInputs; - this.sideOutputTags = sideOutputTags; - this.inputCoder = inputCoder; - this.windowedValueCoder = windowedValueCoder; - this.outputCoders = new HashMap<>(outputCoders); - this.windowingStrategy = windowingStrategy; - this.idToViewMap = new HashMap<>(idToViewMap); - this.outputManagerFactory = outputManagerFactory; - this.transformFullName = transformFullName; - this.transformId = transformId; - this.keyCoder = keyCoder; - this.isBounded = isBounded; - this.isPortable = isPortable; - this.stagePayload = stagePayload; - this.jobInfo = jobInfo; - this.idToTupleTagMap = new HashMap<>(idToTupleTagMap); - this.bundleCheckTimerId = "_samza_bundle_check_" + transformId; - this.bundleStateId = "_samza_bundle_" + transformId; - this.doFnSchemaInformation = doFnSchemaInformation; - this.sideInputMapping = sideInputMapping; - this.stateIdToStoreMapping = stateIdToStoreMapping; - } - - @Override - @SuppressWarnings("unchecked") - public void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter emitter) { - this.inputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - this.sideInputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - this.pushbackWatermarkHold = BoundedWindow.TIMESTAMP_MAX_VALUE; - - final DoFnSignature signature = DoFnSignatures.getSignature(doFn.getClass()); - final SamzaExecutionContext samzaExecutionContext = - (SamzaExecutionContext) context.getApplicationContainerContext(); - this.samzaPipelineOptions = samzaExecutionContext.getPipelineOptions(); - this.bundleDisabled = samzaPipelineOptions.getMaxBundleSize() <= 1; - - final String stateId = "pardo-" + transformId; - final SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory = - SamzaStoreStateInternals.createNonKeyedStateInternalsFactory( - stateId, context.getTaskContext(), samzaPipelineOptions); - final FutureCollector outputFutureCollector = createFutureCollector(); - - this.bundleManager = - isPortable - ? new PortableBundleManager<>( - createBundleProgressListener(), - samzaPipelineOptions.getMaxBundleSize(), - samzaPipelineOptions.getMaxBundleTimeMs(), - timerRegistry, - bundleCheckTimerId) - : new ClassicBundleManager<>( - createBundleProgressListener(), - outputFutureCollector, - samzaPipelineOptions.getMaxBundleSize(), - samzaPipelineOptions.getMaxBundleTimeMs(), - timerRegistry, - bundleCheckTimerId); - - this.timerInternalsFactory = - SamzaTimerInternalsFactory.createTimerInternalFactory( - keyCoder, - (Scheduler) timerRegistry, - getTimerStateId(signature), - nonKeyedStateInternalsFactory, - windowingStrategy, - isBounded, - samzaPipelineOptions); - - this.sideInputHandler = - new SideInputHandler(sideInputs, nonKeyedStateInternalsFactory.stateInternalsForKey(null)); - - if (isPortable) { - final ExecutableStage executableStage = ExecutableStage.fromPayload(stagePayload); - stageContext = SamzaExecutableStageContextFactory.getInstance().get(jobInfo); - stageBundleFactory = stageContext.getStageBundleFactory(executableStage); - this.fnRunner = - SamzaDoFnRunners.createPortable( - transformId, - DoFnUtils.toStepName(executableStage), - bundleStateId, - windowedValueCoder, - executableStage, - sideInputMapping, - sideInputHandler, - nonKeyedStateInternalsFactory, - timerInternalsFactory, - samzaPipelineOptions, - outputManagerFactory.create(emitter, outputFutureCollector), - stageBundleFactory, - samzaExecutionContext, - mainOutputTag, - idToTupleTagMap, - context, - transformFullName); - } else { - this.fnRunner = - SamzaDoFnRunners.create( - samzaPipelineOptions, - doFn, - windowingStrategy, - transformFullName, - stateId, - context, - mainOutputTag, - sideInputHandler, - timerInternalsFactory, - keyCoder, - outputManagerFactory.create(emitter, outputFutureCollector), - inputCoder, - sideOutputTags, - outputCoders, - doFnSchemaInformation, - (Map>) sideInputMapping, - stateIdToStoreMapping, - emitter, - outputFutureCollector); - } - - this.pushbackFnRunner = - SimplePushbackSideInputDoFnRunner.create(fnRunner, sideInputs, sideInputHandler); - this.pushbackValues = new ArrayList<>(); - - final Iterator invokerReg = - ServiceLoader.load(SamzaDoFnInvokerRegistrar.class).iterator(); - if (!invokerReg.hasNext()) { - // use the default invoker here - doFnInvoker = DoFnInvokers.tryInvokeSetupFor(doFn, samzaPipelineOptions); - } else { - doFnInvoker = - Iterators.getOnlyElement(invokerReg).invokerSetupFor(doFn, samzaPipelineOptions, context); - } - } - - FutureCollector createFutureCollector() { - return new FutureCollectorImpl<>(); - } - - private String getTimerStateId(DoFnSignature signature) { - final StringBuilder builder = new StringBuilder("timer"); - if (signature.usesTimers()) { - signature.timerDeclarations().keySet().forEach(builder::append); - } - return builder.toString(); - } - - @Override - public void processElement(WindowedValue inputElement, OpEmitter emitter) { - try { - bundleManager.tryStartBundle(); - final Iterable> rejectedValues = - pushbackFnRunner.processElementInReadyWindows(inputElement); - for (WindowedValue rejectedValue : rejectedValues) { - if (rejectedValue.getTimestamp().compareTo(pushbackWatermarkHold) < 0) { - pushbackWatermarkHold = rejectedValue.getTimestamp(); - } - pushbackValues.add(rejectedValue); - } - - bundleManager.tryFinishBundle(emitter); - } catch (Throwable t) { - LOG.error("Encountered error during process element", t); - bundleManager.signalFailure(t); - throw t; - } - } - - private void doProcessWatermark(Instant watermark, OpEmitter emitter) { - this.inputWatermark = watermark; - - if (sideInputWatermark.isEqual(BoundedWindow.TIMESTAMP_MAX_VALUE)) { - // this means we will never see any more side input - emitAllPushbackValues(); - } - - final Instant actualInputWatermark = - pushbackWatermarkHold.isBefore(inputWatermark) ? pushbackWatermarkHold : inputWatermark; - - timerInternalsFactory.setInputWatermark(actualInputWatermark); - - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - if (!readyTimers.isEmpty()) { - pushbackFnRunner.startBundle(); - for (KeyedTimerData keyedTimerData : readyTimers) { - fireTimer(keyedTimerData); - } - pushbackFnRunner.finishBundle(); - } - - if (timerInternalsFactory.getOutputWatermark() == null - || timerInternalsFactory.getOutputWatermark().isBefore(actualInputWatermark)) { - timerInternalsFactory.setOutputWatermark(actualInputWatermark); - emitter.emitWatermark(timerInternalsFactory.getOutputWatermark()); - } - } - - @Override - public void processWatermark(Instant watermark, OpEmitter emitter) { - bundleManager.processWatermark(watermark, emitter); - } - - @Override - public void processSideInput( - String id, WindowedValue> elements, OpEmitter emitter) { - checkState( - bundleDisabled, "Side input not supported in bundling mode. Please disable bundling."); - @SuppressWarnings("unchecked") - final WindowedValue> retypedElements = (WindowedValue>) elements; - - final PCollectionView view = idToViewMap.get(id); - if (view == null) { - throw new IllegalArgumentException("No mapping of id " + id + " to view."); - } - - sideInputHandler.addSideInputValue(view, retypedElements); - - final List> previousPushbackValues = new ArrayList<>(pushbackValues); - pushbackWatermarkHold = BoundedWindow.TIMESTAMP_MAX_VALUE; - pushbackValues.clear(); - - for (final WindowedValue value : previousPushbackValues) { - processElement(value, emitter); - } - - // We may be able to advance the output watermark since we may have played some pushed back - // events. - processWatermark(this.inputWatermark, emitter); - } - - @Override - public void processSideInputWatermark(Instant watermark, OpEmitter emitter) { - checkState( - bundleDisabled, "Side input not supported in bundling mode. Please disable bundling."); - sideInputWatermark = watermark; - - if (sideInputWatermark.isEqual(BoundedWindow.TIMESTAMP_MAX_VALUE)) { - // this means we will never see any more side input - processWatermark(this.inputWatermark, emitter); - } - } - - @Override - @SuppressWarnings("unchecked") - public void processTimer(KeyedTimerData keyedTimerData, OpEmitter emitter) { - // this is internal timer in processing time to check whether a bundle should be closed - if (bundleCheckTimerId.equals(keyedTimerData.getTimerData().getTimerId())) { - bundleManager.processTimer(keyedTimerData, emitter); - return; - } - - pushbackFnRunner.startBundle(); - fireTimer(keyedTimerData); - pushbackFnRunner.finishBundle(); - - this.timerInternalsFactory.removeProcessingTimer((KeyedTimerData) keyedTimerData); - } - - @Override - public void close() { - doFnInvoker.invokeTeardown(); - try (AutoCloseable factory = stageBundleFactory; - AutoCloseable context = stageContext) { - // do nothing - } catch (Exception e) { - LOG.error("Failed to close stage bundle factory", e); - } - } - - private void fireTimer(KeyedTimerData keyedTimerData) { - final TimerInternals.TimerData timer = keyedTimerData.getTimerData(); - LOG.debug("Firing timer {}", timer); - - final StateNamespace namespace = timer.getNamespace(); - // NOTE: not sure why this is safe, but DoFnOperator makes this assumption - final BoundedWindow window = ((StateNamespaces.WindowNamespace) namespace).getWindow(); - - fnRunner.onTimer( - timer.getTimerId(), - timer.getTimerFamilyId(), - keyedTimerData.getKey(), - window, - timer.getTimestamp(), - timer.getOutputTimestamp(), - timer.getDomain(), - timer.causedByDrain()); - } - - // todo: should this go through bundle manager to start and finish the bundle? - private void emitAllPushbackValues() { - if (!pushbackValues.isEmpty()) { - pushbackFnRunner.startBundle(); - - final List> previousPushbackValues = new ArrayList<>(pushbackValues); - pushbackWatermarkHold = BoundedWindow.TIMESTAMP_MAX_VALUE; - pushbackValues.clear(); - - for (final WindowedValue value : previousPushbackValues) { - fnRunner.processElement(value); - } - - pushbackFnRunner.finishBundle(); - } - } - - private BundleManager.BundleProgressListener createBundleProgressListener() { - return new BundleManager.BundleProgressListener() { - @Override - public void onBundleStarted() { - pushbackFnRunner.startBundle(); - } - - @Override - public void onBundleFinished(OpEmitter emitter) { - pushbackFnRunner.finishBundle(); - } - - @Override - public void onWatermark(Instant watermark, OpEmitter emitter) { - doProcessWatermark(watermark, emitter); - } - }; - } - - static CompletionStage> createOutputFuture( - WindowedValue windowedValue, - CompletionStage valueFuture, - Function valueMapper) { - return valueFuture.thenApply( - res -> - WindowedValues.of( - valueMapper.apply(res), - windowedValue.getTimestamp(), - windowedValue.getWindows(), - windowedValue.getPaneInfo())); - } - - /** - * Factory class to create an {@link org.apache.beam.sdk.util.WindowedValueMultiReceiver} that - * emits values to the main output only, which is a single {@link - * org.apache.beam.sdk.values.PCollection}. - * - * @param type of the output element. - */ - public static class SingleOutputManagerFactory implements OutputManagerFactory { - @Override - public WindowedValueMultiReceiver create(OpEmitter emitter) { - return createOutputManager(emitter, null); - } - - @Override - public WindowedValueMultiReceiver create( - OpEmitter emitter, FutureCollector collector) { - return createOutputManager(emitter, collector); - } - - private WindowedValueMultiReceiver createOutputManager( - OpEmitter emitter, FutureCollector collector) { - return new WindowedValueMultiReceiver() { - @Override - @SuppressWarnings("unchecked") - public void output(TupleTag tupleTag, WindowedValue windowedValue) { - // With only one input we know that T is of type OutT. - if (windowedValue.getValue() instanceof CompletionStage) { - CompletionStage valueFuture = (CompletionStage) windowedValue.getValue(); - if (collector != null) { - collector.add(createOutputFuture(windowedValue, valueFuture, value -> (OutT) value)); - } - } else { - final WindowedValue retypedWindowedValue = (WindowedValue) windowedValue; - emitter.emitElement(retypedWindowedValue); - } - } - }; - } - } - - /** - * Factory class to create an {@link org.apache.beam.runners.core.WindowedValueMultiReceiver} that - * emits values to the main output as well as the side outputs via union type {@link - * RawUnionValue}. - */ - public static class MultiOutputManagerFactory implements OutputManagerFactory { - private final Map, Integer> tagToIndexMap; - - public MultiOutputManagerFactory(Map, Integer> tagToIndexMap) { - this.tagToIndexMap = tagToIndexMap; - } - - @Override - public WindowedValueMultiReceiver create(OpEmitter emitter) { - return createOutputManager(emitter, null); - } - - @Override - public WindowedValueMultiReceiver create( - OpEmitter emitter, FutureCollector collector) { - return createOutputManager(emitter, collector); - } - - private WindowedValueMultiReceiver createOutputManager( - OpEmitter emitter, FutureCollector collector) { - return new WindowedValueMultiReceiver() { - @Override - @SuppressWarnings("unchecked") - public void output(TupleTag tupleTag, WindowedValue windowedValue) { - final int index = tagToIndexMap.get(tupleTag); - final T rawValue = windowedValue.getValue(); - if (rawValue instanceof CompletionStage) { - CompletionStage valueFuture = (CompletionStage) rawValue; - if (collector != null) { - collector.add( - createOutputFuture( - windowedValue, valueFuture, res -> new RawUnionValue(index, res))); - } - } else { - final RawUnionValue rawUnionValue = new RawUnionValue(index, rawValue); - emitter.emitElement(windowedValue.withValue(rawUnionValue)); - } - } - }; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnRunnerWithKeyedInternals.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnRunnerWithKeyedInternals.java deleted file mode 100644 index 84cf5b26c505..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/DoFnRunnerWithKeyedInternals.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.core.KeyedWorkItem; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.WindowedValue; -import org.joda.time.Instant; - -/** This class wraps a DoFnRunner with keyed StateInternals and TimerInternals access. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class DoFnRunnerWithKeyedInternals implements DoFnRunner { - private final DoFnRunner underlying; - private final KeyedInternals keyedInternals; - - DoFnRunnerWithKeyedInternals( - DoFnRunner doFnRunner, KeyedInternals keyedInternals) { - this.underlying = doFnRunner; - this.keyedInternals = keyedInternals; - } - - @Override - public void startBundle() { - underlying.startBundle(); - } - - @Override - public void processElement(WindowedValue elem) { - // NOTE: this is thread-safe if we only allow concurrency on the per-key basis. - setKeyedInternals(elem.getValue()); - - try { - underlying.processElement(elem); - } finally { - clearKeyedInternals(); - } - } - - @Override - public void onTimer( - String timerId, - String timerFamilyId, - KeyT key, - BoundedWindow window, - Instant timestamp, - Instant outputTimestamp, - TimeDomain timeDomain, - CausedByDrain causedByDrain) { - // Note: wrap with KV.of(key, null) as a special use case of setKeyedInternals() to set key - // directly. - setKeyedInternals(KV.of(key, null)); - - try { - underlying.onTimer( - timerId, - timerFamilyId, - key, - window, - timestamp, - outputTimestamp, - timeDomain, - causedByDrain); - } finally { - clearKeyedInternals(); - } - } - - @Override - public void finishBundle() { - underlying.finishBundle(); - } - - @Override - public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { - underlying.onWindowExpiration(window, timestamp, key); - } - - @Override - public DoFn getFn() { - return underlying.getFn(); - } - - private void setKeyedInternals(Object value) { - if (value instanceof KeyedWorkItem) { - keyedInternals.setKey(((KeyedWorkItem) value).key()); - } else if (value instanceof KeyedTimerData) { - final Object key = ((KeyedTimerData) value).getKey(); - if (key != null) { - keyedInternals.setKey(key); - } - } else if (value instanceof KV) { - keyedInternals.setKey(((KV) value).getKey()); - } else { - throw new UnsupportedOperationException( - String.format( - "%s is not supported in %s", value.getClass(), DoFnRunnerWithKeyedInternals.class)); - } - } - - private void clearKeyedInternals() { - keyedInternals.clearKey(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollector.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollector.java deleted file mode 100644 index 750e42d96e26..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollector.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.concurrent.CompletionStage; -import org.apache.beam.sdk.values.WindowedValue; - -/** - * A future collector that buffers the output from the users {@link - * org.apache.beam.sdk.transforms.DoFn} and propagates the result future to downstream operators - * only after {@link #finish()} is invoked. - * - * @param type of the output element - */ -public interface FutureCollector { - /** - * Outputs the element to the collector. - * - * @param element to add to the collector - */ - void add(CompletionStage> element); - - /** - * Outputs a collection of elements to the collector. - * - * @param elements to add to the collector - */ - void addAll(CompletionStage>> elements); - - /** - * Discards the elements within the collector. Once the elements have been discarded, callers need - * to prepare the collector again before invoking {@link #add(CompletionStage)}. - */ - void discard(); - - /** - * Seals this {@link FutureCollector}, returning a {@link CompletionStage} containing all of the - * elements that were added to it. The {@link #add(CompletionStage)} method will throw an {@link - * IllegalStateException} if called after a call to finish. - * - *

The {@link FutureCollector} needs to be started again to collect newer batch of output. - */ - CompletionStage>> finish(); - - /** - * Prepares the {@link FutureCollector} to accept output elements. The {@link - * #add(CompletionStage)} method will throw an {@link IllegalStateException} if called without - * preparing the collector. - */ - void prepare(); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollectorImpl.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollectorImpl.java deleted file mode 100644 index e364eb7c4078..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/FutureCollectorImpl.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.beam.runners.samza.util.FutureUtils; -import org.apache.beam.sdk.values.WindowedValue; - -class FutureCollectorImpl implements FutureCollector { - private final AtomicBoolean collectorSealed; - private CompletionStage>> outputFuture; - - FutureCollectorImpl() { - outputFuture = CompletableFuture.completedFuture(new ArrayList<>()); - collectorSealed = new AtomicBoolean(true); - } - - @Override - public void add(CompletionStage> element) { - checkState( - !collectorSealed.get(), - "Cannot add element to an unprepared collector. Make sure prepare() is invoked before adding elements."); - - // We need synchronize guard against scenarios when watermark/finish bundle trigger outputs. - synchronized (this) { - outputFuture = - outputFuture.thenCombine( - element, - (collection, event) -> { - collection.add(event); - return collection; - }); - } - } - - @Override - public void addAll(CompletionStage>> elements) { - checkState( - !collectorSealed.get(), - "Cannot add elements to an unprepared collector. Make sure prepare() is invoked before adding elements."); - - synchronized (this) { - outputFuture = FutureUtils.combineFutures(outputFuture, elements); - } - } - - @Override - public void discard() { - collectorSealed.compareAndSet(false, true); - - synchronized (this) { - outputFuture = CompletableFuture.completedFuture(new ArrayList<>()); - } - } - - @Override - public CompletionStage>> finish() { - /* - * We can ignore the results here because its okay to call finish without invoking prepare. It will be a no-op - * and an empty collection will be returned. - */ - collectorSealed.compareAndSet(false, true); - - synchronized (this) { - final CompletionStage>> sealedOutputFuture = outputFuture; - outputFuture = CompletableFuture.completedFuture(new ArrayList<>()); - return sealedOutputFuture; - } - } - - @Override - public void prepare() { - boolean isCollectorSealed = collectorSealed.compareAndSet(true, false); - checkState( - isCollectorSealed, - "Failed to prepare the collector. Collector needs to be sealed before prepare() is invoked."); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/GroupByKeyOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/GroupByKeyOp.java deleted file mode 100644 index a9fe11de4d92..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/GroupByKeyOp.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.Collections; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.core.DoFnRunners; -import org.apache.beam.runners.core.GroupAlsoByWindowViaWindowSetNewDoFn; -import org.apache.beam.runners.core.KeyedWorkItem; -import org.apache.beam.runners.core.KeyedWorkItemCoder; -import org.apache.beam.runners.core.KeyedWorkItems; -import org.apache.beam.runners.core.NullSideInputReader; -import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateInternalsFactory; -import org.apache.beam.runners.core.StepContext; -import org.apache.beam.runners.core.SystemReduceFn; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.core.TimerInternals.TimerData; -import org.apache.beam.runners.samza.SamzaExecutionContext; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.metrics.DoFnRunnerWithMetrics; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.WindowedValueMultiReceiver; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection.IsBounded; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Samza operator for {@link org.apache.beam.sdk.transforms.GroupByKey}. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class GroupByKeyOp - implements Op, KV, K> { - private static final Logger LOG = LoggerFactory.getLogger(GroupByKeyOp.class); - private static final String TIMER_STATE_ID = "timer"; - - private final TupleTag> mainOutputTag; - private final KeyedWorkItemCoder inputCoder; - private final WindowingStrategy windowingStrategy; - private final OutputManagerFactory> outputManagerFactory; - private final Coder keyCoder; - private final SystemReduceFn reduceFn; - private final String transformFullName; - private final String transformId; - private final IsBounded isBounded; - - private transient StateInternalsFactory stateInternalsFactory; - private transient SamzaTimerInternalsFactory timerInternalsFactory; - private transient DoFnRunner, KV> fnRunner; - private transient SamzaPipelineOptions pipelineOptions; - - public GroupByKeyOp( - TupleTag> mainOutputTag, - Coder> inputCoder, - SystemReduceFn reduceFn, - WindowingStrategy windowingStrategy, - OutputManagerFactory> outputManagerFactory, - String transformFullName, - String transformId, - IsBounded isBounded) { - this.mainOutputTag = mainOutputTag; - this.windowingStrategy = windowingStrategy; - this.outputManagerFactory = outputManagerFactory; - this.transformFullName = transformFullName; - this.transformId = transformId; - this.isBounded = isBounded; - - if (!(inputCoder instanceof KeyedWorkItemCoder)) { - throw new IllegalArgumentException( - String.format( - "GroupByKeyOp requires input to use KeyedWorkItemCoder. Got: %s", - inputCoder.getClass())); - } - this.inputCoder = (KeyedWorkItemCoder) inputCoder; - this.keyCoder = this.inputCoder.getKeyCoder(); - this.reduceFn = reduceFn; - } - - @Override - public void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter> emitter) { - - final SamzaExecutionContext samzaExecutionContext = - (SamzaExecutionContext) context.getApplicationContainerContext(); - this.pipelineOptions = samzaExecutionContext.getPipelineOptions(); - - final SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory = - SamzaStoreStateInternals.createNonKeyedStateInternalsFactory( - transformId, context.getTaskContext(), pipelineOptions); - - final WindowedValueMultiReceiver outputManager = outputManagerFactory.create(emitter); - - this.stateInternalsFactory = - new SamzaStoreStateInternals.Factory<>( - transformId, - Collections.singletonMap( - SamzaStoreStateInternals.BEAM_STORE, - SamzaStoreStateInternals.getBeamStore(context.getTaskContext())), - keyCoder, - pipelineOptions.getStoreBatchGetSize()); - - this.timerInternalsFactory = - SamzaTimerInternalsFactory.createTimerInternalFactory( - keyCoder, - timerRegistry, - TIMER_STATE_ID, - nonKeyedStateInternalsFactory, - windowingStrategy, - isBounded, - pipelineOptions); - - final DoFn, KV> doFn = - GroupAlsoByWindowViaWindowSetNewDoFn.create( - windowingStrategy, - stateInternalsFactory, - timerInternalsFactory, - NullSideInputReader.of(Collections.emptyList()), - reduceFn, - outputManager, - mainOutputTag); - - final KeyedInternals keyedInternals = - new KeyedInternals<>(stateInternalsFactory, timerInternalsFactory); - - final StepContext stepContext = - new StepContext() { - @Override - public StateInternals stateInternals() { - return keyedInternals.stateInternals(); - } - - @Override - public TimerInternals timerInternals() { - return keyedInternals.timerInternals(); - } - }; - - final DoFnRunner, KV> doFnRunner = - DoFnRunners.simpleRunner( - PipelineOptionsFactory.create(), - doFn, - NullSideInputReader.of(Collections.emptyList()), - outputManager, - mainOutputTag, - Collections.emptyList(), - stepContext, - null, - Collections.emptyMap(), - windowingStrategy, - DoFnSchemaInformation.create(), - Collections.emptyMap()); - - final DoFnRunner, KV> dropLateDataRunner = - pipelineOptions.getDropLateData() - ? DoFnRunners.lateDataDroppingRunner( - doFnRunner, keyedInternals.timerInternals(), windowingStrategy) - : doFnRunner; - - final SamzaExecutionContext executionContext = - (SamzaExecutionContext) context.getApplicationContainerContext(); - final DoFnRunner, KV> doFnRunnerWithMetrics = - DoFnRunnerWithMetrics.wrap( - dropLateDataRunner, executionContext.getMetricsContainer(), transformFullName); - - this.fnRunner = new DoFnRunnerWithKeyedInternals<>(doFnRunnerWithMetrics, keyedInternals); - } - - @Override - public void processElement( - WindowedValue> inputElement, OpEmitter> emitter) { - fnRunner.startBundle(); - fnRunner.processElement(inputElement); - fnRunner.finishBundle(); - } - - @Override - public void processWatermark(Instant watermark, OpEmitter> emitter) { - timerInternalsFactory.setInputWatermark(watermark); - - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - if (!readyTimers.isEmpty()) { - fnRunner.startBundle(); - for (KeyedTimerData keyedTimerData : readyTimers) { - fireTimer(keyedTimerData.getKey(), keyedTimerData.getTimerData()); - } - fnRunner.finishBundle(); - } - - if (timerInternalsFactory.getOutputWatermark() == null - || timerInternalsFactory.getOutputWatermark().isBefore(watermark)) { - timerInternalsFactory.setOutputWatermark(watermark); - emitter.emitWatermark(timerInternalsFactory.getOutputWatermark()); - } - } - - @Override - public void processTimer(KeyedTimerData keyedTimerData, OpEmitter> emitter) { - fnRunner.startBundle(); - fireTimer(keyedTimerData.getKey(), keyedTimerData.getTimerData()); - fnRunner.finishBundle(); - - timerInternalsFactory.removeProcessingTimer(keyedTimerData); - } - - private void fireTimer(K key, TimerData timer) { - LOG.debug("Firing timer {} for key {}", timer, key); - fnRunner.processElement( - WindowedValues.valueInGlobalWindow( - KeyedWorkItems.timersWorkItem(key, Collections.singletonList(timer)))); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedInternals.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedInternals.java deleted file mode 100644 index dc442d88ac32..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedInternals.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.ArrayList; -import java.util.List; -import javax.annotation.concurrent.ThreadSafe; -import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateInternalsFactory; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateTag; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.core.TimerInternalsFactory; -import org.apache.beam.runners.samza.state.SamzaMapState; -import org.apache.beam.runners.samza.state.SamzaSetState; -import org.apache.beam.sdk.state.State; -import org.apache.beam.sdk.state.StateContext; -import org.apache.beam.sdk.state.TimeDomain; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Instant; - -/** Provides access to the keyed StateInternals and TimerInternals. */ -@ThreadSafe -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -class KeyedInternals { - - private static class KeyedStates { - private final K key; - private final List states; - - private KeyedStates(K key) { - this.key = key; - this.states = new ArrayList<>(); - } - } - - private static final ThreadLocal threadLocalKeyedStates = new ThreadLocal<>(); - private final StateInternalsFactory stateFactory; - private final TimerInternalsFactory timerFactory; - - KeyedInternals(StateInternalsFactory stateFactory, TimerInternalsFactory timerFactory) { - this.stateFactory = stateFactory; - this.timerFactory = timerFactory; - } - - StateInternals stateInternals() { - return new KeyedStateInternals(); - } - - TimerInternals timerInternals() { - return new KeyedTimerInternals(); - } - - void setKey(K key) { - checkState( - threadLocalKeyedStates.get() == null, - "States for key %s is not cleared before processing", - key); - - threadLocalKeyedStates.set(new KeyedStates(key)); - } - - K getKey() { - KeyedStates keyedStates = threadLocalKeyedStates.get(); - return keyedStates == null ? null : keyedStates.key; - } - - void clearKey() { - final List states = threadLocalKeyedStates.get().states; - states.forEach( - state -> { - if (state instanceof SamzaMapState) { - ((SamzaMapState) state).closeIterators(); - } else if (state instanceof SamzaSetState) { - ((SamzaSetState) state).closeIterators(); - } - }); - states.clear(); - - threadLocalKeyedStates.remove(); - } - - private class KeyedStateInternals implements StateInternals { - - @Override - public K getKey() { - return KeyedInternals.this.getKey(); - } - - @Override - public T state( - StateNamespace namespace, StateTag address, StateContext c) { - checkState(getKey() != null, "Key is not set before state access in Stateful ParDo."); - - final T state = stateFactory.stateInternalsForKey(getKey()).state(namespace, address, c); - threadLocalKeyedStates.get().states.add(state); - return state; - } - } - - private class KeyedTimerInternals implements TimerInternals { - - private TimerInternals getInternals() { - return timerFactory.timerInternalsForKey(getKey()); - } - - @Override - public void setTimer( - StateNamespace namespace, - String timerId, - String timerFamilyId, - Instant target, - Instant outputTimestamp, - TimeDomain timeDomain) { - getInternals() - .setTimer(namespace, timerId, timerFamilyId, target, outputTimestamp, timeDomain); - } - - @Override - public void setTimer(TimerData timerData) { - getInternals().setTimer(timerData); - } - - @Override - public void deleteTimer( - StateNamespace namespace, String timerId, String timerFamilyId, TimeDomain timeDomain) { - getInternals().deleteTimer(namespace, timerId, timerFamilyId, timeDomain); - } - - @Override - public void deleteTimer(StateNamespace namespace, String timerId, String timerFamilyId) { - getInternals().deleteTimer(namespace, timerId, timerFamilyId); - } - - @Override - public void deleteTimer(TimerData timerKey) { - getInternals().deleteTimer(timerKey); - } - - @Override - public Instant currentProcessingTime() { - return getInternals().currentProcessingTime(); - } - - @Override - public @Nullable Instant currentSynchronizedProcessingTime() { - return getInternals().currentSynchronizedProcessingTime(); - } - - @Override - public Instant currentInputWatermarkTime() { - return getInternals().currentInputWatermarkTime(); - } - - @Override - public @Nullable Instant currentOutputWatermarkTime() { - return getInternals().currentOutputWatermarkTime(); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedTimerData.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedTimerData.java deleted file mode 100644 index bd7c62dbb323..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KeyedTimerData.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.List; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.core.TimerInternals.TimerData; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.CoderException; -import org.apache.beam.sdk.coders.InstantCoder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.StructuredCoder; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Instant; - -/** - * {@link TimerInternals.TimerData} with key, used by {@link SamzaTimerInternalsFactory}. Implements - * {@link Comparable} by first comparing the wrapped TimerData then the key. - */ -@SuppressWarnings({ - "keyfor", - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class KeyedTimerData implements Comparable> { - private final byte[] keyBytes; - private final K key; - private final TimerInternals.TimerData timerData; - - public KeyedTimerData(byte[] keyBytes, K key, TimerData timerData) { - this.keyBytes = keyBytes; - this.key = key; - this.timerData = timerData; - } - - public K getKey() { - return key; - } - - public byte[] getKeyBytes() { - return keyBytes; - } - - public TimerInternals.TimerData getTimerData() { - return timerData; - } - - @Override - public int compareTo(KeyedTimerData other) { - final int timerCompare = getTimerData().compareTo(other.getTimerData()); - if (timerCompare != 0) { - return timerCompare; - } - - if (keyBytes == null) { - return other.keyBytes == null ? 0 : -1; - } - - if (other.keyBytes == null) { - return 1; - } - - if (keyBytes.length < other.keyBytes.length) { - return -1; - } - - if (keyBytes.length > other.keyBytes.length) { - return 1; - } - - for (int i = 0; i < keyBytes.length; ++i) { - final char b1 = (char) keyBytes[i]; - final char b2 = (char) other.keyBytes[i]; - if (b1 != b2) { - return b1 - b2; - } - } - - return 0; - } - - @Override - public String toString() { - return "KeyedTimerData{" - + "key=" - + key - + ", keyBytes=" - + Arrays.toString(keyBytes) - + ", timerData=" - + timerData - + '}'; - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - - if (!(o instanceof KeyedTimerData)) { - return false; - } - - final KeyedTimerData that = (KeyedTimerData) o; - - return Arrays.equals(keyBytes, that.keyBytes) && timerData.equals(that.timerData); - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(keyBytes); - result = 31 * result + timerData.hashCode(); - return result; - } - - /** - * Coder for {@link KeyedTimerData}. Note we don't use the {@link TimerInternals.TimerDataCoderV2} - * here directly since we want to en/decode timestamp first so the timers will be sorted in the - * state. - */ - public static class KeyedTimerDataCoder extends StructuredCoder> { - private static final StringUtf8Coder STRING_CODER = StringUtf8Coder.of(); - private static final InstantCoder INSTANT_CODER = InstantCoder.of(); - - private final Coder keyCoder; - private final Coder windowCoder; - - KeyedTimerDataCoder(Coder keyCoder, Coder windowCoder) { - this.keyCoder = keyCoder; - this.windowCoder = windowCoder; - } - - @Override - public void encode(KeyedTimerData value, OutputStream outStream) - throws CoderException, IOException { - - final TimerData timer = value.getTimerData(); - // encode the timestamps first - // all new fields should be encoded at last - INSTANT_CODER.encode(timer.getTimestamp(), outStream); - STRING_CODER.encode(timer.getTimerId(), outStream); - STRING_CODER.encode(timer.getNamespace().stringKey(), outStream); - STRING_CODER.encode(timer.getDomain().name(), outStream); - - if (keyCoder != null) { - keyCoder.encode(value.key, outStream); - } - - STRING_CODER.encode(timer.getTimerFamilyId(), outStream); - INSTANT_CODER.encode(timer.getOutputTimestamp(), outStream); - } - - @Override - public KeyedTimerData decode(InputStream inStream) throws CoderException, IOException { - // decode the timestamp first - final Instant timestamp = INSTANT_CODER.decode(inStream); - final String timerId = STRING_CODER.decode(inStream); - final StateNamespace namespace = - StateNamespaces.fromString(STRING_CODER.decode(inStream), windowCoder); - final TimeDomain domain = TimeDomain.valueOf(STRING_CODER.decode(inStream)); - - byte[] keyBytes = null; - K key = null; - if (keyCoder != null) { - key = keyCoder.decode(inStream); - - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - keyCoder.encode(key, baos); - } catch (IOException e) { - throw new RuntimeException("Could not encode key: " + key, e); - } - keyBytes = baos.toByteArray(); - } - - final String timerFamilyId = inStream.available() > 0 ? STRING_CODER.decode(inStream) : ""; - final Instant outputTimestamp = - inStream.available() > 0 ? INSTANT_CODER.decode(inStream) : timestamp; - final TimerData timer = - TimerData.of(timerId, timerFamilyId, namespace, timestamp, outputTimestamp, domain); - return new KeyedTimerData<>(keyBytes, key, timer); - } - - @Override - public List> getCoderArguments() { - return Arrays.asList(keyCoder, windowCoder); - } - - @Override - public void verifyDeterministic() throws NonDeterministicException {} - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KvToKeyedWorkItemOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KvToKeyedWorkItemOp.java deleted file mode 100644 index 7403c4480768..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/KvToKeyedWorkItemOp.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import org.apache.beam.runners.core.KeyedWorkItem; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.WindowedValue; - -/** Samza operator to map input stream of {@link KV} to {@link KeyedWorkItem}. */ -public class KvToKeyedWorkItemOp implements Op, KeyedWorkItem, K> { - - @Override - public void processElement( - WindowedValue> inputElement, OpEmitter> emitter) { - final KV kv = inputElement.getValue(); - for (WindowedValue> windowedValue : inputElement.explodeWindows()) { - final KeyedWorkItem workItem = - new SingletonKeyedWorkItem<>(kv.getKey(), windowedValue.withValue(kv.getValue())); - emitter.emitElement(windowedValue.withValue(workItem)); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/Op.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/Op.java deleted file mode 100644 index 14e0151bb708..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/Op.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.Serializable; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; - -/** - * Interface of Samza operator for BEAM. This interface demultiplexes messages from BEAM so that - * elements and side inputs can be handled separately in Samza. Watermark propagation can be - * overridden so we can hold watermarks for side inputs. The output values and watermark will be - * collected via {@link OpEmitter}. - */ -public interface Op extends Serializable { - /** - * A hook that allows initialization for any non-serializable operator state, such as getting - * stores. - * - *

While an emitter is supplied to this function it is not usable except in the methods {@link - * #processElement(WindowedValue, OpEmitter)}, {@link #processWatermark(Instant, OpEmitter)}, and - * {@link #processSideInput(String, WindowedValue, OpEmitter)}. - */ - default void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter emitter) {} - - void processElement(WindowedValue inputElement, OpEmitter emitter); - - default void processWatermark(Instant watermark, OpEmitter emitter) { - emitter.emitWatermark(watermark); - } - - default void processSideInput( - String id, WindowedValue> elements, OpEmitter emitter) { - throw new UnsupportedOperationException("Side inputs not supported for: " + this.getClass()); - } - - default void processSideInputWatermark(Instant watermark, OpEmitter emitter) {} - - default void processTimer(KeyedTimerData keyedTimerData, OpEmitter emitter) {} - - default void close() {} -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java deleted file mode 100644 index f2eecbbbc9c7..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Queue; -import java.util.ServiceLoader; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -import org.apache.beam.runners.samza.SamzaPipelineExceptionContext; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.translation.TranslationContext; -import org.apache.beam.runners.samza.util.FutureUtils; -import org.apache.beam.runners.samza.util.SamzaPipelineExceptionListener; -import org.apache.beam.sdk.util.UserCodeException; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.apache.samza.operators.functions.AsyncFlatMapFunction; -import org.apache.samza.operators.functions.ScheduledFunction; -import org.apache.samza.operators.functions.WatermarkFunction; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Adaptor class that runs a Samza {@link Op} for BEAM in the Samza {@link AsyncFlatMapFunction}. - * This class is initialized once for each Op within a Task for each Task. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class OpAdapter - implements AsyncFlatMapFunction, OpMessage>, - WatermarkFunction>, - ScheduledFunction, OpMessage>, - Serializable { - private static final Logger LOG = LoggerFactory.getLogger(OpAdapter.class); - - private final Op op; - private final String transformFullName; - private final transient SamzaPipelineOptions samzaPipelineOptions; - private transient OpEmitter emitter; - private transient Config config; - private transient Context context; - private transient List exceptionListeners; - - public static AsyncFlatMapFunction, OpMessage> adapt( - Op op, TranslationContext ctx) { - return new OpAdapter<>(op, ctx.getTransformFullName(), ctx.getPipelineOptions()); - } - - private OpAdapter( - Op op, String transformFullName, SamzaPipelineOptions samzaPipelineOptions) { - this.op = op; - this.transformFullName = transformFullName; - this.samzaPipelineOptions = samzaPipelineOptions; - } - - @Override - public final void init(Context context) { - this.emitter = new OpEmitterImpl<>(); - this.config = context.getJobContext().getConfig(); - this.context = context; - this.exceptionListeners = - StreamSupport.stream( - ServiceLoader.load(SamzaPipelineExceptionListener.Registrar.class).spliterator(), - false) - .collect(Collectors.toList()); - } - - @Override - public final void schedule(Scheduler> timerRegistry) { - assert context != null; - - op.open(config, context, timerRegistry, emitter); - } - - @Override - public synchronized CompletionStage>> apply(OpMessage message) { - try { - switch (message.getType()) { - case ELEMENT: - op.processElement(message.getElement(), emitter); - break; - case SIDE_INPUT: - op.processSideInput(message.getViewId(), message.getViewElements(), emitter); - break; - case SIDE_INPUT_WATERMARK: - op.processSideInputWatermark(message.getSideInputWatermark(), emitter); - break; - default: - throw new IllegalArgumentException( - String.format("Unexpected input type: %s", message.getType())); - } - } catch (Exception e) { - LOG.error("Exception happened in transform: {}", transformFullName, e); - notifyExceptionListeners(transformFullName, e, samzaPipelineOptions); - throw UserCodeException.wrap(e); - } - - CompletionStage>> resultFuture = - CompletableFuture.completedFuture(emitter.collectOutput()); - - return FutureUtils.combineFutures(resultFuture, emitter.collectFuture()); - } - - @Override - public synchronized Collection> processWatermark(long time) { - try { - op.processWatermark(new Instant(time), emitter); - } catch (Exception e) { - LOG.error( - "Op {} threw an exception during processing watermark", this.getClass().getName(), e); - throw UserCodeException.wrap(e); - } - - return emitter.collectOutput(); - } - - @Override - public synchronized Long getOutputWatermark() { - return emitter.collectWatermark(); - } - - @Override - public synchronized Collection> onCallback( - KeyedTimerData keyedTimerData, long time) { - try { - op.processTimer(keyedTimerData, emitter); - } catch (Exception e) { - LOG.error("Op {} threw an exception during processing timer", this.getClass().getName(), e); - throw UserCodeException.wrap(e); - } - - return emitter.collectOutput(); - } - - @Override - public void close() { - op.close(); - } - - static class OpEmitterImpl implements OpEmitter { - private final Queue> outputQueue; - private CompletionStage>> outputFuture; - private Instant outputWatermark; - - OpEmitterImpl() { - outputQueue = new ConcurrentLinkedQueue<>(); - } - - @Override - public void emitElement(WindowedValue element) { - outputQueue.add(OpMessage.ofElement(element)); - } - - @Override - public void emitFuture(CompletionStage>> resultFuture) { - final CompletionStage>> resultFutureWrapped = - resultFuture.thenApply( - res -> res.stream().map(OpMessage::ofElement).collect(Collectors.toList())); - - outputFuture = FutureUtils.combineFutures(outputFuture, resultFutureWrapped); - } - - @Override - public void emitWatermark(Instant watermark) { - outputWatermark = watermark; - } - - @Override - public void emitView(String id, WindowedValue> elements) { - outputQueue.add(OpMessage.ofSideInput(id, elements)); - } - - @Override - public Collection> collectOutput() { - final List> outputList = new ArrayList<>(); - OpMessage output; - while ((output = outputQueue.poll()) != null) { - outputList.add(output); - } - return outputList; - } - - @Override - public CompletionStage>> collectFuture() { - final CompletionStage>> future = outputFuture; - outputFuture = null; - return future; - } - - @Override - public Long collectWatermark() { - final Instant watermark = outputWatermark; - outputWatermark = null; - return watermark == null ? null : watermark.getMillis(); - } - } - - private void notifyExceptionListeners( - String transformFullName, Exception e, SamzaPipelineOptions samzaPipelineOptions) { - try { - exceptionListeners.forEach( - listener -> { - listener - .getExceptionListener(samzaPipelineOptions) - .onException(new SamzaPipelineExceptionContext(transformFullName, e)); - }); - } catch (Exception t) { - // ignore exception/interruption by listeners - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpEmitter.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpEmitter.java deleted file mode 100644 index c74d1cf1e11e..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpEmitter.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.concurrent.CompletionStage; -import org.apache.beam.sdk.values.WindowedValue; -import org.joda.time.Instant; - -/** Output emitter for Samza {@link Op}. */ -public interface OpEmitter { - - void emitFuture(CompletionStage>> resultFuture); - - void emitElement(WindowedValue element); - - void emitWatermark(Instant watermark); - - void emitView(String id, WindowedValue> elements); - - Collection> collectOutput(); - - CompletionStage>> collectFuture(); - - Long collectWatermark(); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpMessage.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpMessage.java deleted file mode 100644 index 33b221a8bbce..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpMessage.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import org.apache.beam.sdk.values.WindowedValue; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Instant; - -/** - * Actual message type used in Samza {@link org.apache.samza.application.StreamApplication}. It - * contains either an element of main inputs or the collection results from a view (used as side - * input). - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class OpMessage { - /** - * Type of the element(s) in the message. - * - *

    - *
  • ELEMENT - an element from main inputs. - *
  • SIDE_INPUT - a collection of elements from a view. - *
- */ - public enum Type { - ELEMENT, - SIDE_INPUT, - SIDE_INPUT_WATERMARK - } - - private final Type type; - private final WindowedValue element; - private final String viewId; - private final WindowedValue> viewElements; - private final Instant sideInputWatermark; - - public static OpMessage ofElement(WindowedValue element) { - return new OpMessage<>(Type.ELEMENT, element, null, null, null); - } - - public static OpMessage ofSideInput( - String viewId, WindowedValue> elements) { - return new OpMessage<>(Type.SIDE_INPUT, null, viewId, elements, null); - } - - public static OpMessage ofSideInputWatermark(Instant watermark) { - return new OpMessage<>(Type.SIDE_INPUT_WATERMARK, null, null, null, watermark); - } - - private OpMessage( - Type type, - WindowedValue element, - String viewId, - WindowedValue> viewElements, - Instant sideInputWatermark) { - this.type = type; - this.element = element; - this.viewId = viewId; - this.viewElements = viewElements; - this.sideInputWatermark = sideInputWatermark; - } - - public Type getType() { - return type; - } - - public WindowedValue getElement() { - ensureType(Type.ELEMENT, "getElement"); - return element; - } - - public String getViewId() { - ensureType(Type.SIDE_INPUT, "getViewId"); - return viewId; - } - - public WindowedValue> getViewElements() { - ensureType(Type.SIDE_INPUT, "getViewElements"); - return viewElements; - } - - public Instant getSideInputWatermark() { - return sideInputWatermark; - } - - private void ensureType(Type type, String method) { - if (this.type != type) { - throw new IllegalStateException( - String.format("Calling %s requires type %s, but was type %s", method, type, this.type)); - } - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - - if (!(o instanceof OpMessage)) { - return false; - } - - OpMessage opMessage = (OpMessage) o; - - if (type != opMessage.type) { - return false; - } - - if (element != null ? !element.equals(opMessage.element) : opMessage.element != null) { - return false; - } - - if (viewId != null ? !viewId.equals(opMessage.viewId) : opMessage.viewId != null) { - return false; - } - - return viewElements != null - ? viewElements.equals(opMessage.viewElements) - : opMessage.viewElements == null; - } - - @Override - public int hashCode() { - int result = type.hashCode(); - result = 31 * result + (element != null ? element.hashCode() : 0); - result = 31 * result + (viewId != null ? viewId.hashCode() : 0); - result = 31 * result + (viewElements != null ? viewElements.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "OpMessage{" - + "type=" - + type - + ", element=" - + element - + ", viewId='" - + viewId - + '\'' - + ", viewElements=" - + viewElements - + '}'; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OutputManagerFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OutputManagerFactory.java deleted file mode 100644 index c7bb7caa19aa..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OutputManagerFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.Serializable; -import org.apache.beam.sdk.util.WindowedValueMultiReceiver; - -/** Factory class to create {@link WindowedValueMultiReceiver}. */ -public interface OutputManagerFactory extends Serializable { - WindowedValueMultiReceiver create(OpEmitter emitter); - - default WindowedValueMultiReceiver create( - OpEmitter emitter, FutureCollector collector) { - return create(emitter); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableBundleManager.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableBundleManager.java deleted file mode 100644 index 26cc73c76f91..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableBundleManager.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Bundle management for the {@link DoFnOp} that handles lifecycle of a bundle. It also serves as a - * proxy for the {@link DoFnOp} to process watermark and decides to 1. Hold watermark if there is at - * least one bundle in progress. 2. Propagates the watermark to downstream DAG, if all the previous - * bundles have completed. - * - *

This class is not thread safe and the current implementation relies on the assumption that - * messages are dispatched to BundleManager in a single threaded mode. - * - * @param output type of the {@link DoFnOp} - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class PortableBundleManager implements BundleManager { - private static final Logger LOG = LoggerFactory.getLogger(PortableBundleManager.class); - private static final long MIN_BUNDLE_CHECK_TIME_MS = 10L; - - private final long maxBundleSize; - private final long maxBundleTimeMs; - private final BundleProgressListener bundleProgressListener; - private final Scheduler> bundleTimerScheduler; - private final String bundleCheckTimerId; - - // Number elements belonging to the current active bundle - private AtomicLong currentBundleElementCount; - // Number of bundles that are in progress but not yet finished - private AtomicLong pendingBundleCount; - // Denotes the start time of the current active bundle - private AtomicLong bundleStartTime; - // Denotes if there is an active in progress bundle. Note at a given time, we can have multiple - // bundle in progress. - // This flag denotes if there is a bundle that is current and hasn't been closed. - private AtomicBoolean isBundleStarted; - // Holder for watermark which gets propagated when the bundle is finished. - private volatile Instant bundleWatermarkHold; - - public PortableBundleManager( - BundleProgressListener bundleProgressListener, - long maxBundleSize, - long maxBundleTimeMs, - Scheduler> bundleTimerScheduler, - String bundleCheckTimerId) { - this.maxBundleSize = maxBundleSize; - this.maxBundleTimeMs = maxBundleTimeMs; - this.bundleProgressListener = bundleProgressListener; - this.bundleTimerScheduler = bundleTimerScheduler; - this.bundleCheckTimerId = bundleCheckTimerId; - - if (maxBundleSize > 1) { - scheduleNextBundleCheck(); - } - - // instance variable initialization for bundle tracking - this.bundleStartTime = new AtomicLong(Long.MAX_VALUE); - this.currentBundleElementCount = new AtomicLong(0); - this.isBundleStarted = new AtomicBoolean(false); - this.pendingBundleCount = new AtomicLong(0); - } - - /* - * Schedule in processing time to check whether the current bundle should be closed. Note that - * we only approximately achieve max bundle time by checking as frequent as half of the max bundle - * time set by users. This would violate the max bundle time by up to half of it but should - * acceptable in most cases (and cheaper than scheduling a timer at the beginning of every bundle). - */ - private void scheduleNextBundleCheck() { - final Instant nextBundleCheckTime = - Instant.now().plus(Duration.millis(maxBundleTimeMs / 2 + MIN_BUNDLE_CHECK_TIME_MS)); - final TimerInternals.TimerData timerData = - TimerInternals.TimerData.of( - this.bundleCheckTimerId, - StateNamespaces.global(), - nextBundleCheckTime, - nextBundleCheckTime, - TimeDomain.PROCESSING_TIME, - CausedByDrain.NORMAL); - bundleTimerScheduler.schedule( - new KeyedTimerData<>(new byte[0], null, timerData), nextBundleCheckTime.getMillis()); - } - - @Override - public void tryStartBundle() { - inconsistentStateCheck(); - - LOG.debug( - "tryStartBundle: elementCount={}, Bundle={}", currentBundleElementCount, this.toString()); - - if (isBundleStarted.compareAndSet(false, true)) { - LOG.debug("Starting a new bundle."); - bundleStartTime.set(System.currentTimeMillis()); - pendingBundleCount.getAndIncrement(); - bundleProgressListener.onBundleStarted(); - } - - currentBundleElementCount.incrementAndGet(); - } - - @Override - public void processWatermark(Instant watermark, OpEmitter emitter) { - // propagate watermark immediately if no bundle is in progress and all the previous bundles have - // completed. - if (shouldProcessWatermark()) { - LOG.debug("Propagating watermark: {} directly since no bundle in progress.", watermark); - bundleProgressListener.onWatermark(watermark, emitter); - return; - } - - // hold back the watermark since there is either a bundle in progress or previously closed - // bundles are unfinished. - this.bundleWatermarkHold = watermark; - } - - @Override - public void processTimer(KeyedTimerData keyedTimerData, OpEmitter emitter) { - inconsistentStateCheck(); - // this is internal timer in processing time to check whether a bundle should be closed - if (bundleCheckTimerId.equals(keyedTimerData.getTimerData().getTimerId())) { - tryFinishBundle(emitter); - scheduleNextBundleCheck(); - } - } - - /** - * Signal the bundle manager to handle failure. We discard the output collected as part of - * processing the current element and reset the bundle count. - * - * @param t failure cause - */ - @Override - public void signalFailure(Throwable t) { - inconsistentStateCheck(); - LOG.error("Encountered error during processing the message. Discarding the output due to: ", t); - - isBundleStarted.set(false); - currentBundleElementCount.set(0); - bundleStartTime.set(Long.MAX_VALUE); - pendingBundleCount.decrementAndGet(); - } - - @Override - public void tryFinishBundle(OpEmitter emitter) { - LOG.debug("tryFinishBundle: elementCount={}", currentBundleElementCount); - inconsistentStateCheck(); - if (shouldFinishBundle() && isBundleStarted.compareAndSet(true, false)) { - LOG.debug("Finishing the current bundle. Bundle={}", this); - currentBundleElementCount.set(0); - bundleStartTime.set(Long.MAX_VALUE); - - Instant watermarkHold = bundleWatermarkHold; - bundleWatermarkHold = null; - - pendingBundleCount.decrementAndGet(); - - bundleProgressListener.onBundleFinished(emitter); - if (watermarkHold != null) { - bundleProgressListener.onWatermark(watermarkHold, emitter); - } - } - } - - public void inconsistentStateCheck() { - if (!isBundleStarted.get() && currentBundleElementCount.get() != 0) { - LOG.warn( - "Bundle is in a inconsistent state. isBundleStarted = false, but currentBundleElementCount = {}", - currentBundleElementCount); - } - } - - private boolean shouldProcessWatermark() { - return !isBundleStarted.get() && pendingBundleCount.get() == 0; - } - - /** - * We close the current bundle in progress if one of the following criteria is met 1. The bundle - * count ≥ maxBundleSize 2. Time elapsed since the bundle started is ≥ maxBundleTimeMs 3. - * Watermark hold equals to TIMESTAMP_MAX_VALUE which usually is the case for bounded jobs - * - * @return true - if one of the criteria above is satisfied; false - otherwise - */ - private boolean shouldFinishBundle() { - return (currentBundleElementCount.get() >= maxBundleSize - || System.currentTimeMillis() - bundleStartTime.get() >= maxBundleTimeMs - || BoundedWindow.TIMESTAMP_MAX_VALUE.equals(bundleWatermarkHold)); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableDoFnOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableDoFnOp.java deleted file mode 100644 index 468e4b9aa8dc..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/PortableDoFnOp.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.core.PushbackSideInputDoFnRunner; -import org.apache.beam.runners.core.SideInputHandler; -import org.apache.beam.runners.core.SimplePushbackSideInputDoFnRunner; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.fnexecution.control.ExecutableStageContext; -import org.apache.beam.runners.fnexecution.control.StageBundleFactory; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.samza.SamzaExecutionContext; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.util.DoFnUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.reflect.DoFnInvoker; -import org.apache.beam.sdk.transforms.reflect.DoFnInvokers; -import org.apache.beam.sdk.transforms.reflect.DoFnSignature; -import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Samza operator for {@link DoFn}. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class PortableDoFnOp implements Op { - private static final Logger LOG = LoggerFactory.getLogger(PortableDoFnOp.class); - - private final TupleTag mainOutputTag; - private final DoFn doFn; - private final Coder keyCoder; - private final Collection> sideInputs; - private final List> sideOutputTags; - private final WindowingStrategy windowingStrategy; - private final OutputManagerFactory outputManagerFactory; - // NOTE: we use HashMap here to guarantee Serializability - // Mapping from view id to a view - private final HashMap> idToViewMap; - private final String transformFullName; - private final String transformId; - private final Coder inputCoder; - private final Coder> windowedValueCoder; - private final HashMap, Coder> outputCoders; - private final PCollection.IsBounded isBounded; - private final String bundleCheckTimerId; - private final String bundleStateId; - - // portable api related - private final boolean isPortable; - private final RunnerApi.ExecutableStagePayload stagePayload; - private final JobInfo jobInfo; - private final HashMap> idToTupleTagMap; - - private transient SamzaTimerInternalsFactory timerInternalsFactory; - private transient DoFnRunner fnRunner; - private transient PushbackSideInputDoFnRunner pushbackFnRunner; - private transient SideInputHandler sideInputHandler; - private transient DoFnInvoker doFnInvoker; - private transient SamzaPipelineOptions samzaPipelineOptions; - - // This is derivable from pushbackValues which is persisted to a store. - // TODO: eagerly initialize the hold in init - @edu.umd.cs.findbugs.annotations.SuppressWarnings( - justification = "No bug", - value = "SE_TRANSIENT_FIELD_NOT_RESTORED") - private transient Instant pushbackWatermarkHold; - - // TODO: add this to checkpointable state - private transient Instant inputWatermark; - private transient BundleManager bundleManager; - private transient Instant sideInputWatermark; - private transient List> pushbackValues; - private transient ExecutableStageContext stageContext; - private transient StageBundleFactory stageBundleFactory; - private transient boolean bundleDisabled; - - private final DoFnSchemaInformation doFnSchemaInformation; - private final Map> sideInputMapping; - private final Map stateIdToStoreMapping; - - public PortableDoFnOp( - TupleTag mainOutputTag, - DoFn doFn, - Coder keyCoder, - Coder inputCoder, - Coder> windowedValueCoder, - Map, Coder> outputCoders, - Collection> sideInputs, - List> sideOutputTags, - WindowingStrategy windowingStrategy, - Map> idToViewMap, - OutputManagerFactory outputManagerFactory, - String transformFullName, - String transformId, - PCollection.IsBounded isBounded, - boolean isPortable, - RunnerApi.ExecutableStagePayload stagePayload, - JobInfo jobInfo, - Map> idToTupleTagMap, - DoFnSchemaInformation doFnSchemaInformation, - Map> sideInputMapping, - Map stateIdToStoreMapping) { - this.mainOutputTag = mainOutputTag; - this.doFn = doFn; - this.sideInputs = sideInputs; - this.sideOutputTags = sideOutputTags; - this.inputCoder = inputCoder; - this.windowedValueCoder = windowedValueCoder; - this.outputCoders = new HashMap<>(outputCoders); - this.windowingStrategy = windowingStrategy; - this.idToViewMap = new HashMap<>(idToViewMap); - this.outputManagerFactory = outputManagerFactory; - this.transformFullName = transformFullName; - this.transformId = transformId; - this.keyCoder = keyCoder; - this.isBounded = isBounded; - this.isPortable = isPortable; - this.stagePayload = stagePayload; - this.jobInfo = jobInfo; - this.idToTupleTagMap = new HashMap<>(idToTupleTagMap); - this.bundleCheckTimerId = "_samza_bundle_check_" + transformId; - this.bundleStateId = "_samza_bundle_" + transformId; - this.doFnSchemaInformation = doFnSchemaInformation; - this.sideInputMapping = sideInputMapping; - this.stateIdToStoreMapping = stateIdToStoreMapping; - } - - @Override - @SuppressWarnings("unchecked") - public void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter emitter) { - this.inputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - this.sideInputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - this.pushbackWatermarkHold = BoundedWindow.TIMESTAMP_MAX_VALUE; - - final DoFnSignature signature = DoFnSignatures.getSignature(doFn.getClass()); - final SamzaExecutionContext samzaExecutionContext = - (SamzaExecutionContext) context.getApplicationContainerContext(); - this.samzaPipelineOptions = samzaExecutionContext.getPipelineOptions(); - this.bundleDisabled = samzaPipelineOptions.getMaxBundleSize() <= 1; - - final String stateId = "pardo-" + transformId; - final SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory = - SamzaStoreStateInternals.createNonKeyedStateInternalsFactory( - stateId, context.getTaskContext(), samzaPipelineOptions); - final FutureCollector outputFutureCollector = createFutureCollector(); - - this.bundleManager = - new ClassicBundleManager<>( - createBundleProgressListener(), - outputFutureCollector, - samzaPipelineOptions.getMaxBundleSize(), - samzaPipelineOptions.getMaxBundleTimeMs(), - timerRegistry, - bundleCheckTimerId); - - this.timerInternalsFactory = - SamzaTimerInternalsFactory.createTimerInternalFactory( - keyCoder, - (Scheduler) timerRegistry, - getTimerStateId(signature), - nonKeyedStateInternalsFactory, - windowingStrategy, - isBounded, - samzaPipelineOptions); - - this.sideInputHandler = - new SideInputHandler(sideInputs, nonKeyedStateInternalsFactory.stateInternalsForKey(null)); - - if (isPortable) { - final ExecutableStage executableStage = ExecutableStage.fromPayload(stagePayload); - stageContext = SamzaExecutableStageContextFactory.getInstance().get(jobInfo); - stageBundleFactory = stageContext.getStageBundleFactory(executableStage); - this.fnRunner = - SamzaDoFnRunners.createPortable( - transformId, - DoFnUtils.toStepName(executableStage), - bundleStateId, - windowedValueCoder, - executableStage, - sideInputMapping, - sideInputHandler, - nonKeyedStateInternalsFactory, - timerInternalsFactory, - samzaPipelineOptions, - outputManagerFactory.create(emitter, outputFutureCollector), - stageBundleFactory, - samzaExecutionContext, - mainOutputTag, - idToTupleTagMap, - context, - transformFullName); - } else { - this.fnRunner = - SamzaDoFnRunners.create( - samzaPipelineOptions, - doFn, - windowingStrategy, - transformFullName, - stateId, - context, - mainOutputTag, - sideInputHandler, - timerInternalsFactory, - keyCoder, - outputManagerFactory.create(emitter, outputFutureCollector), - inputCoder, - sideOutputTags, - outputCoders, - doFnSchemaInformation, - (Map>) sideInputMapping, - stateIdToStoreMapping, - emitter, - outputFutureCollector); - } - - this.pushbackFnRunner = - SimplePushbackSideInputDoFnRunner.create(fnRunner, sideInputs, sideInputHandler); - this.pushbackValues = new ArrayList<>(); - - final Iterator invokerReg = - ServiceLoader.load(SamzaDoFnInvokerRegistrar.class).iterator(); - if (!invokerReg.hasNext()) { - // use the default invoker here - doFnInvoker = DoFnInvokers.tryInvokeSetupFor(doFn, samzaPipelineOptions); - } else { - doFnInvoker = - Iterators.getOnlyElement(invokerReg).invokerSetupFor(doFn, samzaPipelineOptions, context); - } - } - - FutureCollector createFutureCollector() { - return new FutureCollectorImpl<>(); - } - - private String getTimerStateId(DoFnSignature signature) { - final StringBuilder builder = new StringBuilder("timer"); - if (signature.usesTimers()) { - signature.timerDeclarations().keySet().forEach(builder::append); - } - return builder.toString(); - } - - @Override - public void processElement(WindowedValue inputElement, OpEmitter emitter) { - try { - bundleManager.tryStartBundle(); - final Iterable> rejectedValues = - pushbackFnRunner.processElementInReadyWindows(inputElement); - for (WindowedValue rejectedValue : rejectedValues) { - if (rejectedValue.getTimestamp().compareTo(pushbackWatermarkHold) < 0) { - pushbackWatermarkHold = rejectedValue.getTimestamp(); - } - pushbackValues.add(rejectedValue); - } - - bundleManager.tryFinishBundle(emitter); - } catch (Throwable t) { - LOG.error("Encountered error during process element", t); - bundleManager.signalFailure(t); - throw t; - } - } - - private void doProcessWatermark(Instant watermark, OpEmitter emitter) { - this.inputWatermark = watermark; - - if (sideInputWatermark.isEqual(BoundedWindow.TIMESTAMP_MAX_VALUE)) { - // this means we will never see any more side input - emitAllPushbackValues(); - } - - final Instant actualInputWatermark = - pushbackWatermarkHold.isBefore(inputWatermark) ? pushbackWatermarkHold : inputWatermark; - - timerInternalsFactory.setInputWatermark(actualInputWatermark); - - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - if (!readyTimers.isEmpty()) { - pushbackFnRunner.startBundle(); - for (KeyedTimerData keyedTimerData : readyTimers) { - fireTimer(keyedTimerData); - } - pushbackFnRunner.finishBundle(); - } - - if (timerInternalsFactory.getOutputWatermark() == null - || timerInternalsFactory.getOutputWatermark().isBefore(actualInputWatermark)) { - timerInternalsFactory.setOutputWatermark(actualInputWatermark); - emitter.emitWatermark(timerInternalsFactory.getOutputWatermark()); - } - } - - @Override - public void processWatermark(Instant watermark, OpEmitter emitter) { - bundleManager.processWatermark(watermark, emitter); - } - - @Override - public void processSideInput( - String id, WindowedValue> elements, OpEmitter emitter) { - checkState( - bundleDisabled, "Side input not supported in bundling mode. Please disable bundling."); - @SuppressWarnings("unchecked") - final WindowedValue> retypedElements = (WindowedValue>) elements; - - final PCollectionView view = idToViewMap.get(id); - if (view == null) { - throw new IllegalArgumentException("No mapping of id " + id + " to view."); - } - - sideInputHandler.addSideInputValue(view, retypedElements); - - final List> previousPushbackValues = new ArrayList<>(pushbackValues); - pushbackWatermarkHold = BoundedWindow.TIMESTAMP_MAX_VALUE; - pushbackValues.clear(); - - for (final WindowedValue value : previousPushbackValues) { - processElement(value, emitter); - } - - // We may be able to advance the output watermark since we may have played some pushed back - // events. - processWatermark(this.inputWatermark, emitter); - } - - @Override - public void processSideInputWatermark(Instant watermark, OpEmitter emitter) { - checkState( - bundleDisabled, "Side input not supported in bundling mode. Please disable bundling."); - sideInputWatermark = watermark; - - if (sideInputWatermark.isEqual(BoundedWindow.TIMESTAMP_MAX_VALUE)) { - // this means we will never see any more side input - processWatermark(this.inputWatermark, emitter); - } - } - - @Override - @SuppressWarnings("unchecked") - public void processTimer(KeyedTimerData keyedTimerData, OpEmitter emitter) { - // this is internal timer in processing time to check whether a bundle should be closed - if (bundleCheckTimerId.equals(keyedTimerData.getTimerData().getTimerId())) { - bundleManager.processTimer(keyedTimerData, emitter); - return; - } - - pushbackFnRunner.startBundle(); - fireTimer(keyedTimerData); - pushbackFnRunner.finishBundle(); - - this.timerInternalsFactory.removeProcessingTimer((KeyedTimerData) keyedTimerData); - } - - @Override - public void close() { - doFnInvoker.invokeTeardown(); - try (AutoCloseable factory = stageBundleFactory; - AutoCloseable context = stageContext) { - // do nothing - } catch (Exception e) { - LOG.error("Failed to close stage bundle factory", e); - } - } - - private void fireTimer(KeyedTimerData keyedTimerData) { - final TimerInternals.TimerData timer = keyedTimerData.getTimerData(); - LOG.debug("Firing timer {}", timer); - - final StateNamespace namespace = timer.getNamespace(); - // NOTE: not sure why this is safe, but DoFnOperator makes this assumption - final BoundedWindow window = ((StateNamespaces.WindowNamespace) namespace).getWindow(); - - fnRunner.onTimer( - timer.getTimerId(), - timer.getTimerFamilyId(), - keyedTimerData.getKey(), - window, - timer.getTimestamp(), - timer.getOutputTimestamp(), - timer.getDomain(), - timer.causedByDrain()); - } - - // todo: should this go through bundle manager to start and finish the bundle? - private void emitAllPushbackValues() { - if (!pushbackValues.isEmpty()) { - pushbackFnRunner.startBundle(); - - final List> previousPushbackValues = new ArrayList<>(pushbackValues); - pushbackWatermarkHold = BoundedWindow.TIMESTAMP_MAX_VALUE; - pushbackValues.clear(); - - for (final WindowedValue value : previousPushbackValues) { - fnRunner.processElement(value); - } - - pushbackFnRunner.finishBundle(); - } - } - - private BundleManager.BundleProgressListener createBundleProgressListener() { - return new BundleManager.BundleProgressListener() { - @Override - public void onBundleStarted() { - pushbackFnRunner.startBundle(); - } - - @Override - public void onBundleFinished(OpEmitter emitter) { - pushbackFnRunner.finishBundle(); - } - - @Override - public void onWatermark(Instant watermark, OpEmitter emitter) { - doProcessWatermark(watermark, emitter); - } - }; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaAssignContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaAssignContext.java deleted file mode 100644 index 0f8933e66cbf..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaAssignContext.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.WindowFn; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.joda.time.Instant; - -@SuppressWarnings({"keyfor", "nullness"}) // TODO(https://github.com/apache/beam/issues/20497) -class SamzaAssignContext extends WindowFn.AssignContext { - private final WindowedValue value; - - public SamzaAssignContext(WindowFn fn, WindowedValue value) { - fn.super(); - this.value = value; - - if (value.getWindows().size() != 1) { - throw new IllegalArgumentException( - String.format( - "Only single windowed value allowed for assignment. Windows: %s", - value.getWindows())); - } - } - - @Override - public InT element() { - return value.getValue(); - } - - @Override - public Instant timestamp() { - return value.getTimestamp(); - } - - @Override - public BoundedWindow window() { - return Iterables.getOnlyElement(value.getWindows()); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnInvokerRegistrar.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnInvokerRegistrar.java deleted file mode 100644 index c0536161cb79..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnInvokerRegistrar.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Map; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.reflect.DoFnInvoker; -import org.apache.samza.context.Context; - -/** A registrar for Samza DoFnInvoker. */ -public interface SamzaDoFnInvokerRegistrar { - - /** Returns the invoker for a {@link DoFn}. */ - DoFnInvoker invokerSetupFor( - DoFn fn, PipelineOptions options, Context context); - - /** Returns the configs for a {@link DoFn}. */ - Map configFor(DoFn fn); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java deleted file mode 100644 index a2ec88a43415..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java +++ /dev/null @@ -1,506 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.core.DoFnRunners; -import org.apache.beam.runners.core.SideInputHandler; -import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.StateTags; -import org.apache.beam.runners.core.StatefulDoFnRunner; -import org.apache.beam.runners.core.StepContext; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.fnexecution.control.OutputReceiverFactory; -import org.apache.beam.runners.fnexecution.control.RemoteBundle; -import org.apache.beam.runners.fnexecution.control.StageBundleFactory; -import org.apache.beam.runners.fnexecution.control.TimerReceiverFactory; -import org.apache.beam.runners.fnexecution.state.StateRequestHandler; -import org.apache.beam.runners.samza.SamzaExecutionContext; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.metrics.DoFnRunnerWithMetrics; -import org.apache.beam.runners.samza.util.StateUtils; -import org.apache.beam.runners.samza.util.WindowUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.fn.data.FnDataReceiver; -import org.apache.beam.sdk.state.BagState; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.PaneInfo; -import org.apache.beam.sdk.util.WindowedValueMultiReceiver; -import org.apache.beam.sdk.util.construction.Timer; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.TypeDescriptor; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.samza.context.Context; -import org.joda.time.Instant; - -/** A factory for Samza runner translator to create underlying DoFnRunner used in {@link DoFnOp}. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaDoFnRunners { - - /** Create DoFnRunner for java runner. */ - public static DoFnRunner create( - SamzaPipelineOptions pipelineOptions, - DoFn doFn, - WindowingStrategy windowingStrategy, - String transformFullName, - String transformId, - Context context, - TupleTag mainOutputTag, - SideInputHandler sideInputHandler, - SamzaTimerInternalsFactory timerInternalsFactory, - Coder keyCoder, - WindowedValueMultiReceiver outputManager, - Coder inputCoder, - List> sideOutputTags, - Map, Coder> outputCoders, - DoFnSchemaInformation doFnSchemaInformation, - Map> sideInputMapping, - Map stateIdToStoreIdMapping, - OpEmitter emitter, - FutureCollector futureCollector) { - final KeyedInternals keyedInternals; - final TimerInternals timerInternals; - final StateInternals stateInternals; - final SamzaStoreStateInternals.Factory stateInternalsFactory = - SamzaStoreStateInternals.createStateInternalsFactory( - transformId, - keyCoder, - context.getTaskContext(), - pipelineOptions, - stateIdToStoreIdMapping); - - final SamzaExecutionContext executionContext = - (SamzaExecutionContext) context.getApplicationContainerContext(); - if (StateUtils.isStateful(doFn)) { - keyedInternals = new KeyedInternals(stateInternalsFactory, timerInternalsFactory); - stateInternals = keyedInternals.stateInternals(); - timerInternals = keyedInternals.timerInternals(); - } else { - keyedInternals = null; - stateInternals = stateInternalsFactory.stateInternalsForKey(null); - timerInternals = timerInternalsFactory.timerInternalsForKey(null); - } - - final StepContext stepContext = createStepContext(stateInternals, timerInternals); - final DoFnRunner underlyingRunner = - DoFnRunners.simpleRunner( - pipelineOptions, - doFn, - sideInputHandler, - outputManager, - mainOutputTag, - sideOutputTags, - stepContext, - inputCoder, - outputCoders, - windowingStrategy, - doFnSchemaInformation, - sideInputMapping); - - final DoFnRunner doFnRunnerWithMetrics = - pipelineOptions.getEnableMetrics() - ? DoFnRunnerWithMetrics.wrap( - underlyingRunner, executionContext.getMetricsContainer(), transformFullName) - : underlyingRunner; - - final DoFnRunner doFnRunnerWithStates; - if (keyedInternals != null) { - final DoFnRunner statefulDoFnRunner = - DoFnRunners.defaultStatefulDoFnRunner( - doFn, - inputCoder, - doFnRunnerWithMetrics, - stepContext, - windowingStrategy, - new StatefulDoFnRunner.TimeInternalsCleanupTimer(timerInternals, windowingStrategy), - createStateCleaner(doFn, windowingStrategy, keyedInternals.stateInternals())); - - doFnRunnerWithStates = new DoFnRunnerWithKeyedInternals<>(statefulDoFnRunner, keyedInternals); - } else { - doFnRunnerWithStates = doFnRunnerWithMetrics; - } - - return pipelineOptions.getNumThreadsForProcessElement() > 1 - ? AsyncDoFnRunner.create( - doFnRunnerWithStates, emitter, futureCollector, keyedInternals != null, pipelineOptions) - : doFnRunnerWithStates; - } - - /** Creates a {@link StepContext} that allows accessing state and timer internals. */ - private static StepContext createStepContext( - StateInternals stateInternals, TimerInternals timerInternals) { - return new StepContext() { - @Override - public StateInternals stateInternals() { - return stateInternals; - } - - @Override - public TimerInternals timerInternals() { - return timerInternals; - } - }; - } - - @SuppressWarnings("unchecked") - private static StatefulDoFnRunner.StateCleaner createStateCleaner( - DoFn doFn, - WindowingStrategy windowingStrategy, - StateInternals stateInternals) { - final TypeDescriptor windowType = windowingStrategy.getWindowFn().getWindowTypeDescriptor(); - if (windowType.isSubtypeOf(TypeDescriptor.of(BoundedWindow.class))) { - final Coder windowCoder = - windowingStrategy.getWindowFn().windowCoder(); - return new StatefulDoFnRunner.StateInternalsStateCleaner<>(doFn, stateInternals, windowCoder); - } else { - return null; - } - } - - /** Create DoFnRunner for portable runner. */ - @SuppressWarnings("unchecked") - public static DoFnRunner createPortable( - String transformId, - String stepName, - String bundleStateId, - Coder> windowedValueCoder, - ExecutableStage executableStage, - Map> sideInputMapping, - SideInputHandler sideInputHandler, - SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory, - SamzaTimerInternalsFactory timerInternalsFactory, - SamzaPipelineOptions pipelineOptions, - WindowedValueMultiReceiver outputManager, - StageBundleFactory stageBundleFactory, - SamzaExecutionContext samzaExecutionContext, - TupleTag mainOutputTag, - Map> idToTupleTagMap, - Context context, - String transformFullName) { - // storing events within a bundle in states - final BagState> bundledEventsBag = - nonKeyedStateInternalsFactory - .stateInternalsForKey(null) - .state(StateNamespaces.global(), StateTags.bag(bundleStateId, windowedValueCoder)); - - final StateRequestHandler stateRequestHandler = - SamzaStateRequestHandlers.of( - transformId, - context.getTaskContext(), - pipelineOptions, - executableStage, - stageBundleFactory, - (Map>) - sideInputMapping, - sideInputHandler); - - final SamzaExecutionContext executionContext = - (SamzaExecutionContext) context.getApplicationContainerContext(); - final DoFnRunner underlyingRunner = - new SdkHarnessDoFnRunner<>( - pipelineOptions, - stepName, - timerInternalsFactory, - WindowUtils.getWindowStrategy( - executableStage.getInputPCollection().getId(), executableStage.getComponents()), - outputManager, - stageBundleFactory, - idToTupleTagMap, - bundledEventsBag, - stateRequestHandler, - samzaExecutionContext, - executableStage.getTransforms()); - return pipelineOptions.getEnableMetrics() - ? DoFnRunnerWithMetrics.wrap( - underlyingRunner, executionContext.getMetricsContainer(), transformFullName) - : underlyingRunner; - } - - static class SdkHarnessDoFnRunner implements DoFnRunner { - - private static final int DEFAULT_METRIC_SAMPLE_RATE = 100; - - private final SamzaPipelineOptions pipelineOptions; - private final SamzaTimerInternalsFactory timerInternalsFactory; - private final WindowingStrategy windowingStrategy; - private final WindowedValueMultiReceiver outputManager; - private final StageBundleFactory stageBundleFactory; - private final Map> idToTupleTagMap; - private final LinkedBlockingQueue> outputQueue = new LinkedBlockingQueue<>(); - private final BagState> bundledEventsBag; - private RemoteBundle remoteBundle; - private FnDataReceiver> inputReceiver; - private final StateRequestHandler stateRequestHandler; - private final SamzaExecutionContext samzaExecutionContext; - private long startBundleTime; - private final String stepName; - private final Collection pTransformNodes; - - private SdkHarnessDoFnRunner( - SamzaPipelineOptions pipelineOptions, - String stepName, - SamzaTimerInternalsFactory timerInternalsFactory, - WindowingStrategy windowingStrategy, - WindowedValueMultiReceiver outputManager, - StageBundleFactory stageBundleFactory, - Map> idToTupleTagMap, - BagState> bundledEventsBag, - StateRequestHandler stateRequestHandler, - SamzaExecutionContext samzaExecutionContext, - Collection pTransformNodes) { - this.pipelineOptions = pipelineOptions; - this.timerInternalsFactory = timerInternalsFactory; - this.windowingStrategy = windowingStrategy; - this.outputManager = outputManager; - this.stageBundleFactory = stageBundleFactory; - this.idToTupleTagMap = idToTupleTagMap; - this.bundledEventsBag = bundledEventsBag; - this.stateRequestHandler = stateRequestHandler; - this.samzaExecutionContext = samzaExecutionContext; - this.stepName = stepName; - this.pTransformNodes = pTransformNodes; - } - - @SuppressWarnings("unchecked") - private void timerDataConsumer(Timer timerElement, TimerInternals.TimerData timerData) { - TimerInternals timerInternals = - timerInternalsFactory.timerInternalsForKey(timerElement.getUserKey()); - if (timerElement.getClearBit()) { - timerInternals.deleteTimer(timerData); - } else { - timerInternals.setTimer(timerData); - } - } - - @Override - public void startBundle() { - try { - OutputReceiverFactory receiverFactory = - new OutputReceiverFactory() { - @Override - public FnDataReceiver create(String pCollectionId) { - return (receivedElement) -> { - // handover to queue, do not block the grpc thread - outputQueue.put(KV.of(pCollectionId, receivedElement)); - }; - } - }; - - final Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); - final TimerReceiverFactory timerReceiverFactory = - new TimerReceiverFactory(stageBundleFactory, this::timerDataConsumer, windowCoder); - - Map transformFullNameToUniqueName = - pTransformNodes.stream() - .collect( - Collectors.toMap( - pTransformNode -> pTransformNode.getId(), - pTransformNode -> pTransformNode.getTransform().getUniqueName())); - - SamzaMetricsBundleProgressHandler samzaMetricsBundleProgressHandler = - new SamzaMetricsBundleProgressHandler( - stepName, - samzaExecutionContext.getMetricsContainer(), - transformFullNameToUniqueName); - - remoteBundle = - stageBundleFactory.getBundle( - receiverFactory, - timerReceiverFactory, - stateRequestHandler, - samzaMetricsBundleProgressHandler); - - startBundleTime = getStartBundleTime(); - - inputReceiver = Iterables.getOnlyElement(remoteBundle.getInputReceivers().values()); - bundledEventsBag - .read() - .forEach( - elem -> { - try { - inputReceiver.accept(elem); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings({ - "RandomModInteger" // https://errorprone.info/bugpattern/RandomModInteger - }) - private long getStartBundleTime() { - /* - * Use random number for sampling purpose instead of counting as - * SdkHarnessDoFnRunner is stateless and counters won't persist - * between invocations of DoFn(s). - */ - return ThreadLocalRandom.current().nextInt() % DEFAULT_METRIC_SAMPLE_RATE == 0 - ? System.nanoTime() - : 0; - } - - @Override - public void processElement(WindowedValue elem) { - try { - bundledEventsBag.add(elem); - inputReceiver.accept(elem); - emitResults(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private void emitResults() { - KV result; - while ((result = outputQueue.poll()) != null) { - outputManager.output( - idToTupleTagMap.get(result.getKey()), (WindowedValue) result.getValue()); - } - } - - private void emitMetrics() { - if (startBundleTime <= 0) { - return; - } - - final long count = Iterables.size(bundledEventsBag.read()); - - if (count <= 0) { - return; - } - - final long finishBundleTime = System.nanoTime(); - final long averageProcessTime = (finishBundleTime - startBundleTime) / count; - - String metricName = "ExecutableStage-" + stepName + "-process-ns"; - samzaExecutionContext - .getMetricsContainer() - .updateExecutableStageBundleMetric(metricName, averageProcessTime); - } - - @Override - public void onTimer( - String timerId, - String timerFamilyId, - KeyT key, - BoundedWindow window, - Instant timestamp, - Instant outputTimestamp, - TimeDomain timeDomain, - CausedByDrain causedByDrain) { - final KV timerReceiverKey = - TimerReceiverFactory.decodeTimerDataTimerId(timerFamilyId); - final FnDataReceiver timerReceiver = - remoteBundle.getTimerReceivers().get(timerReceiverKey); - final Timer timerValue = - Timer.of( - key, - timerId, - Collections.singletonList(window), - timestamp, - outputTimestamp, - // TODO: Support propagating the PaneInfo through. - PaneInfo.NO_FIRING, - causedByDrain); - try { - timerReceiver.accept(timerValue); - } catch (Exception e) { - throw new RuntimeException( - String.format(Locale.ENGLISH, "Failed to process timer %s", timerReceiver), e); - } - } - - @Override - public void finishBundle() { - try { - runWithTimeout( - pipelineOptions.getBundleProcessingTimeout(), - () -> { - // RemoteBundle close blocks until all results are received - try { - remoteBundle.close(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - emitResults(); - emitMetrics(); - bundledEventsBag.clear(); - } catch (Exception e) { - throw new RuntimeException("Failed to finish remote bundle", e); - } finally { - remoteBundle = null; - inputReceiver = null; - } - } - - /** - * Run a function and wait for at most the given time (in milliseconds). - * - * @param timeoutInMs the time to wait for completing the function call. If the value of timeout - * is negative, wait forever until the function call is completed - * @param runnable the main function - */ - static void runWithTimeout(long timeoutInMs, Runnable runnable) - throws ExecutionException, InterruptedException, TimeoutException { - if (timeoutInMs < 0) { - runnable.run(); - } else { - CompletableFuture.runAsync(runnable).get(timeoutInMs, TimeUnit.MILLISECONDS); - } - } - - @Override - public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) {} - - @Override - public DoFn getFn() { - throw new UnsupportedOperationException(); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaExecutableStageContextFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaExecutableStageContextFactory.java deleted file mode 100644 index f034d031f2c2..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaExecutableStageContextFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.apache.beam.runners.fnexecution.control.DefaultExecutableStageContext; -import org.apache.beam.runners.fnexecution.control.ExecutableStageContext; -import org.apache.beam.runners.fnexecution.control.ReferenceCountingExecutableStageContextFactory; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; - -/** - * Singleton class that contains one {@link ExecutableStageContext.Factory} per job. Assumes it is - * safe to release the backing environment asynchronously. - */ -public class SamzaExecutableStageContextFactory implements ExecutableStageContext.Factory { - - private static final SamzaExecutableStageContextFactory instance = - new SamzaExecutableStageContextFactory(); - // This map should only ever have a single element, as each job will have its own - // classloader and therefore its own instance of SamzaExecutableStageContextFactory. This - // code supports multiple JobInfos in order to provide a sensible implementation of - // Factory.get(JobInfo), which in theory could be called with different JobInfos. - private static final ConcurrentMap jobFactories = - new ConcurrentHashMap<>(); - - private SamzaExecutableStageContextFactory() {} - - public static SamzaExecutableStageContextFactory getInstance() { - return instance; - } - - @Override - public ExecutableStageContext get(JobInfo jobInfo) { - ExecutableStageContext.Factory jobFactory = - jobFactories.computeIfAbsent( - jobInfo.jobId(), - k -> - ReferenceCountingExecutableStageContextFactory.create( - DefaultExecutableStageContext::create, - // Always release environment asynchronously. - (caller) -> false)); - - return jobFactory.get(jobInfo); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandler.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandler.java deleted file mode 100644 index 010ae53455f8..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandler.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.DISTRIBUTION_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.LATEST_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.SUM_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoEncodings.decodeInt64Counter; -import static org.apache.beam.runners.core.metrics.MonitoringInfoEncodings.decodeInt64Distribution; -import static org.apache.beam.runners.core.metrics.MonitoringInfoEncodings.decodeInt64Gauge; - -import java.util.Map; -import org.apache.beam.model.fnexecution.v1.BeamFnApi; -import org.apache.beam.model.pipeline.v1.MetricsApi; -import org.apache.beam.runners.core.metrics.DistributionData; -import org.apache.beam.runners.core.metrics.MonitoringInfoConstants; -import org.apache.beam.runners.fnexecution.control.BundleProgressHandler; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.sdk.metrics.Counter; -import org.apache.beam.sdk.metrics.Distribution; -import org.apache.beam.sdk.metrics.Gauge; -import org.apache.beam.sdk.metrics.MetricName; -import org.apache.beam.sdk.metrics.MetricsContainer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@inheritDoc} Parses metrics information contained in the bundle progress messages. Passed the - * updated metrics to the provided SamzaMetricsContainer. - */ -class SamzaMetricsBundleProgressHandler implements BundleProgressHandler { - - private static final Logger LOG = - LoggerFactory.getLogger(SamzaMetricsBundleProgressHandler.class); - private final String stepName; - - private final SamzaMetricsContainer samzaMetricsContainer; - private final Map transformIdToUniqueName; - - /** - * Constructor of a SamzaMetricsBundleProgressHandler. - * - *

The full metric names in classic mode is {transformUniqueName}:{className}:{metricName}. We - * attempt to follow the same format in portable mode, but the monitoringInfos returned by the - * worker only contains the transformId. The current solution is to provide a mapping from - * transformId back to uniqueName. A future improvement would be making the monitoring infos - * contain the uniqueName. - * - * @param stepName Default stepName provided by the runner. - * @param samzaMetricsContainer The destination for publishing the metrics. - * @param transformIdToUniqueName A mapping from transformId to uniqueName for pTransforms. - */ - public SamzaMetricsBundleProgressHandler( - String stepName, - SamzaMetricsContainer samzaMetricsContainer, - Map transformIdToUniqueName) { - this.stepName = stepName; - this.samzaMetricsContainer = samzaMetricsContainer; - this.transformIdToUniqueName = transformIdToUniqueName; - } - - @Override - /** - * {@inheritDoc} Handles a progress report from the bundle while it is executing. We choose to - * ignore the progress report. The metrics do not have to be updated on every progress report, so - * we save computation resources by ignoring it. - */ - public void onProgress(BeamFnApi.ProcessBundleProgressResponse progress) {} - - @Override - /** - * {@inheritDoc} Handles the bundle's completion report. Parses the monitoringInfos in the - * response, then updates the MetricsRegistry. - */ - public void onCompleted(BeamFnApi.ProcessBundleResponse response) { - response.getMonitoringInfosList().stream() - .filter(monitoringInfo -> !monitoringInfo.getPayload().isEmpty()) - .map(this::parseAndUpdateMetric) - .distinct() - .forEach(samzaMetricsContainer::updateMetrics); - } - - /** - * Parses the metric contained in monitoringInfo, then publishes the metric to the - * metricContainer. - * - *

We attempt to construct a classic mode metricName - * ({transformUniqueName}:{className}:{metricName}). All the info should be in the labels, but we - * have fallbacks in case the labels don't exist. - * - *

Priorities for the transformUniqueName 1. Obtained transformUniqueName using the - * transformIdToUniqueName 2. The transformId provided by the monitoringInfo 3. The stepName - * provided by the runner, which maybe a result of fusing. - * - *

Priorities for the className 1. The namespace label 2. The monitoringInfo urn. Copying the - * implementation in MonitoringInfoMetricName. - * - *

Priorities for the metricName 1. The name label 2. The monitoringInfo urn. Copying the - * implementation in MonitoringInfoMetricName. - * - * @see - * org.apache.beam.runners.core.metrics.MonitoringInfoMetricName#of(MetricsApi.MonitoringInfo) - * @return the final transformUniqueName for the metric - */ - private String parseAndUpdateMetric(MetricsApi.MonitoringInfo monitoringInfo) { - String pTransformId = - monitoringInfo.getLabelsOrDefault(MonitoringInfoConstants.Labels.PTRANSFORM, stepName); - String transformUniqueName = transformIdToUniqueName.getOrDefault(pTransformId, pTransformId); - String className = - monitoringInfo.getLabelsOrDefault( - MonitoringInfoConstants.Labels.NAMESPACE, monitoringInfo.getUrn()); - String userMetricName = - monitoringInfo.getLabelsOrDefault( - MonitoringInfoConstants.Labels.NAME, monitoringInfo.getLabelsMap().toString()); - - MetricsContainer metricsContainer = samzaMetricsContainer.getContainer(transformUniqueName); - MetricName metricName = MetricName.named(className, userMetricName); - - switch (monitoringInfo.getType()) { - case SUM_INT64_TYPE: - Counter counter = metricsContainer.getCounter(metricName); - counter.inc(decodeInt64Counter(monitoringInfo.getPayload())); - break; - - case DISTRIBUTION_INT64_TYPE: - Distribution distribution = metricsContainer.getDistribution(metricName); - DistributionData data = decodeInt64Distribution(monitoringInfo.getPayload()); - distribution.update(data.sum(), data.count(), data.min(), data.max()); - break; - - case LATEST_INT64_TYPE: - Gauge gauge = metricsContainer.getGauge(metricName); - // Gauge doesn't expose update as public. This will reset the timestamp. - - gauge.set(decodeInt64Gauge(monitoringInfo.getPayload()).value()); - break; - - default: - LOG.debug("Unsupported metric type {}", monitoringInfo.getType()); - } - return transformUniqueName; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStateRequestHandlers.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStateRequestHandlers.java deleted file mode 100644 index 0b6a0b6f4f14..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStateRequestHandlers.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.IOException; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.Map; -import org.apache.beam.model.fnexecution.v1.BeamFnApi; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.core.SideInputHandler; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.StateTags; -import org.apache.beam.runners.fnexecution.control.ProcessBundleDescriptors; -import org.apache.beam.runners.fnexecution.control.StageBundleFactory; -import org.apache.beam.runners.fnexecution.state.StateRequestHandler; -import org.apache.beam.runners.fnexecution.state.StateRequestHandlers; -import org.apache.beam.runners.fnexecution.translation.StreamingSideInputHandlerFactory; -import org.apache.beam.runners.fnexecution.wire.ByteStringCoder; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.util.StateUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.state.BagState; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; -import org.apache.samza.context.TaskContext; - -/** - * This class creates {@link StateRequestHandler} for side inputs and states of the Samza portable - * runner. - */ -public class SamzaStateRequestHandlers { - - public static StateRequestHandler of( - String transformId, - TaskContext context, - SamzaPipelineOptions pipelineOptions, - ExecutableStage executableStage, - StageBundleFactory stageBundleFactory, - Map> sideInputIds, - SideInputHandler sideInputHandler) { - final StateRequestHandler sideInputStateHandler = - createSideInputStateHandler(executableStage, sideInputIds, sideInputHandler); - final StateRequestHandler userStateRequestHandler = - createUserStateRequestHandler( - transformId, executableStage, context, pipelineOptions, stageBundleFactory); - final EnumMap handlerMap = - new EnumMap<>(BeamFnApi.StateKey.TypeCase.class); - handlerMap.put(BeamFnApi.StateKey.TypeCase.ITERABLE_SIDE_INPUT, sideInputStateHandler); - handlerMap.put(BeamFnApi.StateKey.TypeCase.MULTIMAP_SIDE_INPUT, sideInputStateHandler); - handlerMap.put(BeamFnApi.StateKey.TypeCase.MULTIMAP_KEYS_SIDE_INPUT, sideInputStateHandler); - handlerMap.put(BeamFnApi.StateKey.TypeCase.BAG_USER_STATE, userStateRequestHandler); - return StateRequestHandlers.delegateBasedUponType(handlerMap); - } - - private static StateRequestHandler createSideInputStateHandler( - ExecutableStage executableStage, - Map> sideInputIds, - SideInputHandler sideInputHandler) { - - if (executableStage.getSideInputs().size() <= 0) { - return StateRequestHandler.unsupported(); - } - - final StateRequestHandlers.SideInputHandlerFactory sideInputHandlerFactory = - Preconditions.checkNotNull( - StreamingSideInputHandlerFactory.forStage( - executableStage, sideInputIds, sideInputHandler)); - try { - return StateRequestHandlers.forSideInputHandlerFactory( - ProcessBundleDescriptors.getSideInputs(executableStage), sideInputHandlerFactory); - } catch (IOException e) { - throw new RuntimeException("Failed to initialize SideInputHandler", e); - } - } - - private static StateRequestHandler createUserStateRequestHandler( - String transformId, - ExecutableStage executableStage, - TaskContext context, - SamzaPipelineOptions pipelineOptions, - StageBundleFactory stageBundleFactory) { - - if (!StateUtils.isStateful(executableStage)) { - return StateRequestHandler.unsupported(); - } - - final SamzaStoreStateInternals.Factory stateInternalsFactory = - SamzaStoreStateInternals.createStateInternalsFactory( - transformId, ByteStringCoder.of(), context, pipelineOptions, executableStage); - - return StateRequestHandlers.forBagUserStateHandlerFactory( - stageBundleFactory.getProcessBundleDescriptor(), - new BagUserStateFactory<>(stateInternalsFactory)); - } - - /** - * Factory to create {@link StateRequestHandlers.BagUserStateHandler} to provide bag state access - * for the given {@link K key} and {@link W window} provided by SDK worker, unlike classic - * pipeline where {@link K key} is set at {@link DoFnRunnerWithKeyedInternals#processElement} and - * {@link W window} is set at {@link - * org.apache.beam.runners.core.SimpleDoFnRunner.DoFnProcessContext#window()}}. - */ - static class BagUserStateFactory< - K extends ByteString, V extends ByteString, W extends BoundedWindow> - implements StateRequestHandlers.BagUserStateHandlerFactory { - - private final SamzaStoreStateInternals.Factory stateInternalsFactory; - - BagUserStateFactory(SamzaStoreStateInternals.Factory stateInternalsFactory) { - this.stateInternalsFactory = stateInternalsFactory; - } - - @Override - public StateRequestHandlers.BagUserStateHandler forUserState( - String pTransformId, - String userStateId, - Coder keyCoder, - Coder valueCoder, - Coder windowCoder) { - return new StateRequestHandlers.BagUserStateHandler() { - - /** {@inheritDoc} */ - @Override - public Iterable get(K key, W window) { - StateNamespace namespace = StateNamespaces.window(windowCoder, window); - BagState bagState = - stateInternalsFactory - .stateInternalsForKey(key) - .state(namespace, StateTags.bag(userStateId, valueCoder)); - return bagState.read(); - } - - /** {@inheritDoc} */ - @Override - public void append(K key, W window, Iterator values) { - StateNamespace namespace = StateNamespaces.window(windowCoder, window); - BagState bagState = - stateInternalsFactory - .stateInternalsForKey(key) - .state(namespace, StateTags.bag(userStateId, valueCoder)); - while (values.hasNext()) { - bagState.add(values.next()); - } - } - - /** {@inheritDoc} */ - @Override - public void clear(K key, W window) { - StateNamespace namespace = StateNamespaces.window(windowCoder, window); - BagState bagState = - stateInternalsFactory - .stateInternalsForKey(key) - .state(namespace, StateTags.bag(userStateId, valueCoder)); - bagState.clear(); - } - }; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternals.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternals.java deleted file mode 100644 index f8530936789f..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternals.java +++ /dev/null @@ -1,1131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.Serializable; -import java.lang.ref.SoftReference; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateInternalsFactory; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateTag; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.state.SamzaMapState; -import org.apache.beam.runners.samza.state.SamzaSetState; -import org.apache.beam.runners.samza.transforms.UpdatingCombineFn; -import org.apache.beam.sdk.coders.BooleanCoder; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.InstantCoder; -import org.apache.beam.sdk.coders.VoidCoder; -import org.apache.beam.sdk.state.BagState; -import org.apache.beam.sdk.state.CombiningState; -import org.apache.beam.sdk.state.MapState; -import org.apache.beam.sdk.state.MultimapState; -import org.apache.beam.sdk.state.OrderedListState; -import org.apache.beam.sdk.state.ReadableState; -import org.apache.beam.sdk.state.ReadableStates; -import org.apache.beam.sdk.state.SetState; -import org.apache.beam.sdk.state.State; -import org.apache.beam.sdk.state.StateContext; -import org.apache.beam.sdk.state.StateContexts; -import org.apache.beam.sdk.state.ValueState; -import org.apache.beam.sdk.state.WatermarkHoldState; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.CombineWithContext; -import org.apache.beam.sdk.transforms.SerializableFunction; -import org.apache.beam.sdk.transforms.windowing.TimestampCombiner; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.UserStateReference; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.primitives.Ints; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.primitives.UnsignedBytes; -import org.apache.samza.config.Config; -import org.apache.samza.context.TaskContext; -import org.apache.samza.serializers.Serde; -import org.apache.samza.serializers.SerdeFactory; -import org.apache.samza.storage.kv.Entry; -import org.apache.samza.storage.kv.KeyValueIterator; -import org.apache.samza.storage.kv.KeyValueStore; -import org.checkerframework.checker.initialization.qual.Initialized; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.nullness.qual.UnknownKeyFor; -import org.joda.time.Instant; - -/** {@link StateInternals} that uses Samza local {@link KeyValueStore} to manage state. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "keyfor", - "nullness" -}) // TODO(https://github.com/apache/beam/issues/20497) -public class SamzaStoreStateInternals implements StateInternals { - static final String BEAM_STORE = "beamStore"; - - private static final ThreadLocal> threadLocalBaos = - new ThreadLocal<>(); - - // the stores include both beamStore for system states as well as stores for user state - private final Map>> stores; - private final K key; - private final byte[] keyBytes; - private final int batchGetSize; - private final String stageId; - - private SamzaStoreStateInternals( - Map>> stores, - @Nullable K key, - byte @Nullable [] keyBytes, - String stageId, - int batchGetSize) { - this.stores = stores; - this.key = key; - this.keyBytes = keyBytes; - this.batchGetSize = batchGetSize; - this.stageId = stageId; - } - - @SuppressWarnings("unchecked") - static KeyValueStore> getBeamStore(TaskContext context) { - return (KeyValueStore>) - context.getStore(SamzaStoreStateInternals.BEAM_STORE); - } - - /** - * Creates non keyed state internal factory to persist states in {@link - * SamzaStoreStateInternals#BEAM_STORE}. - */ - static Factory createNonKeyedStateInternalsFactory( - String id, TaskContext context, SamzaPipelineOptions pipelineOptions) { - return createStateInternalsFactory(id, null, context, pipelineOptions, Collections.emptyMap()); - } - - static Factory createStateInternalsFactory( - String id, - Coder keyCoder, - TaskContext context, - SamzaPipelineOptions pipelineOptions, - ExecutableStage executableStage) { - - Map stateIdToStoreMap = - executableStage.getUserStates().stream() - .collect( - Collectors.toMap(UserStateReference::localName, UserStateReference::localName)); - - return createStateInternalsFactory(id, keyCoder, context, pipelineOptions, stateIdToStoreMap); - } - - @SuppressWarnings("unchecked") - static Factory createStateInternalsFactory( - String id, - @Nullable Coder keyCoder, - TaskContext context, - SamzaPipelineOptions pipelineOptions, - Map stateIdToStoreMap) { - final int batchGetSize = pipelineOptions.getStoreBatchGetSize(); - final Map>> stores = new HashMap<>(); - stores.put(BEAM_STORE, getBeamStore(context)); - - final Coder stateKeyCoder; - if (keyCoder != null) { - stateIdToStoreMap - .keySet() - .forEach( - stateId -> - stores.put( - stateId, - (KeyValueStore>) - context.getStore(stateIdToStoreMap.get(stateId)))); - stateKeyCoder = keyCoder; - } else { - stateKeyCoder = (Coder) VoidCoder.of(); - } - return new Factory<>(Objects.toString(id), stores, stateKeyCoder, batchGetSize); - } - - @Override - public K getKey() { - return key; - } - - @Override - public T state(StateNamespace stateNamespace, StateTag stateTag) { - return state(stateNamespace, stateTag, StateContexts.nullContext()); - } - - @Override - public V state( - StateNamespace namespace, StateTag address, StateContext stateContext) { - return address.bind( - new StateTag.StateBinder() { - @Override - public ValueState bindValue(StateTag> spec, Coder coder) { - return new SamzaValueState<>(namespace, address, coder); - } - - @Override - public BagState bindBag(StateTag> spec, Coder elemCoder) { - return new SamzaBagState<>(namespace, address, elemCoder); - } - - @Override - public SetState bindSet(StateTag> spec, Coder elemCoder) { - return new SamzaSetStateImpl<>(namespace, address, elemCoder); - } - - @Override - public MapState bindMap( - StateTag> spec, - Coder mapKeyCoder, - Coder mapValueCoder) { - return new SamzaMapStateImpl<>(namespace, address, mapKeyCoder, mapValueCoder); - } - - @Override - public MultimapState bindMultimap( - StateTag> spec, - Coder keyCoder, - Coder valueCoder) { - throw new UnsupportedOperationException( - String.format("%s is not supported", MultimapState.class.getSimpleName())); - } - - @Override - public OrderedListState bindOrderedList( - StateTag> spec, Coder elemCoder) { - throw new UnsupportedOperationException( - String.format("%s is not supported", OrderedListState.class.getSimpleName())); - } - - @Override - public - CombiningState bindCombiningValue( - StateTag> spec, - Coder accumCoder, - Combine.CombineFn combineFn) { - return new SamzaAccumulatorCombiningState<>(namespace, address, accumCoder, combineFn); - } - - @Override - public - CombiningState bindCombiningValueWithContext( - StateTag> spec, - Coder accumCoder, - CombineWithContext.CombineFnWithContext combineFn) { - throw new UnsupportedOperationException( - String.format("%s is not supported", CombiningState.class.getSimpleName())); - } - - @Override - public WatermarkHoldState bindWatermark( - StateTag spec, TimestampCombiner timestampCombiner) { - return new SamzaWatermarkHoldState(namespace, address, timestampCombiner); - } - }); - } - - /** Reuse the ByteArrayOutputStream buffer. */ - private static ByteArrayOutputStream getThreadLocalBaos() { - final SoftReference refBaos = threadLocalBaos.get(); - ByteArrayOutputStream baos = refBaos == null ? null : refBaos.get(); - if (baos == null) { - baos = new ByteArrayOutputStream(); - threadLocalBaos.set(new SoftReference<>(baos)); - } - - baos.reset(); - return baos; - } - - /** Factory class to create {@link SamzaStoreStateInternals}. */ - public static class Factory implements StateInternalsFactory { - private final String stageId; - private final Map>> stores; - private final Coder keyCoder; - private final int batchGetSize; - - public Factory( - String stageId, - Map>> stores, - Coder keyCoder, - int batchGetSize) { - this.stageId = stageId; - this.stores = stores; - this.keyCoder = keyCoder; - this.batchGetSize = batchGetSize; - } - - @Override - public StateInternals stateInternalsForKey(@Nullable K key) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final DataOutputStream dos = new DataOutputStream(baos); - - try { - if (key != null) { - keyCoder.encode(key, baos); - } - final byte[] keyBytes = baos.toByteArray(); - baos.reset(); - - dos.write(keyBytes.length); - dos.write(keyBytes); - } catch (IOException e) { - throw new RuntimeException("Cannot encode key for state store", e); - } - - return new SamzaStoreStateInternals<>(stores, key, baos.toByteArray(), stageId, batchGetSize); - } - } - - private abstract class AbstractSamzaState { - private final StateNamespace namespace; - private final String addressId; - private final boolean isBeamStore; - private final String stageId; - private final byte[] keyBytes; - private byte[] encodedStoreKey; - protected final Coder coder; - protected final KeyValueStore> store; - - @SuppressWarnings({"unchecked", "rawtypes"}) - protected AbstractSamzaState( - StateNamespace namespace, StateTag address, Coder coder) { - this.coder = coder; - this.namespace = namespace; - this.addressId = address.getId(); - this.isBeamStore = !stores.containsKey(address.getId()); - this.store = - isBeamStore - ? (KeyValueStore) stores.get(BEAM_STORE) - : (KeyValueStore) stores.get(address.getId()); - this.stageId = SamzaStoreStateInternals.this.stageId; - this.keyBytes = SamzaStoreStateInternals.this.keyBytes; - } - - protected void clearInternal() { - store.delete(getEncodedStoreKey()); - } - - protected void writeInternal(T value) { - store.put(getEncodedStoreKey(), StateValue.of(value, coder)); - } - - protected T readInternal() { - final StateValue stateValue = store.get(getEncodedStoreKey()); - return decodeValue(stateValue); - } - - protected ReadableState isEmptyInternal() { - return new ReadableState() { - @Override - public Boolean read() { - return store.get(getEncodedStoreKey()) == null; - } - - @Override - public ReadableState readLater() { - return this; - } - }; - } - - protected ByteArray getEncodedStoreKey() { - return ByteArray.of(getEncodedStoreKeyBytes()); - } - - protected byte[] getEncodedStoreKeyBytes() { - if (encodedStoreKey == null) { - final ByteArrayOutputStream baos = getThreadLocalBaos(); - try (DataOutputStream dos = new DataOutputStream(baos)) { - dos.write(keyBytes); - dos.writeUTF(namespace.stringKey()); - - if (isBeamStore) { - // for system state, we need to differentiate based on the following: - dos.writeUTF(stageId); - dos.writeUTF(addressId); - } - } catch (IOException e) { - throw new RuntimeException("Could not encode full address for state: " + addressId, e); - } - this.encodedStoreKey = baos.toByteArray(); - } - return encodedStoreKey; - } - - protected T decodeValue(StateValue stateValue) { - return stateValue == null ? null : stateValue.getValue(coder); - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof SamzaStoreStateInternals.AbstractSamzaState)) { - return false; - } - - @SuppressWarnings("unchecked") - final AbstractSamzaState that = (AbstractSamzaState) o; - if (isBeamStore || that.isBeamStore) { - if (!isBeamStore || !that.isBeamStore || !stageId.equals(that.stageId)) { - return false; - } - } - return Arrays.equals(keyBytes, that.keyBytes) - && addressId.equals(that.addressId) - && this.namespace.equals(that.namespace); - } - - @Override - public int hashCode() { - int result = namespace.hashCode(); - result = 31 * result + Arrays.hashCode(getEncodedStoreKeyBytes()); - return result; - } - } - - private class SamzaValueState extends AbstractSamzaState implements ValueState { - private SamzaValueState( - StateNamespace namespace, StateTag address, Coder coder) { - super(namespace, address, coder); - } - - @Override - public void write(T input) { - writeInternal(input); - } - - @Override - public T read() { - return readInternal(); - } - - @Override - public ValueState readLater() { - return this; - } - - @Override - public void clear() { - clearInternal(); - } - } - - private class SamzaBagState extends AbstractSamzaState implements BagState { - - private SamzaBagState( - StateNamespace namespace, StateTag address, Coder coder) { - super(namespace, address, coder); - } - - @Override - public void add(T value) { - synchronized (store) { - final int size = getSize(); - final ByteArray encodedKey = encodeKey(size); - store.put(encodedKey, StateValue.of(value, coder)); - store.put(getEncodedStoreKey(), StateValue.of(Ints.toByteArray(size + 1))); - } - } - - @Override - public ReadableState isEmpty() { - synchronized (store) { - return isEmptyInternal(); - } - } - - @Override - @Nonnull - public List read() { - synchronized (store) { - final int size = getSize(); - if (size == 0) { - return Collections.emptyList(); - } - - final List values = new ArrayList<>(size); - final List keys = new ArrayList<>(size); - int start = 0; - while (start < size) { - final int end = Math.min(size, start + batchGetSize); - for (int i = start; i < end; i++) { - keys.add(encodeKey(i)); - } - store.getAll(keys).values().forEach(value -> values.add(decodeValue(value))); - - start += batchGetSize; - keys.clear(); - } - return values; - } - } - - @Override - public BagState readLater() { - return this; - } - - @Override - public void clear() { - synchronized (store) { - final int size = getSize(); - if (size != 0) { - final List keys = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - keys.add(encodeKey(i)); - } - store.deleteAll(keys); - store.delete(getEncodedStoreKey()); - } - } - } - - private int getSize() { - final StateValue stateSize = store.get(getEncodedStoreKey()); - return (stateSize == null || stateSize.valueBytes == null) - ? 0 - : Ints.fromByteArray(stateSize.valueBytes); - } - - private ByteArray encodeKey(int size) { - final ByteArrayOutputStream baos = getThreadLocalBaos(); - try (DataOutputStream dos = new DataOutputStream(baos)) { - dos.write(getEncodedStoreKeyBytes()); - dos.writeInt(size); - return ByteArray.of(baos.toByteArray()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - private class SamzaSetStateImpl implements SamzaSetState { - private final SamzaMapStateImpl mapState; - - private SamzaSetStateImpl( - StateNamespace namespace, StateTag address, Coder coder) { - mapState = new SamzaMapStateImpl<>(namespace, address, coder, BooleanCoder.of()); - } - - @Override - public ReadableState contains(T t) { - return mapState.get(t); - } - - @Override - public @Nullable ReadableState addIfAbsent(T t) { - return mapState.putIfAbsent(t, true); - } - - @Override - public void remove(T t) { - mapState.remove(t); - } - - @Override - public void add(T value) { - mapState.put(value, true); - } - - @Override - public ReadableState isEmpty() { - return new ReadableState() { - - @Override - public Boolean read() { - return Iterables.isEmpty(mapState.entries().read()); - } - - @Override - public ReadableState readLater() { - return this; - } - }; - } - - @Override - public Iterable read() { - return mapState.keys().read(); - } - - @Override - public SetState readLater() { - return this; - } - - @Override - public void clear() { - mapState.clear(); - } - - @Override - public ReadableState> readIterator() { - final Iterator> iter = mapState.readIterator().read(); - return new ReadableState>() { - @Nullable - @Override - public Iterator read() { - return new Iterator() { - @Override - public boolean hasNext() { - return iter.hasNext(); - } - - @Override - public T next() { - return iter.next().getKey(); - } - }; - } - - @Override - public ReadableState> readLater() { - return this; - } - }; - } - - @Override - public void closeIterators() { - mapState.closeIterators(); - } - } - - private class SamzaMapStateImpl extends AbstractSamzaState - implements SamzaMapState { - - private final Coder keyCoder; - private final int storeKeySize; - private final List>> openIterators = - Collections.synchronizedList(new ArrayList<>()); - - private int maxKeySize; - - protected SamzaMapStateImpl( - StateNamespace namespace, - StateTag address, - Coder keyCoder, - Coder valueCoder) { - super(namespace, address, valueCoder); - - this.keyCoder = keyCoder; - this.storeKeySize = getEncodedStoreKeyBytes().length; - // initial max key size is around 100k, so we can restore timer keys - this.maxKeySize = this.storeKeySize + 100_000; - } - - @Override - public void put(KeyT key, ValueT value) { - final ByteArray encodedKey = encodeKey(key); - maxKeySize = Math.max(maxKeySize, encodedKey.getValue().length); - store.put(encodedKey, StateValue.of(value, coder)); - } - - @Override - public @Nullable ReadableState computeIfAbsent( - KeyT key, Function mappingFunction) { - final ByteArray encodedKey = encodeKey(key); - final ValueT current = decodeValue(store.get(encodedKey)); - if (current == null) { - put(key, mappingFunction.apply(key)); - } - - return current == null ? null : ReadableStates.immediate(current); - } - - @Override - public void remove(KeyT key) { - store.delete(encodeKey(key)); - } - - @Override - public ReadableState get(KeyT key) { - return getOrDefault(key, null); - } - - @Override - public @UnknownKeyFor @NonNull @Initialized ReadableState getOrDefault( - KeyT key, @Nullable ValueT defaultValue) { - return new ReadableState() { - @Override - public @Nullable ValueT read() { - ValueT value = decodeValue(store.get(encodeKey(key))); - return value != null ? value : defaultValue; - } - - @Override - public @UnknownKeyFor @NonNull @Initialized ReadableState readLater() { - return this; - } - }; - } - - @Override - public ReadableState> keys() { - return new ReadableState>() { - @Override - public Iterable read() { - return createIterable(entry -> decodeKey(entry.getKey())); - } - - @Override - public ReadableState> readLater() { - return this; - } - }; - } - - @Override - public ReadableState> values() { - return new ReadableState>() { - @Override - public Iterable read() { - return createIterable(entry -> decodeValue(entry.getValue())); - } - - @Override - public ReadableState> readLater() { - return this; - } - }; - } - - @Override - public ReadableState>> entries() { - return new ReadableState>>() { - @Override - public Iterable> read() { - return createIterable( - entry -> - new AbstractMap.SimpleEntry<>( - decodeKey(entry.getKey()), decodeValue(entry.getValue()))); - } - - @Override - public ReadableState>> readLater() { - return this; - } - }; - } - - @Override - public @UnknownKeyFor @NonNull @Initialized ReadableState< - @UnknownKeyFor @NonNull @Initialized Boolean> - isEmpty() { - ReadableState> keys = this.keys(); - return new ReadableState() { - @Override - public @Nullable Boolean read() { - return Iterables.isEmpty(keys.read()); - } - - @Override - public @UnknownKeyFor @NonNull @Initialized ReadableState readLater() { - keys.readLater(); - return this; - } - }; - } - - @Override - public ReadableState>> readIterator() { - final ByteArray maxKey = createMaxKey(); - final KeyValueIterator> kvIter = - store.range(getEncodedStoreKey(), maxKey); - openIterators.add(kvIter); - - return new ReadableState>>() { - @Nullable - @Override - public Iterator> read() { - return new Iterator>() { - @Override - public boolean hasNext() { - boolean hasNext = kvIter.hasNext(); - if (!hasNext) { - kvIter.close(); - openIterators.remove(kvIter); - } - return hasNext; - } - - @Override - public Map.Entry next() { - Entry> entry = kvIter.next(); - return new AbstractMap.SimpleEntry<>( - decodeKey(entry.getKey()), decodeValue(entry.getValue())); - } - }; - } - - @Override - public ReadableState>> readLater() { - return this; - } - }; - } - - /** - * Since we are not able to track the instances of the iterators created here and close them - * properly, we need to load the content into memory. - */ - private Iterable createIterable( - SerializableFunction< - org.apache.samza.storage.kv.Entry>, OutputT> - fn) { - final ByteArray maxKey = createMaxKey(); - final KeyValueIterator> kvIter = - store.range(getEncodedStoreKey(), maxKey); - final List>> iterable = ImmutableList.copyOf(kvIter); - kvIter.close(); - - return new Iterable() { - @Override - public Iterator iterator() { - final Iterator>> iter = iterable.iterator(); - - return new Iterator() { - @Override - public boolean hasNext() { - return iter.hasNext(); - } - - @Override - public OutputT next() { - return fn.apply(iter.next()); - } - }; - } - }; - } - - @Override - public void clear() { - final ByteArray maxKey = createMaxKey(); - final KeyValueIterator> kvIter = - store.range(getEncodedStoreKey(), maxKey); - while (kvIter.hasNext()) { - store.delete(kvIter.next().getKey()); - } - kvIter.close(); - } - - private ByteArray encodeKey(KeyT key) { - try { - final ByteArrayOutputStream baos = getThreadLocalBaos(); - baos.write(getEncodedStoreKeyBytes()); - keyCoder.encode(key, baos); - return ByteArray.of(baos.toByteArray()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private KeyT decodeKey(ByteArray keyBytes) { - try { - final byte[] realKey = - Arrays.copyOfRange(keyBytes.value, storeKeySize, keyBytes.value.length); - return keyCoder.decode(new ByteArrayInputStream(realKey)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private ByteArray createMaxKey() { - byte[] maxKey = new byte[maxKeySize]; - Arrays.fill(maxKey, (byte) 0xff); - - final byte[] encodedKey = getEncodedStoreKeyBytes(); - System.arraycopy(encodedKey, 0, maxKey, 0, encodedKey.length); - return ByteArray.of(maxKey); - } - - @Override - public void closeIterators() { - openIterators.forEach(KeyValueIterator::close); - openIterators.clear(); - } - } - - private class SamzaAccumulatorCombiningState extends AbstractSamzaState - implements CombiningState { - - private final Combine.CombineFn combineFn; - - protected SamzaAccumulatorCombiningState( - StateNamespace namespace, - StateTag address, - Coder coder, - Combine.CombineFn combineFn) { - super(namespace, address, coder); - - this.combineFn = combineFn; - } - - @Override - public void clear() { - clearInternal(); - } - - @Override - public void add(InT value) { - final AccumT accum = getAccum(); - final AccumT current = combineFn.addInput(accum, value); - writeInternal(current); - } - - @Override - public ReadableState isEmpty() { - return isEmptyInternal(); - } - - @Override - public AccumT getAccum() { - final AccumT accum = readInternal(); - return accum != null ? accum : combineFn.createAccumulator(); - } - - @Override - public void addAccum(AccumT accum) { - final AccumT currentAccum = getAccum(); - final AccumT mergedAccum = mergeAccumulators(Arrays.asList(currentAccum, accum)); - writeInternal(mergedAccum); - } - - @Override - public AccumT mergeAccumulators(Iterable accumulators) { - return combineFn.mergeAccumulators(accumulators); - } - - @Override - public CombiningState readLater() { - return this; - } - - @Override - @Nonnull - public OutT read() { - AccumT accum = getAccum(); - OutT output = combineFn.extractOutput(accum); - if (combineFn instanceof UpdatingCombineFn) { - AccumT updatedAccum = - ((UpdatingCombineFn) combineFn).updateAfterFiring(accum); - writeInternal(updatedAccum); - } - return output; - } - } - - private class SamzaWatermarkHoldState extends AbstractSamzaState - implements WatermarkHoldState { - - private final TimestampCombiner timestampCombiner; - - public SamzaWatermarkHoldState( - StateNamespace namespace, StateTag address, TimestampCombiner timestampCombiner) { - super(namespace, address, InstantCoder.of()); - this.timestampCombiner = timestampCombiner; - } - - @Override - public void add(Instant value) { - final Instant currentValue = readInternal(); - final Instant combinedValue = - currentValue == null ? value : timestampCombiner.combine(currentValue, value); - - if (!combinedValue.equals(currentValue)) { - writeInternal(combinedValue); - } - } - - @Override - public ReadableState isEmpty() { - return isEmptyInternal(); - } - - @Override - public Instant read() { - return readInternal(); - } - - @Override - public TimestampCombiner getTimestampCombiner() { - return this.timestampCombiner; - } - - @Override - public WatermarkHoldState readLater() { - return this; - } - - @Override - public void clear() { - clearInternal(); - } - } - - /** Wrapper of byte[] so it can used as key in the KeyValueStore for caching. */ - public static class ByteArray implements Serializable, Comparable { - - private final byte[] value; - - public static ByteArray of(byte[] value) { - return new ByteArray(value); - } - - private ByteArray(byte[] value) { - this.value = value; - } - - public byte[] getValue() { - return value; - } - - @Override - public boolean equals(@Nullable Object o) { - if (!(o instanceof ByteArray)) { - return false; - } - ByteArray byteArray = (ByteArray) o; - return Arrays.equals(value, byteArray.value); - } - - @Override - public int hashCode() { - return value != null ? Arrays.hashCode(value) : 0; - } - - @Override - public int compareTo(ByteArray other) { - return UnsignedBytes.lexicographicalComparator().compare(value, other.value); - } - } - - /** Factory class to provide {@link ByteArraySerde}. */ - public static class ByteArraySerdeFactory implements SerdeFactory { - - @Override - public Serde getSerde(String name, Config config) { - return new ByteArraySerde(); - } - - /** Serde for {@link ByteArray}. */ - public static class ByteArraySerde implements Serde { - - @Override - public byte[] toBytes(ByteArray byteArray) { - return byteArray.value; - } - - @Override - public ByteArray fromBytes(byte[] bytes) { - return ByteArray.of(bytes); - } - } - } - - /** - * Wrapper for state value so that unencoded value can be read directly from the cache of - * KeyValueStore. - */ - public static class StateValue implements Serializable { - private T value; - private Coder valueCoder; - private byte[] valueBytes; - - private StateValue(T value, Coder valueCoder, byte[] valueBytes) { - this.value = value; - this.valueCoder = valueCoder; - this.valueBytes = valueBytes; - } - - public static StateValue of(T value, Coder valueCoder) { - return new StateValue<>(value, valueCoder, null); - } - - public static StateValue of(byte[] valueBytes) { - return new StateValue<>(null, null, valueBytes); - } - - public T getValue(Coder coder) { - if (value == null && valueBytes != null) { - if (valueCoder == null) { - valueCoder = coder; - } - try { - value = valueCoder.decode(new ByteArrayInputStream(valueBytes)); - } catch (IOException e) { - throw new RuntimeException("Could not decode state", e); - } - } - return value; - } - - public byte[] getValueBytes() { - if (valueBytes == null && value != null) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - valueCoder.encode(value, baos); - } catch (IOException e) { - throw new RuntimeException("Could not encode state value: " + value, e); - } - valueBytes = baos.toByteArray(); - } - return valueBytes; - } - } - - /** Factory class to provide {@link StateValueSerdeFactory.StateValueSerde}. */ - public static class StateValueSerdeFactory implements SerdeFactory> { - @Override - public Serde> getSerde(String name, Config config) { - return new StateValueSerde(); - } - - /** Serde for {@link StateValue}. */ - public static class StateValueSerde implements Serde> { - @Override - public StateValue fromBytes(byte[] bytes) { - return StateValue.of(bytes); - } - - @Override - public byte[] toBytes(StateValue stateValue) { - return stateValue == null ? null : stateValue.getValueBytes(); - } - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactory.java deleted file mode 100644 index bd6547b805a4..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactory.java +++ /dev/null @@ -1,733 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import com.google.auto.value.AutoValue; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; -import java.util.TreeSet; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.StateTags; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.core.TimerInternalsFactory; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.SamzaRunner; -import org.apache.beam.runners.samza.state.SamzaMapState; -import org.apache.beam.runners.samza.state.SamzaSetState; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.CoderException; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.StructuredCoder; -import org.apache.beam.sdk.coders.VarLongCoder; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.GlobalWindow; -import org.apache.beam.sdk.values.PCollection.IsBounded; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.samza.operators.Scheduler; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link TimerInternalsFactory} that creates Samza {@link TimerInternals}. This class keeps track - * of the {@link org.apache.beam.runners.core.TimerInternals.TimerData} added to the sorted timer - * set, and removes the ready timers when the watermark is advanced. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaTimerInternalsFactory implements TimerInternalsFactory { - private static final Logger LOG = LoggerFactory.getLogger(SamzaTimerInternalsFactory.class); - private final NavigableSet> eventTimeBuffer; - private final Coder keyCoder; - private final Scheduler> timerRegistry; - private final SamzaTimerState state; - private final IsBounded isBounded; - - private Instant inputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - private Instant outputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - - // Size of each event timer is around 200B, by default with buffer size 50k, the default size is - // 10M - private final int maxEventTimerBufferSize; - // Max event time stored in eventTimerBuffer - // If it is set to long.MAX_VALUE, it indicates the State does not contain any KeyedTimerData - private long maxEventTimeInBuffer; - - // The maximum number of ready timers to process at once per watermark. - private final long maxReadyTimersToProcessOnce; - - private SamzaTimerInternalsFactory( - Coder keyCoder, - Scheduler> timerRegistry, - String timerStateId, - SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory, - Coder windowCoder, - IsBounded isBounded, - SamzaPipelineOptions pipelineOptions) { - this.keyCoder = keyCoder; - this.timerRegistry = timerRegistry; - this.eventTimeBuffer = new TreeSet<>(); - this.maxEventTimerBufferSize = - pipelineOptions.getEventTimerBufferSize(); // must be placed before state initialization - this.maxEventTimeInBuffer = Long.MAX_VALUE; - this.maxReadyTimersToProcessOnce = pipelineOptions.getMaxReadyTimersToProcessOnce(); - this.state = new SamzaTimerState(timerStateId, nonKeyedStateInternalsFactory, windowCoder); - this.isBounded = isBounded; - } - - static SamzaTimerInternalsFactory createTimerInternalFactory( - Coder keyCoder, - Scheduler> timerRegistry, - String timerStateId, - SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory, - WindowingStrategy windowingStrategy, - IsBounded isBounded, - SamzaPipelineOptions pipelineOptions) { - - final Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); - - return new SamzaTimerInternalsFactory<>( - keyCoder, - timerRegistry, - timerStateId, - nonKeyedStateInternalsFactory, - windowCoder, - isBounded, - pipelineOptions); - } - - @Override - public TimerInternals timerInternalsForKey(@Nullable K key) { - final byte[] keyBytes; - - if (keyCoder == null) { - if (key != null) { - throw new IllegalArgumentException( - String.format("Received non-null key for unkeyed timer factory. Key: %s", key)); - } - keyBytes = null; - } else { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - keyCoder.encode(key, baos); - } catch (IOException e) { - throw new RuntimeException("Could not encode key: " + key, e); - } - keyBytes = baos.toByteArray(); - } - - return new SamzaTimerInternals(keyBytes, key); - } - - public void setInputWatermark(Instant watermark) { - if (watermark.isBefore(inputWatermark)) { - throw new IllegalArgumentException("New input watermark is before current watermark"); - } - LOG.debug("Advancing input watermark from {} to {}.", inputWatermark, watermark); - inputWatermark = watermark; - } - - public void setOutputWatermark(Instant watermark) { - if (watermark.isAfter(inputWatermark)) { - LOG.debug("Clipping new output watermark from {} to {}.", watermark, inputWatermark); - watermark = inputWatermark; - } - - if (watermark.isBefore(outputWatermark)) { - throw new IllegalArgumentException("New output watermark is before current watermark"); - } - LOG.debug("Advancing output watermark from {} to {}.", outputWatermark, watermark); - outputWatermark = watermark; - } - - /** - * The method is called when watermark comes. It compares timers in memory buffer with watermark - * to prepare ready timers. When memory buffer is empty, it asks store to reload timers into - * buffer. note that the number of timers returned may be larger than memory buffer size. - * - * @return a collection of ready timers to be fired - */ - public Collection> removeReadyTimers() { - final Collection> readyTimers = new ArrayList<>(); - - while (!eventTimeBuffer.isEmpty() - && !eventTimeBuffer.first().getTimerData().getTimestamp().isAfter(inputWatermark) - && readyTimers.size() < maxReadyTimersToProcessOnce) { - - final KeyedTimerData keyedTimerData = eventTimeBuffer.pollFirst(); - readyTimers.add(keyedTimerData); - state.deletePersisted(keyedTimerData); - - if (eventTimeBuffer.isEmpty()) { - state.reloadEventTimeTimers(); - } - } - LOG.debug("Removed {} ready timers", readyTimers.size()); - - if (readyTimers.size() == maxReadyTimersToProcessOnce - && !eventTimeBuffer.isEmpty() - && eventTimeBuffer.first().getTimerData().getTimestamp().isBefore(inputWatermark)) { - LOG.warn( - "Loaded {} expired timers, the remaining will be processed at next watermark.", - maxReadyTimersToProcessOnce); - } - return readyTimers; - } - - public void removeProcessingTimer(KeyedTimerData keyedTimerData) { - state.deletePersisted(keyedTimerData); - } - - public Instant getInputWatermark() { - return inputWatermark; - } - - public Instant getOutputWatermark() { - return outputWatermark; - } - - // for unit test only - NavigableSet> getEventTimeBuffer() { - return eventTimeBuffer; - } - - private class SamzaTimerInternals implements TimerInternals { - private final byte[] keyBytes; - private final K key; - - public SamzaTimerInternals(byte[] keyBytes, K key) { - this.keyBytes = keyBytes; - this.key = key; - } - - @Override - public void setTimer( - StateNamespace namespace, - String timerId, - String timerFamilyId, - Instant target, - Instant outputTimestamp, - TimeDomain timeDomain) { - setTimer( - TimerData.of(timerId, timerFamilyId, namespace, target, outputTimestamp, timeDomain)); - } - - @Override - public void setTimer(TimerData timerData) { - if (isBounded == IsBounded.UNBOUNDED - && timerData.getTimestamp().getMillis() - > GlobalWindow.INSTANCE.maxTimestamp().getMillis()) { - // No need to register a timer greater than maxTimestamp if the input is unbounded. - // 1. It will ignore timers with (maxTimestamp + 1) created by stateful ParDo with global - // window. - // 2. It will register timers with maxTimestamp so that global window can be closed - // correctly when max watermark comes. - return; - } - - final KeyedTimerData keyedTimerData = new KeyedTimerData<>(keyBytes, key, timerData); - if (eventTimeBuffer.contains(keyedTimerData)) { - return; - } - - final Long lastTimestamp = state.get(keyedTimerData); - final Long newTimestamp = timerData.getTimestamp().getMillis(); - - if (newTimestamp.equals(lastTimestamp)) { - return; - } - - if (lastTimestamp != null) { - deleteTimer( - timerData.getNamespace(), - timerData.getTimerId(), - timerData.getTimerFamilyId(), - new Instant(lastTimestamp), - new Instant(lastTimestamp), - timerData.getDomain()); - } - - // persist it first - state.persist(keyedTimerData); - - // TO-DO: apply the same memory optimization over processing timers - switch (timerData.getDomain()) { - case EVENT_TIME: - /* - * To determine if the upcoming KeyedTimerData could be added to the Buffer while - * guaranteeing the Buffer's timestamps are all <= than those in State Store to preserve - * timestamp eviction priority: - * - *

1) If maxEventTimeInBuffer == long.MAX_VALUE, it indicates that the State is empty, - * therefore all the Event times greater or lesser than newTimestamp are in the buffer; - * - *

2) If newTimestamp < maxEventTimeInBuffer, it indicates that there are entries - * greater than newTimestamp, so it is safe to add it to the buffer - * - *

In case that the Buffer is full, we remove the largest timer from memory according - * to {@link KeyedTimerData.compareTo()} - */ - if (newTimestamp < maxEventTimeInBuffer) { - eventTimeBuffer.add(keyedTimerData); - if (eventTimeBuffer.size() > maxEventTimerBufferSize) { - eventTimeBuffer.pollLast(); - maxEventTimeInBuffer = - eventTimeBuffer.last().getTimerData().getTimestamp().getMillis(); - } - } - break; - - case PROCESSING_TIME: - timerRegistry.schedule(keyedTimerData, timerData.getTimestamp().getMillis()); - break; - - default: - throw new UnsupportedOperationException( - String.format( - "%s currently only supports even time or processing time", SamzaRunner.class)); - } - } - - /** @deprecated use {@link #deleteTimer(StateNamespace, String, String, TimeDomain)}. */ - @Override - @Deprecated - public void deleteTimer(StateNamespace namespace, String timerId, String timerFamilyId) { - deleteTimer(namespace, timerId, timerFamilyId, TimeDomain.EVENT_TIME); - } - - /** @deprecated use {@link #deleteTimer(StateNamespace, String, String, TimeDomain)}. */ - @Override - @Deprecated - public void deleteTimer(TimerData timerData) { - deleteTimer( - timerData.getNamespace(), - timerData.getTimerId(), - timerData.getTimerFamilyId(), - timerData.getDomain()); - } - - @Override - public void deleteTimer( - StateNamespace namespace, String timerId, String timerFamilyId, TimeDomain timeDomain) { - final TimerKey timerKey = TimerKey.of(key, namespace, timerId, timerFamilyId); - final Long lastTimestamp = state.get(timerKey, timeDomain); - - if (lastTimestamp == null) { - return; - } - - final Instant timestamp = Instant.ofEpochMilli(lastTimestamp); - deleteTimer(namespace, timerId, timerFamilyId, timestamp, timestamp, timeDomain); - } - - private void deleteTimer( - StateNamespace namespace, - String timerId, - String timerFamilyId, - Instant timestamp, - Instant outputTimestamp, - TimeDomain timeDomain) { - final TimerData timerData = - TimerData.of(timerId, timerFamilyId, namespace, timestamp, outputTimestamp, timeDomain); - final KeyedTimerData keyedTimerData = new KeyedTimerData<>(keyBytes, key, timerData); - - state.deletePersisted(keyedTimerData); - - switch (timerData.getDomain()) { - case EVENT_TIME: - eventTimeBuffer.remove(keyedTimerData); - break; - - case PROCESSING_TIME: - timerRegistry.delete(keyedTimerData); - break; - - default: - throw new UnsupportedOperationException( - String.format( - "%s currently only supports event time or processing time but get %s", - SamzaRunner.class, timerData.getDomain())); - } - } - - @Override - public Instant currentProcessingTime() { - return new Instant(); - } - - @Override - public Instant currentSynchronizedProcessingTime() { - throw new UnsupportedOperationException( - String.format( - "%s does not currently support synchronized processing time", SamzaRunner.class)); - } - - @Override - public Instant currentInputWatermarkTime() { - return inputWatermark; - } - - @Override - public Instant currentOutputWatermarkTime() { - return outputWatermark; - } - } - - private class SamzaTimerState { - private final SamzaMapState, Long> eventTimeTimerState; - private final SamzaSetState> timestampSortedEventTimeTimerState; - private final SamzaMapState, Long> processingTimeTimerState; - - SamzaTimerState( - String timerStateId, - SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory, - Coder windowCoder) { - - this.eventTimeTimerState = - (SamzaMapState, Long>) - nonKeyedStateInternalsFactory - .stateInternalsForKey(null) - .state( - StateNamespaces.global(), - StateTags.map( - timerStateId + "-et", - new TimerKeyCoder<>(keyCoder, windowCoder), - VarLongCoder.of())); - - this.timestampSortedEventTimeTimerState = - (SamzaSetState>) - nonKeyedStateInternalsFactory - .stateInternalsForKey(null) - .state( - StateNamespaces.global(), - StateTags.set( - timerStateId + "-ts", - new KeyedTimerData.KeyedTimerDataCoder<>(keyCoder, windowCoder))); - - this.processingTimeTimerState = - (SamzaMapState, Long>) - nonKeyedStateInternalsFactory - .stateInternalsForKey(null) - .state( - StateNamespaces.global(), - StateTags.map( - timerStateId + "-pt", - new TimerKeyCoder<>(keyCoder, windowCoder), - VarLongCoder.of())); - - init(); - } - - Long get(KeyedTimerData keyedTimerData) { - return get(TimerKey.of(keyedTimerData), keyedTimerData.getTimerData().getDomain()); - } - - Long get(TimerKey key, TimeDomain domain) { - switch (domain) { - case EVENT_TIME: - return eventTimeTimerState.get(key).read(); - - case PROCESSING_TIME: - return processingTimeTimerState.get(key).read(); - - default: - throw new UnsupportedOperationException( - String.format( - "%s currently only supports event time or processing time but get %s", - SamzaRunner.class, domain)); - } - } - - void persist(KeyedTimerData keyedTimerData) { - final TimerKey timerKey = TimerKey.of(keyedTimerData); - switch (keyedTimerData.getTimerData().getDomain()) { - case EVENT_TIME: - final Long timestamp = eventTimeTimerState.get(timerKey).read(); - - if (timestamp != null) { - final KeyedTimerData keyedTimerDataInStore = - TimerKey.toKeyedTimerData(timerKey, timestamp, TimeDomain.EVENT_TIME, keyCoder); - timestampSortedEventTimeTimerState.remove(keyedTimerDataInStore); - } - eventTimeTimerState.put( - timerKey, keyedTimerData.getTimerData().getTimestamp().getMillis()); - - timestampSortedEventTimeTimerState.add(keyedTimerData); - - break; - - case PROCESSING_TIME: - processingTimeTimerState.put( - timerKey, keyedTimerData.getTimerData().getTimestamp().getMillis()); - break; - - default: - throw new UnsupportedOperationException( - String.format( - "%s currently only supports event time or processing time but get %s", - SamzaRunner.class, keyedTimerData.getTimerData().getDomain())); - } - } - - void deletePersisted(KeyedTimerData keyedTimerData) { - final TimerKey timerKey = TimerKey.of(keyedTimerData); - switch (keyedTimerData.getTimerData().getDomain()) { - case EVENT_TIME: - eventTimeTimerState.remove(timerKey); - timestampSortedEventTimeTimerState.remove(keyedTimerData); - break; - - case PROCESSING_TIME: - processingTimeTimerState.remove(timerKey); - break; - - default: - throw new UnsupportedOperationException( - String.format( - "%s currently only supports event time or processing time but get %s", - SamzaRunner.class, keyedTimerData.getTimerData().getDomain())); - } - } - - /** - * Reload event time timers from state to memory buffer. Buffer size is bound by - * maxEventTimerBufferSize - */ - private void reloadEventTimeTimers() { - final Iterator> iter = - timestampSortedEventTimeTimerState.readIterator().read(); - - while (iter.hasNext() && eventTimeBuffer.size() < maxEventTimerBufferSize) { - final KeyedTimerData keyedTimerData = iter.next(); - eventTimeBuffer.add(keyedTimerData); - maxEventTimeInBuffer = keyedTimerData.getTimerData().getTimestamp().getMillis(); - } - - timestampSortedEventTimeTimerState.closeIterators(); - LOG.info("Loaded {} event time timers in memory", eventTimeBuffer.size()); - - if (eventTimeBuffer.size() < maxEventTimerBufferSize) { - LOG.debug( - "Event time timers in State is empty, filled {} timers out of {} buffer capacity", - eventTimeBuffer.size(), - maxEventTimeInBuffer); - // Reset the flag variable to indicate there are no more KeyedTimerData in State - maxEventTimeInBuffer = Long.MAX_VALUE; - } - } - - private void loadProcessingTimeTimers() { - final Iterator, Long>> iter = - processingTimeTimerState.readIterator().read(); - // since the iterator will reach to the end, it will be closed automatically - int count = 0; - while (iter.hasNext()) { - final Map.Entry, Long> entry = iter.next(); - final KeyedTimerData keyedTimerData = - TimerKey.toKeyedTimerData( - entry.getKey(), entry.getValue(), TimeDomain.PROCESSING_TIME, keyCoder); - - timerRegistry.schedule( - keyedTimerData, keyedTimerData.getTimerData().getTimestamp().getMillis()); - ++count; - } - processingTimeTimerState.closeIterators(); - - LOG.info("Loaded {} processing time timers in memory", count); - } - - /** - * Restore timer state from RocksDB. This is needed for migration of existing jobs. Give events - * in eventTimeTimerState, construct timestampSortedEventTimeTimerState preparing for memory - * reloading. TO-DO: processing time timers are still loaded into memory in one shot; will apply - * the same optimization mechanism as event time timer - */ - private void init() { - final Iterator, Long>> eventTimersIter = - eventTimeTimerState.readIterator().read(); - // use hasNext to check empty, because this is relatively cheap compared with Iterators.size() - if (eventTimersIter.hasNext()) { - final Iterator sortedEventTimerIter = - timestampSortedEventTimeTimerState.readIterator().read(); - - if (!sortedEventTimerIter.hasNext()) { - // inline the migration code - while (eventTimersIter.hasNext()) { - final Map.Entry, Long> entry = eventTimersIter.next(); - final KeyedTimerData keyedTimerData = - TimerKey.toKeyedTimerData( - entry.getKey(), entry.getValue(), TimeDomain.EVENT_TIME, keyCoder); - timestampSortedEventTimeTimerState.add(keyedTimerData); - } - } - timestampSortedEventTimeTimerState.closeIterators(); - } - eventTimeTimerState.closeIterators(); - - reloadEventTimeTimers(); - loadProcessingTimeTimers(); - } - } - - @AutoValue - abstract static class TimerKey { - abstract @Nullable K getKey(); - - abstract StateNamespace getStateNamespace(); - - abstract String getTimerId(); - - abstract String getTimerFamilyId(); - - static Builder builder() { - return new AutoValue_SamzaTimerInternalsFactory_TimerKey.Builder<>(); - } - - static TimerKey of(KeyedTimerData keyedTimerData) { - final TimerInternals.TimerData timerData = keyedTimerData.getTimerData(); - return of( - keyedTimerData.getKey(), - timerData.getNamespace(), - timerData.getTimerId(), - timerData.getTimerFamilyId()); - } - - static TimerKey of( - K key, StateNamespace namespace, String timerId, String timerFamilyId) { - return TimerKey.builder() - .setKey(key) - .setStateNamespace(namespace) - .setTimerId(timerId) - .setTimerFamilyId(timerFamilyId) - .build(); - } - - static KeyedTimerData toKeyedTimerData( - TimerKey timerKey, long timestamp, TimeDomain domain, Coder keyCoder) { - byte[] keyBytes = null; - if (keyCoder != null && timerKey.getKey() != null) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - keyCoder.encode(timerKey.getKey(), baos); - } catch (IOException e) { - throw new RuntimeException("Could not encode key: " + timerKey.getKey(), e); - } - keyBytes = baos.toByteArray(); - } - - return new KeyedTimerData<>( - keyBytes, - timerKey.getKey(), - TimerInternals.TimerData.of( - timerKey.getTimerId(), - timerKey.getTimerFamilyId(), - timerKey.getStateNamespace(), - new Instant(timestamp), - new Instant(timestamp), - domain)); - } - - @AutoValue.Builder - abstract static class Builder { - abstract Builder setKey(K key); - - abstract Builder setStateNamespace(StateNamespace stateNamespace); - - abstract Builder setTimerId(String timerId); - - abstract Builder setTimerFamilyId(String timerFamilyId); - - abstract TimerKey build(); - } - } - - /** Coder for {@link TimerKey}. */ - public static class TimerKeyCoder extends StructuredCoder> { - private static final StringUtf8Coder STRING_CODER = StringUtf8Coder.of(); - - private final Coder keyCoder; - private final Coder windowCoder; - - TimerKeyCoder(Coder keyCoder, Coder windowCoder) { - this.keyCoder = keyCoder; - this.windowCoder = windowCoder; - } - - @Override - public void encode(TimerKey value, OutputStream outStream) - throws CoderException, IOException { - - // encode the timestamp first - STRING_CODER.encode(value.getTimerId(), outStream); - STRING_CODER.encode(value.getStateNamespace().stringKey(), outStream); - - if (keyCoder != null) { - keyCoder.encode(value.getKey(), outStream); - } - - STRING_CODER.encode(value.getTimerFamilyId(), outStream); - } - - @Override - public TimerKey decode(InputStream inStream) throws CoderException, IOException { - // decode the timestamp first - final String timerId = STRING_CODER.decode(inStream); - // The namespace needs two-phase deserialization: - // first from bytes into a string, then from string to namespace object using windowCoder. - final StateNamespace namespace = - StateNamespaces.fromString(STRING_CODER.decode(inStream), windowCoder); - K key = null; - if (keyCoder != null) { - key = keyCoder.decode(inStream); - } - - // check if the stream has more available bytes. This is to ensure backward compatibility with - // old rocksdb state which does not encode timer family data - final String timerFamilyId = inStream.available() > 0 ? STRING_CODER.decode(inStream) : ""; - - return TimerKey.builder() - .setTimerId(timerId) - .setStateNamespace(namespace) - .setKey(key) - .setTimerFamilyId(timerFamilyId) - .build(); - } - - @Override - public List> getCoderArguments() { - return Arrays.asList(keyCoder, windowCoder); - } - - @Override - public void verifyDeterministic() throws NonDeterministicException {} - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SingletonKeyedWorkItem.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SingletonKeyedWorkItem.java deleted file mode 100644 index 5a59e8616cc5..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SingletonKeyedWorkItem.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collections; -import org.apache.beam.runners.core.KeyedWorkItem; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.values.WindowedValue; - -/** Implementation of {@link KeyedWorkItem} which contains only a single value. */ -class SingletonKeyedWorkItem implements KeyedWorkItem { - private final K key; - private final WindowedValue value; - - public SingletonKeyedWorkItem(K key, WindowedValue value) { - this.key = key; - this.value = value; - } - - @Override - public K key() { - return key; - } - - @Override - public Iterable timersIterable() { - return Collections.emptyList(); - } - - @Override - public Iterable> elementsIterable() { - return Collections.singletonList(value); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SplittableParDoProcessKeyedElementsOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SplittableParDoProcessKeyedElementsOp.java deleted file mode 100644 index f7886c95f123..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SplittableParDoProcessKeyedElementsOp.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.core.DoFnRunners; -import org.apache.beam.runners.core.KeyedWorkItem; -import org.apache.beam.runners.core.KeyedWorkItems; -import org.apache.beam.runners.core.NullSideInputReader; -import org.apache.beam.runners.core.OutputAndTimeBoundedSplittableProcessElementInvoker; -import org.apache.beam.runners.core.SplittableParDoViaKeyedWorkItems; -import org.apache.beam.runners.core.SplittableParDoViaKeyedWorkItems.ProcessElements; -import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateInternalsFactory; -import org.apache.beam.runners.core.StepContext; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.core.TimerInternals.TimerData; -import org.apache.beam.runners.core.construction.SerializablePipelineOptions; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.sdk.coders.ByteArrayCoder; -import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.join.RawUnionValue; -import org.apache.beam.sdk.transforms.reflect.DoFnInvokers; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.WindowedValueMultiReceiver; -import org.apache.beam.sdk.util.construction.SplittableParDo; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection.IsBounded; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; -import org.apache.samza.config.Config; -import org.apache.samza.context.Context; -import org.apache.samza.operators.Scheduler; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Samza operator for {@link org.apache.beam.sdk.transforms.GroupByKey}. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SplittableParDoProcessKeyedElementsOp< - InputT, OutputT, RestrictionT, PositionT, WatermarkEstimatorStateT> - implements Op>, RawUnionValue, byte[]> { - private static final Logger LOG = - LoggerFactory.getLogger(SplittableParDoProcessKeyedElementsOp.class); - private static final String TIMER_STATE_ID = "timer"; - - private final TupleTag mainOutputTag; - private final WindowingStrategy windowingStrategy; - private final OutputManagerFactory outputManagerFactory; - private final SplittableParDoViaKeyedWorkItems.ProcessElements< - InputT, OutputT, RestrictionT, PositionT, WatermarkEstimatorStateT> - processElements; - private final String transformId; - private final IsBounded isBounded; - - private transient StateInternalsFactory stateInternalsFactory; - private transient SamzaTimerInternalsFactory timerInternalsFactory; - private transient DoFnRunner>, OutputT> fnRunner; - private transient SamzaPipelineOptions pipelineOptions; - private transient @MonotonicNonNull ScheduledExecutorService ses = null; - - public SplittableParDoProcessKeyedElementsOp( - TupleTag mainOutputTag, - SplittableParDo.ProcessKeyedElements - processKeyedElements, - WindowingStrategy windowingStrategy, - OutputManagerFactory outputManagerFactory, - String transformFullName, - String transformId, - IsBounded isBounded) { - this.mainOutputTag = mainOutputTag; - this.windowingStrategy = windowingStrategy; - this.outputManagerFactory = outputManagerFactory; - this.transformId = transformId; - this.isBounded = isBounded; - - this.processElements = new ProcessElements<>(processKeyedElements); - } - - @Override - public void open( - Config config, - Context context, - Scheduler> timerRegistry, - OpEmitter emitter) { - this.pipelineOptions = - Base64Serializer.deserializeUnchecked( - config.get("beamPipelineOptions"), SerializablePipelineOptions.class) - .get() - .as(SamzaPipelineOptions.class); - - final SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory = - SamzaStoreStateInternals.createNonKeyedStateInternalsFactory( - transformId, context.getTaskContext(), pipelineOptions); - - final WindowedValueMultiReceiver outputManager = outputManagerFactory.create(emitter); - - this.stateInternalsFactory = - new SamzaStoreStateInternals.Factory<>( - transformId, - Collections.singletonMap( - SamzaStoreStateInternals.BEAM_STORE, - SamzaStoreStateInternals.getBeamStore(context.getTaskContext())), - ByteArrayCoder.of(), - pipelineOptions.getStoreBatchGetSize()); - - this.timerInternalsFactory = - SamzaTimerInternalsFactory.createTimerInternalFactory( - ByteArrayCoder.of(), - timerRegistry, - TIMER_STATE_ID, - nonKeyedStateInternalsFactory, - windowingStrategy, - isBounded, - pipelineOptions); - - if (this.ses == null) { - this.ses = - Executors.newSingleThreadScheduledExecutor( - new ThreadFactoryBuilder().setNameFormat("samza-sdf-executor-%d").build()); - } - - final KeyedInternals keyedInternals = - new KeyedInternals<>(stateInternalsFactory, timerInternalsFactory); - - SplittableParDoViaKeyedWorkItems.ProcessFn< - InputT, OutputT, RestrictionT, PositionT, WatermarkEstimatorStateT> - processFn = processElements.newProcessFn(processElements.getFn()); - DoFnInvokers.tryInvokeSetupFor(processFn, pipelineOptions); - processFn.setStateInternalsFactory(stateInternalsFactory); - processFn.setTimerInternalsFactory(timerInternalsFactory); - processFn.setSideInputReader(NullSideInputReader.empty()); - processFn.setProcessElementInvoker( - new OutputAndTimeBoundedSplittableProcessElementInvoker<>( - processElements.getFn(), - pipelineOptions, - outputManager, - mainOutputTag, - NullSideInputReader.empty(), - ses, - 10000, - Duration.standardSeconds(10), - () -> { - throw new UnsupportedOperationException("BundleFinalizer unsupported in Samza"); - })); - - final StepContext stepContext = - new StepContext() { - @Override - public StateInternals stateInternals() { - return keyedInternals.stateInternals(); - } - - @Override - public TimerInternals timerInternals() { - return keyedInternals.timerInternals(); - } - }; - - this.fnRunner = - DoFnRunners.simpleRunner( - pipelineOptions, - processFn, - NullSideInputReader.of(Collections.emptyList()), - outputManager, - mainOutputTag, - Collections.emptyList(), - stepContext, - null, - Collections.emptyMap(), - windowingStrategy, - DoFnSchemaInformation.create(), - Collections.emptyMap()); - } - - @Override - public void processElement( - WindowedValue>> inputElement, - OpEmitter emitter) { - fnRunner.startBundle(); - fnRunner.processElement(inputElement); - fnRunner.finishBundle(); - } - - @Override - public void processWatermark(Instant watermark, OpEmitter emitter) { - timerInternalsFactory.setInputWatermark(watermark); - - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - if (!readyTimers.isEmpty()) { - fnRunner.startBundle(); - for (KeyedTimerData keyedTimerData : readyTimers) { - fireTimer(keyedTimerData.getKey(), keyedTimerData.getTimerData()); - } - fnRunner.finishBundle(); - } - - if (timerInternalsFactory.getOutputWatermark() == null - || timerInternalsFactory.getOutputWatermark().isBefore(watermark)) { - timerInternalsFactory.setOutputWatermark(watermark); - emitter.emitWatermark(timerInternalsFactory.getOutputWatermark()); - } - } - - @Override - public void processTimer( - KeyedTimerData keyedTimerData, OpEmitter emitter) { - fnRunner.startBundle(); - fireTimer(keyedTimerData.getKey(), keyedTimerData.getTimerData()); - fnRunner.finishBundle(); - - timerInternalsFactory.removeProcessingTimer(keyedTimerData); - } - - private void fireTimer(byte[] key, TimerData timer) { - LOG.debug("Firing timer {} for key {}", timer, key); - fnRunner.processElement( - WindowedValues.valueInGlobalWindow( - KeyedWorkItems.timersWorkItem(key, Collections.singletonList(timer)))); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/WindowAssignOp.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/WindowAssignOp.java deleted file mode 100644 index cd83f490bc25..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/WindowAssignOp.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.Collection; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.WindowFn; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; - -/** Samza operator for {@link org.apache.beam.sdk.transforms.windowing.Window.Assign}. */ -public class WindowAssignOp implements Op { - private final WindowFn windowFn; - - public WindowAssignOp(WindowFn windowFn) { - this.windowFn = windowFn; - } - - @Override - public void processElement(WindowedValue inputElement, OpEmitter emitter) { - final Collection windows; - try { - windows = windowFn.assignWindows(new SamzaAssignContext<>(windowFn, inputElement)); - } catch (Exception e) { - throw new RuntimeException(e); - } - - windows.stream() - .map( - window -> - WindowedValues.of( - inputElement.getValue(), - inputElement.getTimestamp(), - window, - inputElement.getPaneInfo())) - .forEach(outputElement -> emitter.emitElement(outputElement)); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/package-info.java deleted file mode 100644 index 52fb79321a38..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.runtime; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaMapState.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaMapState.java deleted file mode 100644 index a8741fbc8625..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaMapState.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.state; - -import java.util.Iterator; -import java.util.Map; -import org.apache.beam.sdk.state.MapState; -import org.apache.beam.sdk.state.ReadableState; - -/** Samza's extended MapState, allowing extra access methods to the state. */ -public interface SamzaMapState extends MapState { - - /** - * Returns an iterator from the current map state. Note this is different from the iterable - * implementation in {@link MapState#entries()}}, where we load the entries into memory and return - * iterable from that. To handle large state that doesn't fit in memory, we also need this method - * so it's possible to iterate on large data set and close the iterator when not needed. - * - * @return a {@link ReadableState} of an iterator - */ - ReadableState>> readIterator(); - - /** Closes the iterator returned from {@link SamzaMapState#readIterator()}. */ - void closeIterators(); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaSetState.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaSetState.java deleted file mode 100644 index 8af82fcd5b75..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/state/SamzaSetState.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.state; - -import java.util.Iterator; -import org.apache.beam.sdk.state.ReadableState; -import org.apache.beam.sdk.state.SetState; - -/** Samza's extended SetState, allowing extra access methods to the state. */ -public interface SamzaSetState extends SetState { - - /** - * Returns an iterator from the current set state. Note this is different from the iterable - * implementation in {@link SetState#read()}, where we load the entries into memory and return - * iterable from that. To handle large state that doesn't fit in memory, we also need this method - * so it's possible to iterate on large data set and close the iterator when not needed. - * - * @return a {@link ReadableState} of an iterator - */ - ReadableState> readIterator(); - - /** Closes the iterator returned from {@link SamzaSetState#readIterator()}. */ - void closeIterators(); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/state/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/state/package-info.java deleted file mode 100644 index dff50070f4ff..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/state/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.state; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/GroupWithoutRepartition.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/GroupWithoutRepartition.java deleted file mode 100644 index c0ce52e34c09..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/GroupWithoutRepartition.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.transforms; - -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PInput; -import org.apache.beam.sdk.values.POutput; - -/** - * A wrapper transform of {@link org.apache.beam.sdk.transforms.GroupByKey} or {@link - * org.apache.beam.sdk.transforms.join.CoGroupByKey} to indicate there is no repartition needed for - * Samza runner. For example: - * - *

input.apply(GroupWithoutRepartition.of(Count.perKey())); - */ -public class GroupWithoutRepartition - extends PTransform { - private final PTransform transform; - - public static - GroupWithoutRepartition of(PTransform transform) { - return new GroupWithoutRepartition<>(transform); - } - - private GroupWithoutRepartition(PTransform transform) { - this.transform = transform; - } - - @Override - @SuppressWarnings("unchecked") - public OutputT expand(InputT input) { - if (input instanceof PCollection) { - return (OutputT) ((PCollection) input).apply(transform); - } else if (input instanceof KeyedPCollectionTuple) { - return (OutputT) ((KeyedPCollectionTuple) input).apply(transform); - } else { - throw new RuntimeException( - transform.getName() - + " is not supported with " - + GroupWithoutRepartition.class.getSimpleName()); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java deleted file mode 100644 index f0a7e5e1ceaa..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.transforms; - -import org.apache.beam.sdk.transforms.Combine; - -/** - * Currently Beam only supports either throw away the accumulation or keep it after firing. This - * CombineFn allows more flexibility to update the accumulation. - */ -public abstract class UpdatingCombineFn - extends Combine.CombineFn { - - /** - * Returns an updated accumulator from the given accumulator after firing a window pane. - * - *

For efficiency, the input accumulator may be modified and returned. - */ - public abstract AccumT updateAfterFiring(AccumT accumulator); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/package-info.java deleted file mode 100644 index 292e7563081a..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.transforms; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigBuilder.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigBuilder.java deleted file mode 100644 index 79b5bac238e1..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigBuilder.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import static org.apache.samza.config.JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE; -import static org.apache.samza.config.JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE; -import static org.apache.samza.config.JobConfig.JOB_ID; -import static org.apache.samza.config.JobConfig.JOB_NAME; -import static org.apache.samza.config.TaskConfig.COMMIT_MS; -import static org.apache.samza.config.TaskConfig.GROUPER_FACTORY; -import static org.apache.samza.config.TaskConfig.MAX_CONCURRENCY; - -import java.io.File; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import org.apache.beam.repackaged.core.org.apache.commons.lang3.StringUtils; -import org.apache.beam.runners.core.construction.SerializablePipelineOptions; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.SamzaExecutionEnvironment; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.container.BeamContainerRunner; -import org.apache.beam.runners.samza.container.BeamJobCoordinatorRunner; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals; -import org.apache.beam.runners.samza.util.ConfigUtils; -import org.apache.beam.runners.samza.util.PortableConfigUtils; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.samza.config.ApplicationConfig; -import org.apache.samza.config.Config; -import org.apache.samza.config.ConfigLoaderFactory; -import org.apache.samza.config.JobCoordinatorConfig; -import org.apache.samza.config.MapConfig; -import org.apache.samza.config.ZkConfig; -import org.apache.samza.config.loaders.PropertiesConfigLoaderFactory; -import org.apache.samza.container.grouper.task.SingleContainerGrouperFactory; -import org.apache.samza.job.yarn.YarnJobFactory; -import org.apache.samza.runtime.LocalApplicationRunner; -import org.apache.samza.runtime.RemoteApplicationRunner; -import org.apache.samza.standalone.PassthroughJobCoordinatorFactory; -import org.apache.samza.storage.kv.RocksDbKeyValueStorageEngineFactory; -import org.apache.samza.zk.ZkJobCoordinatorFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Builder class to generate configs for BEAM samza runner during runtime. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class ConfigBuilder { - private static final Logger LOG = LoggerFactory.getLogger(ConfigBuilder.class); - - private static final String BEAM_STORE_FACTORY = "stores.beamStore.factory"; - private static final String APP_RUNNER_CLASS = "app.runner.class"; - private static final String YARN_PACKAGE_PATH = "yarn.package.path"; - private static final String JOB_FACTORY_CLASS = "job.factory.class"; - - private final Map config = new HashMap<>(); - private final SamzaPipelineOptions options; - - public ConfigBuilder(SamzaPipelineOptions options) { - this.options = options; - } - - public void put(String name, String property) { - config.put(name, property); - } - - public void putAll(Map properties) { - config.putAll(properties); - } - - /** Returns built configuration. */ - public Config build() { - try { - // apply framework configs - config.putAll(createSystemConfig(options, config)); - - // apply user configs - config.putAll(createUserConfig(options)); - - config.put(ApplicationConfig.APP_NAME, options.getJobName()); - config.put(ApplicationConfig.APP_ID, options.getJobInstance()); - config.put(JOB_NAME, options.getJobName()); - config.put(JOB_ID, options.getJobInstance()); - - // bundle-related configs - if (!PortableConfigUtils.isPortable(options)) { - config.putAll(createBundleConfig(options, config)); - LOG.info("Set bundle-related configs for classic mode"); - } else { - LOG.info("Skipped bundle-related configs for portable mode"); - } - - // remove config overrides before serialization (LISAMZA-15259) - options.setConfigOverride(new HashMap<>()); - config.put( - "beamPipelineOptions", - Base64Serializer.serializeUnchecked(new SerializablePipelineOptions(options))); - - validateConfigs(options, config); - - return new MapConfig(config); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @VisibleForTesting - static Map createBundleConfig( - SamzaPipelineOptions options, Map config) { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put(MAX_CONCURRENCY, String.valueOf(options.getMaxBundleSize())); - - if (options.getMaxBundleSize() > 1) { - final int threadPoolSize = ConfigUtils.asJobConfig(config).getThreadPoolSize(); - // Since Samza doesn't allow mixing bundle > 1 with multithreading tasks right now, - // we disable the task thread pool in both user and autosizing configs. - LOG.info("Remove threadPoolSize configs when maxBundleSize > 1"); - builder.put(JOB_CONTAINER_THREAD_POOL_SIZE, "0"); - builder.put(JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE, "0"); - - if (threadPoolSize > 1 && options.getNumThreadsForProcessElement() <= 1) { - // In case the user sets the thread pool through samza config instead options, - // set the bundle thread pool size based on container thread pool config - // this allows Samza auto-sizing to tune the threads - LOG.info("Convert threadPoolSize {} to numThreadsForProcessElement", threadPoolSize); - // NumThreadsForProcessElement in option is the source of truth - options.setNumThreadsForProcessElement(threadPoolSize); - } - } - return builder.build(); - } - - private static Map createUserConfig(SamzaPipelineOptions options) - throws Exception { - final Map config = new HashMap<>(); - - // apply user configs - final String configFilePath = options.getConfigFilePath(); - - // If user provides a config file, use it as base configs. - if (StringUtils.isNoneEmpty(configFilePath)) { - LOG.info("configFilePath: {}", configFilePath); - - final Config properties = new MapConfig(Collections.singletonMap("path", configFilePath)); - final ConfigLoaderFactory configLoaderFactory = - options.getConfigLoaderFactory().getDeclaredConstructor().newInstance(); - - LOG.info("configLoaderFactory: {}", configLoaderFactory.getClass().getName()); - - // Config file must exist for default properties config - // TODO: add check to all non-empty files once we don't need to - // pass the command-line args through the containers - if (configLoaderFactory instanceof PropertiesConfigLoaderFactory) { - checkArgument( - new File(configFilePath).exists(), "Config file %s does not exist", configFilePath); - } - - config.putAll(configLoaderFactory.getLoader(properties).getConfig()); - } - // Apply override on top - if (options.getConfigOverride() != null) { - config.putAll(options.getConfigOverride()); - } - - return config; - } - - private static void validateZKStandAloneRun(Map config) { - checkArgument( - config.containsKey(APP_RUNNER_CLASS), - "Config %s not found for %s Deployment", - APP_RUNNER_CLASS, - SamzaExecutionEnvironment.STANDALONE); - checkArgument( - config.get(APP_RUNNER_CLASS).equals(LocalApplicationRunner.class.getName()), - "Config %s must be set to %s for %s Deployment", - APP_RUNNER_CLASS, - LocalApplicationRunner.class.getName(), - SamzaExecutionEnvironment.STANDALONE); - checkArgument( - config.containsKey(JobCoordinatorConfig.JOB_COORDINATOR_FACTORY), - "Config %s not found for %s Deployment", - JobCoordinatorConfig.JOB_COORDINATOR_FACTORY, - SamzaExecutionEnvironment.STANDALONE); - checkArgument( - config - .get(JobCoordinatorConfig.JOB_COORDINATOR_FACTORY) - .equals(ZkJobCoordinatorFactory.class.getName()), - "Config %s must be set to %s for %s Deployment", - JobCoordinatorConfig.JOB_COORDINATOR_FACTORY, - ZkJobCoordinatorFactory.class.getName(), - SamzaExecutionEnvironment.STANDALONE); - checkArgument( - config.containsKey(ZkConfig.ZK_CONNECT), - "Config %s not found for %s Deployment", - ZkConfig.ZK_CONNECT, - SamzaExecutionEnvironment.STANDALONE); - } - - private static void validateYarnRun(Map config) { - checkArgument( - config.containsKey(YARN_PACKAGE_PATH), - "Config %s not found for %s Deployment", - YARN_PACKAGE_PATH, - SamzaExecutionEnvironment.YARN); - final String appRunner = config.get(APP_RUNNER_CLASS); - checkArgument( - appRunner == null - || BeamJobCoordinatorRunner.class.getName().equals(appRunner) - || RemoteApplicationRunner.class.getName().equals(appRunner) - || BeamContainerRunner.class.getName().equals(appRunner), - "Config %s must be set to %s for %s Deployment, but found %s", - APP_RUNNER_CLASS, - String.format( - "[%s, %s or %s]", - BeamJobCoordinatorRunner.class.getName(), - RemoteApplicationRunner.class.getName(), - BeamContainerRunner.class.getName()), - SamzaExecutionEnvironment.YARN, - appRunner); - checkArgument( - config.containsKey(JOB_FACTORY_CLASS), - "Config %s not found for %s Deployment", - JOB_FACTORY_CLASS, - SamzaExecutionEnvironment.YARN); - } - - @VisibleForTesting - public static Map localRunConfig() { - // Default Samza config using local deployment of a single JVM - return ImmutableMap.builder() - .put(APP_RUNNER_CLASS, LocalApplicationRunner.class.getName()) - .put( - JobCoordinatorConfig.JOB_COORDINATOR_FACTORY, - PassthroughJobCoordinatorFactory.class.getName()) - .put(GROUPER_FACTORY, SingleContainerGrouperFactory.class.getName()) - .put(COMMIT_MS, "-1") - .put("processor.id", "1") - .put( - // TODO: remove after SAMZA-1531 is resolved - ApplicationConfig.APP_RUN_ID, - System.currentTimeMillis() - + "-" - // use the most significant bits in UUID (8 digits) to avoid collision - + UUID.randomUUID().toString().substring(0, 8)) - .build(); - } - - public static Map yarnRunConfig() { - // Default Samza config using yarn deployment - return ImmutableMap.builder() - .put(APP_RUNNER_CLASS, RemoteApplicationRunner.class.getName()) - .put(JOB_FACTORY_CLASS, YarnJobFactory.class.getName()) - .build(); - } - - public static Map standAloneRunConfig() { - // Default Samza config using stand alone deployment - return ImmutableMap.builder() - .put(APP_RUNNER_CLASS, LocalApplicationRunner.class.getName()) - .put(JobCoordinatorConfig.JOB_COORDINATOR_FACTORY, ZkJobCoordinatorFactory.class.getName()) - .build(); - } - - private static Map createSystemConfig( - SamzaPipelineOptions options, Map config) { - final ImmutableMap.Builder configBuilder = - ImmutableMap.builder() - .put("stores.beamStore.key.serde", "byteArraySerde") - .put("stores.beamStore.msg.serde", "stateValueSerde") - .put( - "serializers.registry.stateValueSerde.class", - SamzaStoreStateInternals.StateValueSerdeFactory.class.getName()) - .put( - "serializers.registry.byteArraySerde.class", - SamzaStoreStateInternals.ByteArraySerdeFactory.class.getName()); - - // if config does not contain "stores.beamStore.factory" at this moment, - // then it is a stateless job. - if (!config.containsKey(BEAM_STORE_FACTORY)) { - options.setStateDurable(false); - configBuilder.put( - BEAM_STORE_FACTORY, - "org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStorageEngineFactory"); - } - - LOG.info("Execution environment is {}", options.getSamzaExecutionEnvironment()); - switch (options.getSamzaExecutionEnvironment()) { - case YARN: - configBuilder.putAll(yarnRunConfig()); - break; - case STANDALONE: - configBuilder.putAll(standAloneRunConfig()); - break; - default: // LOCAL - configBuilder.putAll(localRunConfig()); - break; - } - - // TODO: remove after we sort out Samza task wrapper - configBuilder.put("samza.li.task.wrapper.enabled", "false"); - - return configBuilder.build(); - } - - static Map createRocksDBStoreConfig(SamzaPipelineOptions options) { - final ImmutableMap.Builder configBuilder = - ImmutableMap.builder() - .put(BEAM_STORE_FACTORY, RocksDbKeyValueStorageEngineFactory.class.getName()) - .put("stores.beamStore.rocksdb.compression", "lz4"); - - if (options.getStateDurable()) { - LOG.info("stateDurable is enabled"); - configBuilder.put("stores.beamStore.changelog", getChangelogTopic(options, "beamStore")); - configBuilder.put("job.host-affinity.enabled", "true"); - } - - return configBuilder.build(); - } - - private static void validateConfigs(SamzaPipelineOptions options, Map config) { - - // validate execution environment - switch (options.getSamzaExecutionEnvironment()) { - case YARN: - validateYarnRun(config); - break; - case STANDALONE: - validateZKStandAloneRun(config); - break; - default: - // do nothing - break; - } - } - - static String getChangelogTopic(SamzaPipelineOptions options, String storeName) { - return String.format( - "%s-%s-%s-changelog", options.getJobName(), options.getJobInstance(), storeName); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigContext.java deleted file mode 100644 index 954f95a593ee..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ConfigContext.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Map; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.util.StoreIdGenerator; -import org.apache.beam.sdk.runners.AppliedPTransform; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; - -/** Helper that provides context data such as output for config generation. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class ConfigContext { - private final Map idMap; - private AppliedPTransform currentTransform; - private final SamzaPipelineOptions options; - private final StoreIdGenerator storeIdGenerator; - - public ConfigContext( - Map idMap, Set nonUniqueStateIds, SamzaPipelineOptions options) { - this.idMap = idMap; - this.options = options; - this.storeIdGenerator = new StoreIdGenerator(nonUniqueStateIds); - } - - public void setCurrentTransform(AppliedPTransform currentTransform) { - this.currentTransform = currentTransform; - } - - public void clearCurrentTransform() { - this.currentTransform = null; - } - - @SuppressWarnings("unchecked") - public OutT getOutput(PTransform transform) { - return (OutT) Iterables.getOnlyElement(this.currentTransform.getOutputs().values()); - } - - public String getOutputId(TransformHierarchy.Node node) { - return getIdForPValue(Iterables.getOnlyElement(node.getOutputs().values())); - } - - public SamzaPipelineOptions getPipelineOptions() { - return this.options; - } - - public StoreIdGenerator getStoreIdGenerator() { - return storeIdGenerator; - } - - private String getIdForPValue(PValue pvalue) { - final String id = idMap.get(pvalue); - if (id == null) { - throw new IllegalArgumentException("No id mapping for value: " + pvalue); - } - return id; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/FlattenPCollectionsTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/FlattenPCollectionsTranslator.java deleted file mode 100644 index fe840ee86461..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/FlattenPCollectionsTranslator.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.beam.runners.samza.runtime.Op; -import org.apache.beam.runners.samza.runtime.OpAdapter; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.Flatten; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.samza.operators.MessageStream; - -/** - * Translates {@link org.apache.beam.sdk.transforms.Flatten.PCollections} to Samza merge operator. - */ -class FlattenPCollectionsTranslator implements TransformTranslator> { - @Override - public void translate( - Flatten.PCollections transform, TransformHierarchy.Node node, TranslationContext ctx) { - doTranslate(transform, node, ctx); - } - - private static void doTranslate( - Flatten.PCollections transform, TransformHierarchy.Node node, TranslationContext ctx) { - final PCollection output = ctx.getOutput(transform); - - final List>> inputStreams = new ArrayList<>(); - for (Map.Entry, PCollection> taggedPValue : node.getInputs().entrySet()) { - @SuppressWarnings("unchecked") - final PCollection input = (PCollection) taggedPValue.getValue(); - inputStreams.add(ctx.getMessageStream(input)); - } - - if (inputStreams.isEmpty()) { - // for some of the validateRunner tests only - final MessageStream> noOpStream = - ctx.getDummyStream() - .flatMapAsync( - OpAdapter.adapt((Op) (inputElement, emitter) -> {}, ctx)); - ctx.registerMessageStream(output, noOpStream); - return; - } - - ctx.registerMessageStream(output, mergeInputStreams(inputStreams)); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - doTranslatePortable(transform, ctx); - } - - private static void doTranslatePortable( - PipelineNode.PTransformNode transform, PortableTranslationContext ctx) { - final List>> inputStreams = ctx.getAllInputMessageStreams(transform); - final String outputId = ctx.getOutputId(transform); - - // For portable api there should be at least the impulse as a dummy input - // We will know once validateRunner tests are available for portable runners - checkState( - !inputStreams.isEmpty(), "no input streams defined for Flatten: %s", transform.getId()); - - ctx.registerMessageStream(outputId, mergeInputStreams(inputStreams)); - } - - // Merge multiple input streams into one, as this is what "flatten" is meant to do - private static MessageStream> mergeInputStreams( - List>> inputStreams) { - if (inputStreams.size() == 1) { - return Iterables.getOnlyElement(inputStreams); - } - final Set>> streamsToMerge = new HashSet<>(); - inputStreams.forEach( - stream -> { - if (!streamsToMerge.add(stream)) { - // Merge same streams. Make a copy of the current stream. - streamsToMerge.add(stream.map(m -> m)); - } - }); - - return MessageStream.mergeAll(streamsToMerge); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/GroupByKeyTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/GroupByKeyTranslator.java deleted file mode 100644 index 18d105e35e9f..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/GroupByKeyTranslator.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.apache.beam.runners.samza.util.SamzaPipelineTranslatorUtils.escape; - -import java.util.Map; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.core.KeyedWorkItem; -import org.apache.beam.runners.core.KeyedWorkItemCoder; -import org.apache.beam.runners.core.SystemReduceFn; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.DoFnOp; -import org.apache.beam.runners.samza.runtime.GroupByKeyOp; -import org.apache.beam.runners.samza.runtime.KvToKeyedWorkItemOp; -import org.apache.beam.runners.samza.runtime.OpAdapter; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.transforms.GroupWithoutRepartition; -import org.apache.beam.runners.samza.util.SamzaCoders; -import org.apache.beam.runners.samza.util.SamzaPipelineTranslatorUtils; -import org.apache.beam.runners.samza.util.WindowUtils; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.CombineFnBase; -import org.apache.beam.sdk.transforms.GroupByKey; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.AppliedCombineFn; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.serializers.KVSerde; - -/** Translates {@link GroupByKey} to Samza {@link GroupByKeyOp}. */ -@SuppressWarnings({"keyfor", "nullness"}) // TODO(https://github.com/apache/beam/issues/20497) -class GroupByKeyTranslator - implements TransformTranslator< - PTransform>, PCollection>>>, - TransformConfigGenerator< - PTransform>, PCollection>>> { - - @Override - public void translate( - PTransform>, PCollection>> transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - doTranslate(transform, node, ctx); - } - - private static void doTranslate( - PTransform>, PCollection>> transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - final PCollection> input = ctx.getInput(transform); - - final PCollection> output = ctx.getOutput(transform); - final TupleTag> outputTag = ctx.getOutputTag(transform); - - @SuppressWarnings("unchecked") - final WindowingStrategy windowingStrategy = - (WindowingStrategy) input.getWindowingStrategy(); - - final MessageStream>> inputStream = ctx.getMessageStream(input); - - final KvCoder kvInputCoder = (KvCoder) input.getCoder(); - final Coder>> elementCoder = SamzaCoders.of(input); - - final SystemReduceFn reduceFn = - getSystemReduceFn(transform, input.getPipeline(), kvInputCoder); - - final MessageStream>> outputStream = - doTranslateGBK( - inputStream, - needRepartition(node, ctx), - reduceFn, - windowingStrategy, - kvInputCoder, - elementCoder, - ctx, - outputTag, - input.isBounded()); - - ctx.registerMessageStream(output, outputStream); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - final String inputId = ctx.getInputId(transform); - final RunnerApi.PCollection input = pipeline.getComponents().getPcollectionsOrThrow(inputId); - final MessageStream>> inputStream = ctx.getMessageStreamById(inputId); - final WindowingStrategy windowingStrategy = - WindowUtils.getWindowStrategy(inputId, pipeline.getComponents()); - final WindowedValues.WindowedValueCoder> windowedInputCoder = - WindowUtils.instantiateWindowedCoder(inputId, pipeline.getComponents()); - final TupleTag> outputTag = - new TupleTag<>(Iterables.getOnlyElement(transform.getTransform().getOutputsMap().keySet())); - - final MessageStream>> outputStream = - doTranslatePortable( - input, inputStream, windowingStrategy, windowedInputCoder, outputTag, ctx); - - ctx.registerMessageStream(ctx.getOutputId(transform), outputStream); - } - - @Override - public Map createConfig( - PTransform>, PCollection>> transform, - TransformHierarchy.Node node, - ConfigContext ctx) { - return ConfigBuilder.createRocksDBStoreConfig(ctx.getPipelineOptions()); - } - - @Override - public Map createPortableConfig( - PipelineNode.PTransformNode transform, SamzaPipelineOptions options) { - return ConfigBuilder.createRocksDBStoreConfig(options); - } - - /** - * The method is used to translate both portable GBK transform as well as grouping side inputs - * into Samza. - */ - static MessageStream>> doTranslatePortable( - RunnerApi.PCollection input, - MessageStream>> inputStream, - WindowingStrategy windowingStrategy, - WindowedValues.WindowedValueCoder> windowedInputCoder, - TupleTag> outputTag, - PortableTranslationContext ctx) { - final boolean needRepartition = ctx.getPipelineOptions().getMaxSourceParallelism() > 1; - final Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); - final KvCoder kvInputCoder = (KvCoder) windowedInputCoder.getValueCoder(); - final Coder>> elementCoder = - WindowedValues.FullWindowedValueCoder.of(kvInputCoder, windowCoder); - - @SuppressWarnings("unchecked") - final SystemReduceFn reduceFn = - (SystemReduceFn) - SystemReduceFn.buffering(kvInputCoder.getValueCoder()); - - final PCollection.IsBounded isBounded = SamzaPipelineTranslatorUtils.isBounded(input); - - return doTranslateGBK( - inputStream, - needRepartition, - reduceFn, - windowingStrategy, - kvInputCoder, - elementCoder, - ctx, - outputTag, - isBounded); - } - - private static MessageStream>> doTranslateGBK( - MessageStream>> inputStream, - boolean needRepartition, - SystemReduceFn reduceFn, - WindowingStrategy windowingStrategy, - KvCoder kvInputCoder, - Coder>> elementCoder, - TranslationContext ctx, - TupleTag> outputTag, - PCollection.IsBounded isBounded) { - final MessageStream>> filteredInputStream = - inputStream.filter(msg -> msg.getType() == OpMessage.Type.ELEMENT); - - final MessageStream>> partitionedInputStream; - if (!needRepartition) { - partitionedInputStream = filteredInputStream; - } else { - partitionedInputStream = - filteredInputStream - .partitionBy( - msg -> msg.getElement().getValue().getKey(), - msg -> msg.getElement(), - KVSerde.of( - SamzaCoders.toSerde(kvInputCoder.getKeyCoder()), - SamzaCoders.toSerde(elementCoder)), - "gbk-" + escape(ctx.getTransformId())) - .map(kv -> OpMessage.ofElement(kv.getValue())); - } - - final Coder> keyedWorkItemCoder = - KeyedWorkItemCoder.of( - kvInputCoder.getKeyCoder(), - kvInputCoder.getValueCoder(), - windowingStrategy.getWindowFn().windowCoder()); - - final MessageStream>> outputStream = - partitionedInputStream - .flatMapAsync(OpAdapter.adapt(new KvToKeyedWorkItemOp<>(), ctx)) - .flatMapAsync( - OpAdapter.adapt( - new GroupByKeyOp<>( - outputTag, - keyedWorkItemCoder, - reduceFn, - windowingStrategy, - new DoFnOp.SingleOutputManagerFactory<>(), - ctx.getTransformFullName(), - ctx.getTransformId(), - isBounded), - ctx)); - return outputStream; - } - - @SuppressWarnings("unchecked") - private static - SystemReduceFn getSystemReduceFn( - PTransform>, PCollection>> transform, - Pipeline pipeline, - KvCoder kvInputCoder) { - if (transform instanceof GroupByKey) { - return (SystemReduceFn) - SystemReduceFn.buffering(kvInputCoder.getValueCoder()); - } else if (transform instanceof Combine.PerKey) { - final CombineFnBase.GlobalCombineFn combineFn = - ((Combine.PerKey) transform).getFn(); - return SystemReduceFn.combining( - kvInputCoder.getKeyCoder(), - AppliedCombineFn.withInputCoder(combineFn, pipeline.getCoderRegistry(), kvInputCoder)); - } else { - throw new RuntimeException("Transform " + transform + " cannot be translated as GroupByKey."); - } - } - - private static boolean needRepartition(TransformHierarchy.Node node, TranslationContext ctx) { - if (ctx.getPipelineOptions().getMaxSourceParallelism() == 1) { - // Only one task will be created, no need for repartition - return false; - } - - if (node == null) { - return true; - } - - if (node.getTransform() instanceof GroupWithoutRepartition) { - return false; - } else { - return needRepartition(node.getEnclosingNode(), ctx); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ImpulseTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ImpulseTranslator.java deleted file mode 100644 index 35d8f1f39962..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ImpulseTranslator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.util.SamzaPipelineTranslatorUtils; -import org.apache.beam.sdk.runners.TransformHierarchy.Node; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.PBegin; -import org.apache.beam.sdk.values.PCollection; -import org.apache.samza.operators.KV; -import org.apache.samza.serializers.KVSerde; -import org.apache.samza.serializers.NoOpSerde; -import org.apache.samza.serializers.Serde; -import org.apache.samza.system.descriptors.GenericInputDescriptor; -import org.apache.samza.system.descriptors.GenericSystemDescriptor; - -/** - * Translate {@link org.apache.beam.sdk.transforms.Impulse} to a samza message stream produced by - * {@link - * org.apache.beam.runners.samza.translation.SamzaImpulseSystemFactory.SamzaImpulseSystemConsumer}. - */ -@SuppressWarnings({ - "rawtypes" // TODO(https://github.com/apache/beam/issues/20447) -}) -public class ImpulseTranslator - implements TransformTranslator>> { - - @Override - public void translate( - PTransform> transform, Node node, TranslationContext ctx) { - final PCollection output = ctx.getOutput(transform); - final String outputId = ctx.getIdForPValue(output); - final GenericSystemDescriptor systemDescriptor = - new GenericSystemDescriptor(outputId, SamzaImpulseSystemFactory.class.getName()); - - // The KvCoder is needed here for Samza not to crop the key. - final Serde>> kvSerde = KVSerde.of(new NoOpSerde(), new NoOpSerde<>()); - final GenericInputDescriptor>> inputDescriptor = - systemDescriptor.getInputDescriptor(outputId, kvSerde); - - ctx.registerInputMessageStream(output, inputDescriptor); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - - final String outputId = ctx.getOutputId(transform); - final String escapedOutputId = SamzaPipelineTranslatorUtils.escape(outputId); - final GenericSystemDescriptor systemDescriptor = - new GenericSystemDescriptor(escapedOutputId, SamzaImpulseSystemFactory.class.getName()); - - // The KvCoder is needed here for Samza not to crop the key. - final Serde>> kvSerde = KVSerde.of(new NoOpSerde(), new NoOpSerde<>()); - final GenericInputDescriptor>> inputDescriptor = - systemDescriptor.getInputDescriptor(escapedOutputId, kvSerde); - - ctx.registerInputMessageStream(outputId, inputDescriptor); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PViewToIdMapper.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PViewToIdMapper.java deleted file mode 100644 index f7bca872759d..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PViewToIdMapper.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.util.NameUtils; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.sdk.values.PValue; - -/** - * This class generates an ID for each {@link PValue} during a topological traversal of the BEAM - * {@link Pipeline}. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class PViewToIdMapper extends Pipeline.PipelineVisitor.Defaults { - private final Map idMap = new HashMap<>(); - private int nextId; - - public static Map buildIdMap(Pipeline pipeline) { - final PViewToIdMapper mapper = new PViewToIdMapper(); - pipeline.traverseTopologically(mapper); - return mapper.getIdMap(); - } - - private PViewToIdMapper() {} - - @Override - public void visitValue(PValue value, TransformHierarchy.Node producer) { - final String valueDesc = pValueToString(value).replaceFirst(".*:([a-zA-Z#0-9]+).*", "$1"); - - final String samzaSafeValueDesc = valueDesc.replaceAll("[^A-Za-z0-9_-]", "_"); - - idMap.put(value, String.format("%d-%s", nextId++, samzaSafeValueDesc)); - } - - @Override - public void visitPrimitiveTransform(TransformHierarchy.Node node) { - if (node.getTransform() instanceof SamzaPublishView) { - final PCollectionView view = ((SamzaPublishView) node.getTransform()).getView(); - visitValue(view, node); - } - } - - public Map getIdMap() { - return Collections.unmodifiableMap(idMap); - } - - /** - * This method is created to replace the {@link org.apache.beam.sdk.values.PValueBase#toString()} - * with the old implementation that doesn't contain the hashcode. - */ - private static String pValueToString(PValue value) { - String name; - try { - name = value.getName(); - } catch (IllegalStateException e) { - name = ""; - } - return name + " [" + NameUtils.approximateSimpleName(value.getClass()) + "]"; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ParDoBoundMultiTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ParDoBoundMultiTranslator.java deleted file mode 100644 index 097eb4c256a3..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ParDoBoundMultiTranslator.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.apache.beam.runners.fnexecution.translation.PipelineTranslatorUtils.instantiateCoder; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.model.pipeline.v1.RunnerApi.ExecutableStagePayload.SideInputId; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.DoFnOp; -import org.apache.beam.runners.samza.runtime.Op; -import org.apache.beam.runners.samza.runtime.OpAdapter; -import org.apache.beam.runners.samza.runtime.OpEmitter; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.runtime.PortableDoFnOp; -import org.apache.beam.runners.samza.runtime.SamzaDoFnInvokerRegistrar; -import org.apache.beam.runners.samza.util.SamzaPipelineTranslatorUtils; -import org.apache.beam.runners.samza.util.StateUtils; -import org.apache.beam.runners.samza.util.WindowUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.IterableCoder; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.coders.VoidCoder; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.ViewFn; -import org.apache.beam.sdk.transforms.join.RawUnionValue; -import org.apache.beam.sdk.transforms.reflect.DoFnSignature; -import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.construction.ParDoTranslation; -import org.apache.beam.sdk.util.construction.RunnerPCollectionView; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.sdk.values.PCollectionViews; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.TypeDescriptor; -import org.apache.beam.sdk.values.TypeDescriptors; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.operators.functions.FlatMapFunction; -import org.apache.samza.operators.functions.WatermarkFunction; -import org.apache.samza.storage.kv.RocksDbKeyValueStorageEngineFactory; -import org.joda.time.Instant; - -/** - * Translates {@link org.apache.beam.sdk.transforms.ParDo.MultiOutput} or ExecutableStage in - * portable api to Samza {@link DoFnOp}. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -class ParDoBoundMultiTranslator - implements TransformTranslator>, - TransformConfigGenerator> { - - private final SamzaDoFnInvokerRegistrar doFnInvokerRegistrar; - - ParDoBoundMultiTranslator() { - final Iterator invokerReg = - ServiceLoader.load(SamzaDoFnInvokerRegistrar.class).iterator(); - doFnInvokerRegistrar = invokerReg.hasNext() ? Iterators.getOnlyElement(invokerReg) : null; - } - - @Override - public void translate( - ParDo.MultiOutput transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - doTranslate(transform, node, ctx); - } - - // static for serializing anonymous functions - private static void doTranslate( - ParDo.MultiOutput transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - final PCollection input = ctx.getInput(transform); - final Map, Coder> outputCoders = - ctx.getCurrentTransform().getOutputs().entrySet().stream() - .filter(e -> e.getValue() != null) - .collect( - Collectors.toMap(e -> e.getKey(), e -> ((PCollection) e.getValue()).getCoder())); - - final Coder keyCoder = - StateUtils.isStateful(transform.getFn()) - ? ((KvCoder) input.getCoder()).getKeyCoder() - : null; - - if (DoFnSignatures.isSplittable(transform.getFn())) { - throw new UnsupportedOperationException("Splittable DoFn is not currently supported"); - } - if (DoFnSignatures.requiresTimeSortedInput(transform.getFn())) { - throw new UnsupportedOperationException( - "@RequiresTimeSortedInput annotation is not currently supported"); - } - - final MessageStream> inputStream = ctx.getMessageStream(input); - final List>> sideInputStreams = - transform.getSideInputs().values().stream() - .map(ctx::getViewStream) - .collect(Collectors.toList()); - final ArrayList, PCollection>> outputs = - new ArrayList<>(node.getOutputs().entrySet()); - - final Map, Integer> tagToIndexMap = new HashMap<>(); - final Map> indexToPCollectionMap = new HashMap<>(); - - for (int index = 0; index < outputs.size(); ++index) { - final Map.Entry, PCollection> taggedOutput = outputs.get(index); - tagToIndexMap.put(taggedOutput.getKey(), index); - - if (taggedOutput.getValue() == null) { - throw new IllegalArgumentException( - "Expected side output to be PCollection, but was: " + taggedOutput.getValue()); - } - final PCollection sideOutputCollection = taggedOutput.getValue(); - indexToPCollectionMap.put(index, sideOutputCollection); - } - - final HashMap> idToPValueMap = new HashMap<>(); - for (PCollectionView view : transform.getSideInputs().values()) { - idToPValueMap.put(ctx.getViewId(view), view); - } - - DoFnSchemaInformation doFnSchemaInformation; - doFnSchemaInformation = ParDoTranslation.getSchemaInformation(ctx.getCurrentTransform()); - - Map> sideInputMapping = - ParDoTranslation.getSideInputMapping(ctx.getCurrentTransform()); - - final DoFnSignature signature = DoFnSignatures.getSignature(transform.getFn().getClass()); - final Map stateIdToStoreMapping = new HashMap<>(); - for (String stateId : signature.stateDeclarations().keySet()) { - final String transformFullName = node.getEnclosingNode().getFullName(); - final String storeId = ctx.getStoreIdGenerator().getId(stateId, transformFullName); - stateIdToStoreMapping.put(stateId, storeId); - } - final DoFnOp op = - new DoFnOp<>( - transform.getMainOutputTag(), - transform.getFn(), - keyCoder, - (Coder) input.getCoder(), - null, - outputCoders, - transform.getSideInputs().values(), - transform.getAdditionalOutputTags().getAll(), - input.getWindowingStrategy(), - idToPValueMap, - new DoFnOp.MultiOutputManagerFactory(tagToIndexMap), - ctx.getTransformFullName(), - ctx.getTransformId(), - input.isBounded(), - false, - null, - null, - Collections.emptyMap(), - doFnSchemaInformation, - sideInputMapping, - stateIdToStoreMapping); - - final MessageStream> mergedStreams; - if (sideInputStreams.isEmpty()) { - mergedStreams = inputStream; - } else { - MessageStream> mergedSideInputStreams = - MessageStream.mergeAll(sideInputStreams).flatMap(new SideInputWatermarkFn()); - mergedStreams = inputStream.merge(Collections.singletonList(mergedSideInputStreams)); - } - - final MessageStream> taggedOutputStream = - mergedStreams.flatMapAsync(OpAdapter.adapt(op, ctx)); - - for (int outputIndex : tagToIndexMap.values()) { - @SuppressWarnings("unchecked") - final MessageStream> outputStream = - taggedOutputStream - .filter( - message -> - message.getType() != OpMessage.Type.ELEMENT - || message.getElement().getValue().getUnionTag() == outputIndex) - .flatMapAsync(OpAdapter.adapt(new RawUnionValueToValue(), ctx)); - - ctx.registerMessageStream(indexToPCollectionMap.get(outputIndex), outputStream); - } - } - - /* - * We reuse ParDo translator to translate ExecutableStage - */ - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - doTranslatePortable(transform, pipeline, ctx); - } - - // static for serializing anonymous functions - private static void doTranslatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - Map outputs = transform.getTransform().getOutputsMap(); - - final RunnerApi.ExecutableStagePayload stagePayload; - try { - stagePayload = - RunnerApi.ExecutableStagePayload.parseFrom( - transform.getTransform().getSpec().getPayload()); - } catch (IOException e) { - throw new RuntimeException(e); - } - - String inputId = stagePayload.getInput(); - final MessageStream> inputStream = ctx.getMessageStreamById(inputId); - - // Analyze side inputs - final List>>> sideInputStreams = new ArrayList<>(); - final Map> sideInputMapping = new HashMap<>(); - final Map> idToViewMapping = new HashMap<>(); - final RunnerApi.Components components = stagePayload.getComponents(); - for (SideInputId sideInputId : stagePayload.getSideInputsList()) { - final String sideInputCollectionId = - components - .getTransformsOrThrow(sideInputId.getTransformId()) - .getInputsOrThrow(sideInputId.getLocalName()); - final WindowingStrategy windowingStrategy = - WindowUtils.getWindowStrategy(sideInputCollectionId, components); - final WindowedValues.WindowedValueCoder coder = - (WindowedValues.WindowedValueCoder) instantiateCoder(sideInputCollectionId, components); - - // Create a runner-side view - final PCollectionView view = createPCollectionView(sideInputId, coder, windowingStrategy); - - // Use GBK to aggregate the side inputs and then broadcast it out - final MessageStream>> broadcastSideInput = - groupAndBroadcastSideInput( - sideInputId, - sideInputCollectionId, - components.getPcollectionsOrThrow(sideInputCollectionId), - (WindowingStrategy) windowingStrategy, - coder, - ctx); - - sideInputStreams.add(broadcastSideInput); - sideInputMapping.put(sideInputId, view); - idToViewMapping.put(getSideInputUniqueId(sideInputId), view); - } - - final Map, Integer> tagToIndexMap = new HashMap<>(); - final Map indexToIdMap = new HashMap<>(); - final Map> idToTupleTagMap = new HashMap<>(); - - // first output as the main output - final TupleTag mainOutputTag = - outputs.isEmpty() ? null : new TupleTag(outputs.keySet().iterator().next()); - - AtomicInteger index = new AtomicInteger(0); - outputs - .keySet() - .iterator() - .forEachRemaining( - outputName -> { - TupleTag tupleTag = new TupleTag<>(outputName); - tagToIndexMap.put(tupleTag, index.get()); - String collectionId = outputs.get(outputName); - indexToIdMap.put(index.get(), collectionId); - idToTupleTagMap.put(collectionId, tupleTag); - index.incrementAndGet(); - }); - - WindowedValues.WindowedValueCoder windowedInputCoder = - WindowUtils.instantiateWindowedCoder(inputId, pipeline.getComponents()); - - // TODO: support schema and side inputs for portable runner - // Note: transform.getTransform() is an ExecutableStage, not ParDo, so we need to extract - // these info from its components. - final DoFnSchemaInformation doFnSchemaInformation = null; - - final RunnerApi.PCollection input = pipeline.getComponents().getPcollectionsOrThrow(inputId); - final PCollection.IsBounded isBounded = SamzaPipelineTranslatorUtils.isBounded(input); - - // No key coder information required for handing the stateless stage or stage with user states - // The key coder information is required for handing the stage with user timers - final Coder timerKeyCoder = - stagePayload.getTimersCount() > 0 - ? ((KvCoder) - ((WindowedValues.FullWindowedValueCoder) windowedInputCoder).getValueCoder()) - .getKeyCoder() - : null; - - final PortableDoFnOp op = - new PortableDoFnOp<>( - mainOutputTag, - new NoOpDoFn<>(), - timerKeyCoder, - windowedInputCoder.getValueCoder(), // input coder not in use - windowedInputCoder, - Collections.emptyMap(), // output coders not in use - new ArrayList<>(sideInputMapping.values()), - new ArrayList<>(idToTupleTagMap.values()), // used by java runner only - WindowUtils.getWindowStrategy(inputId, stagePayload.getComponents()), - idToViewMapping, - new DoFnOp.MultiOutputManagerFactory(tagToIndexMap), - ctx.getTransformFullName(), - ctx.getTransformId(), - isBounded, - true, - stagePayload, - ctx.getJobInfo(), - idToTupleTagMap, - doFnSchemaInformation, - sideInputMapping, - Collections.emptyMap()); - - final MessageStream> mergedStreams; - if (sideInputStreams.isEmpty()) { - mergedStreams = inputStream; - } else { - MessageStream> mergedSideInputStreams = - MessageStream.mergeAll(sideInputStreams).flatMap(new SideInputWatermarkFn()); - mergedStreams = inputStream.merge(Collections.singletonList(mergedSideInputStreams)); - } - - final MessageStream> taggedOutputStream = - mergedStreams.flatMapAsync(OpAdapter.adapt(op, ctx)); - - for (int outputIndex : tagToIndexMap.values()) { - @SuppressWarnings("unchecked") - final MessageStream> outputStream = - taggedOutputStream - .filter( - message -> - message.getType() != OpMessage.Type.ELEMENT - || message.getElement().getValue().getUnionTag() == outputIndex) - .flatMapAsync(OpAdapter.adapt(new RawUnionValueToValue(), ctx)); - - ctx.registerMessageStream(indexToIdMap.get(outputIndex), outputStream); - } - } - - @Override - public Map createConfig( - ParDo.MultiOutput transform, TransformHierarchy.Node node, ConfigContext ctx) { - final Map config = new HashMap<>(); - final DoFnSignature signature = DoFnSignatures.getSignature(transform.getFn().getClass()); - final SamzaPipelineOptions options = ctx.getPipelineOptions(); - - // If a ParDo observes directly or indirectly with window, then this is a stateful ParDo - // in this case, we will use RocksDB as system store. - if (signature.processElement().observesWindow()) { - config.putAll(ConfigBuilder.createRocksDBStoreConfig(options)); - } - - if (signature.usesState()) { - // set up user state configs - for (String stateId : signature.stateDeclarations().keySet()) { - final String transformFullName = node.getEnclosingNode().getFullName(); - final String storeId = ctx.getStoreIdGenerator().getId(stateId, transformFullName); - config.put( - "stores." + storeId + ".factory", RocksDbKeyValueStorageEngineFactory.class.getName()); - config.put("stores." + storeId + ".key.serde", "byteArraySerde"); - config.put("stores." + storeId + ".msg.serde", "stateValueSerde"); - config.put("stores." + storeId + ".rocksdb.compression", "lz4"); - - if (options.getStateDurable()) { - config.put( - "stores." + storeId + ".changelog", - ConfigBuilder.getChangelogTopic(options, storeId)); - } - } - } - - if (doFnInvokerRegistrar != null) { - config.putAll(doFnInvokerRegistrar.configFor(transform.getFn())); - } - - return config; - } - - @Override - public Map createPortableConfig( - PipelineNode.PTransformNode transform, SamzaPipelineOptions options) { - - final RunnerApi.ExecutableStagePayload stagePayload; - try { - stagePayload = - RunnerApi.ExecutableStagePayload.parseFrom( - transform.getTransform().getSpec().getPayload()); - } catch (IOException e) { - throw new RuntimeException(e); - } - - if (!StateUtils.isStateful(stagePayload)) { - return Collections.emptyMap(); - } - - final Map config = - new HashMap<>(ConfigBuilder.createRocksDBStoreConfig(options)); - for (RunnerApi.ExecutableStagePayload.UserStateId stateId : stagePayload.getUserStatesList()) { - final String storeId = stateId.getLocalName(); - - config.put( - "stores." + storeId + ".factory", RocksDbKeyValueStorageEngineFactory.class.getName()); - config.put("stores." + storeId + ".key.serde", "byteArraySerde"); - config.put("stores." + storeId + ".msg.serde", "stateValueSerde"); - config.put("stores." + storeId + ".rocksdb.compression", "lz4"); - - if (options.getStateDurable()) { - config.put( - "stores." + storeId + ".changelog", ConfigBuilder.getChangelogTopic(options, storeId)); - } - } - - return config; - } - - @SuppressWarnings("unchecked") - private static final ViewFn>, ?> VIEW_FN = - (ViewFn) - new PCollectionViews.MultimapViewFn<>( - (PCollectionViews.TypeDescriptorSupplier>>) - () -> TypeDescriptors.iterables(new TypeDescriptor>() {}), - (PCollectionViews.TypeDescriptorSupplier) TypeDescriptors::voids); - - // This method follows the same way in Flink to create a runner-side Java - // PCollectionView to represent a portable side input. - private static PCollectionView createPCollectionView( - SideInputId sideInputId, - WindowedValues.WindowedValueCoder coder, - WindowingStrategy windowingStrategy) { - - return new RunnerPCollectionView<>( - null, - new TupleTag<>(sideInputId.getLocalName()), - VIEW_FN, - // TODO: support custom mapping fn - windowingStrategy.getWindowFn().getDefaultWindowMappingFn(), - windowingStrategy, - coder.getValueCoder()); - } - - // Group the side input globally with a null key and then broadcast it - // to all tasks. - private static - MessageStream>> groupAndBroadcastSideInput( - SideInputId sideInputId, - String sideInputCollectionId, - RunnerApi.PCollection sideInputPCollection, - WindowingStrategy windowingStrategy, - WindowedValues.WindowedValueCoder coder, - PortableTranslationContext ctx) { - final MessageStream> sideInput = - ctx.getMessageStreamById(sideInputCollectionId); - final MessageStream>> keyedSideInput = - sideInput.map( - opMessage -> { - WindowedValue wv = opMessage.getElement(); - return OpMessage.ofElement(wv.withValue(KV.of(null, wv.getValue()))); - }); - final WindowedValues.WindowedValueCoder> kvCoder = - coder.withValueCoder(KvCoder.of(VoidCoder.of(), coder.getValueCoder())); - final MessageStream>>> groupedSideInput = - GroupByKeyTranslator.doTranslatePortable( - sideInputPCollection, - keyedSideInput, - windowingStrategy, - kvCoder, - new TupleTag<>("main output"), - ctx); - final MessageStream>> nonkeyGroupedSideInput = - groupedSideInput.map( - opMessage -> { - WindowedValue>> wv = opMessage.getElement(); - return OpMessage.ofElement(wv.withValue(wv.getValue().getValue())); - }); - final MessageStream>> broadcastSideInput = - SamzaPublishViewTranslator.doTranslate( - nonkeyGroupedSideInput, - coder.withValueCoder(IterableCoder.of(coder.getValueCoder())), - ctx.getTransformId(), - getSideInputUniqueId(sideInputId), - ctx.getPipelineOptions()); - - return broadcastSideInput; - } - - private static String getSideInputUniqueId(SideInputId sideInputId) { - return sideInputId.getTransformId() + "-" + sideInputId.getLocalName(); - } - - static class SideInputWatermarkFn - implements FlatMapFunction, OpMessage>, - WatermarkFunction> { - - @Override - public Collection> apply(OpMessage message) { - return Collections.singletonList(message); - } - - @Override - public Collection> processWatermark(long watermark) { - return Collections.singletonList(OpMessage.ofSideInputWatermark(new Instant(watermark))); - } - - @Override - public Long getOutputWatermark() { - // Always return max so the side input watermark will not be aggregated with main inputs. - return Long.MAX_VALUE; - } - } - - static class RawUnionValueToValue implements Op { - @Override - public void processElement(WindowedValue inputElement, OpEmitter emitter) { - @SuppressWarnings("unchecked") - final OutT value = (OutT) inputElement.getValue().getValue(); - emitter.emitElement(inputElement.withValue(value)); - } - } - - private static class NoOpDoFn extends DoFn { - @ProcessElement - public void doNothing(@SuppressWarnings("unused") ProcessContext context) {} - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PortableTranslationContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PortableTranslationContext.java deleted file mode 100644 index 8821ee62928d..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/PortableTranslationContext.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.samza.application.descriptors.StreamApplicationDescriptor; -import org.apache.samza.operators.KV; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.system.descriptors.InputDescriptor; - -/** - * Helper that keeps the mapping from BEAM PCollection id to Samza {@link MessageStream}. It also - * provides other context data such as input and output of a {@link - * org.apache.beam.model.pipeline.v1.RunnerApi.PTransform}. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class PortableTranslationContext extends TranslationContext { - private final Map> messageStreams = new HashMap<>(); - private final JobInfo jobInfo; - - private PipelineNode.PTransformNode currentTransform; - - public PortableTranslationContext( - StreamApplicationDescriptor appDescriptor, SamzaPipelineOptions options, JobInfo jobInfo) { - super(appDescriptor, Collections.emptyMap(), Collections.emptySet(), options); - this.jobInfo = jobInfo; - } - - public List>> getAllInputMessageStreams( - PipelineNode.PTransformNode transform) { - final Collection inputStreamIds = transform.getTransform().getInputsMap().values(); - return inputStreamIds.stream().map(this::getMessageStreamById).collect(Collectors.toList()); - } - - public MessageStream> getOneInputMessageStream( - PipelineNode.PTransformNode transform) { - String id = Iterables.getOnlyElement(transform.getTransform().getInputsMap().values()); - return getMessageStreamById(id); - } - - @SuppressWarnings("unchecked") - public MessageStream> getMessageStreamById(String id) { - return (MessageStream>) messageStreams.get(id); - } - - public String getInputId(PipelineNode.PTransformNode transform) { - return Iterables.getOnlyElement(transform.getTransform().getInputsMap().values()); - } - - public String getOutputId(PipelineNode.PTransformNode transform) { - return Iterables.getOnlyElement(transform.getTransform().getOutputsMap().values()); - } - - public JobInfo getJobInfo() { - return jobInfo; - } - - public void registerMessageStream(String id, MessageStream> stream) { - if (messageStreams.containsKey(id)) { - throw new IllegalArgumentException("Stream already registered for id: " + id); - } - messageStreams.put(id, stream); - } - - /** Register an input stream with certain config id. */ - public void registerInputMessageStream( - String id, InputDescriptor>, ?> inputDescriptor) { - registerInputMessageStreams(id, Collections.singletonList(inputDescriptor)); - } - - public void registerInputMessageStreams( - String id, List>, ?>> inputDescriptors) { - registerInputMessageStreams(id, inputDescriptors, this::registerMessageStream); - } - - public void setCurrentTransform(PipelineNode.PTransformNode currentTransform) { - this.currentTransform = currentTransform; - } - - @Override - public void clearCurrentTransform() { - this.currentTransform = null; - } - - @Override - public String getTransformFullName() { - return currentTransform.getTransform().getUniqueName(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReadTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReadTranslator.java deleted file mode 100644 index a666e36ad02f..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReadTranslator.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Map; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.adapter.BoundedSourceSystem; -import org.apache.beam.runners.samza.adapter.UnboundedSourceSystem; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.util.SamzaCoders; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.io.BoundedSource; -import org.apache.beam.sdk.io.Source; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.SplittableParDo; -import org.apache.beam.sdk.values.PBegin; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.samza.operators.KV; -import org.apache.samza.serializers.KVSerde; -import org.apache.samza.serializers.NoOpSerde; -import org.apache.samza.serializers.Serde; -import org.apache.samza.system.descriptors.GenericInputDescriptor; -import org.apache.samza.system.descriptors.GenericSystemDescriptor; - -/** - * Translates {@link org.apache.beam.sdk.io.Read} to Samza input {@link - * org.apache.samza.operators.MessageStream}. - */ -public class ReadTranslator implements TransformTranslator>> { - - @Override - public void translate( - PTransform> transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - final PCollection output = ctx.getOutput(transform); - final Coder> coder = SamzaCoders.of(output); - final Source source = - transform instanceof SplittableParDo.PrimitiveBoundedRead - ? ((SplittableParDo.PrimitiveBoundedRead) transform).getSource() - : ((SplittableParDo.PrimitiveUnboundedRead) transform).getSource(); - final String id = ctx.getIdForPValue(output); - - // Create system descriptor - final GenericSystemDescriptor systemDescriptor; - if (source instanceof BoundedSource) { - systemDescriptor = - new GenericSystemDescriptor(id, BoundedSourceSystem.Factory.class.getName()); - } else { - systemDescriptor = - new GenericSystemDescriptor(id, UnboundedSourceSystem.Factory.class.getName()); - } - - final Map systemConfig = - ImmutableMap.of( - "source", Base64Serializer.serializeUnchecked(source), - "coder", Base64Serializer.serializeUnchecked(coder), - "stepName", node.getFullName()); - systemDescriptor.withSystemConfigs(systemConfig); - - // Create stream descriptor - @SuppressWarnings("unchecked") - final Serde>> kvSerde = - (Serde) KVSerde.of(new NoOpSerde<>(), new NoOpSerde<>()); - final GenericInputDescriptor>> inputDescriptor = - systemDescriptor.getInputDescriptor(id, kvSerde); - if (source instanceof BoundedSource) { - inputDescriptor.isBounded(); - } - - ctx.registerInputMessageStream(output, inputDescriptor); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/RedistributeByKeyTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/RedistributeByKeyTranslator.java deleted file mode 100644 index e59d74abe38f..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/RedistributeByKeyTranslator.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import com.google.auto.service.AutoService; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.NativeTransforms; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; - -/** - * Translates Reshuffle transform into Samza's native partitionBy operator, which will partition - * each incoming message by the key into a Task corresponding to that key. - */ -public class RedistributeByKeyTranslator - implements TransformTranslator>, PCollection>>> { - - private final ReshuffleTranslator reshuffleTranslator = - new ReshuffleTranslator<>("rdstr-"); - - @Override - public void translate( - PTransform>, PCollection>> transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - reshuffleTranslator.translate(transform, node, ctx); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - reshuffleTranslator.translatePortable(transform, pipeline, ctx); - } - - /** Predicate to determine whether a URN is a Samza native transform. */ - @AutoService(NativeTransforms.IsNativeTransform.class) - public static class IsSamzaNativeTransform implements NativeTransforms.IsNativeTransform { - @Override - public boolean test(RunnerApi.PTransform pTransform) { - return false; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReshuffleTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReshuffleTranslator.java deleted file mode 100644 index c318505d9849..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/ReshuffleTranslator.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import com.google.auto.service.AutoService; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.util.SamzaCoders; -import org.apache.beam.runners.samza.util.WindowUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.NativeTransforms; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.serializers.KVSerde; - -/** - * Translates Reshuffle transform into Samza's native partitionBy operator, which will partition - * each incoming message by the key into a Task corresponding to that key. - */ -public class ReshuffleTranslator - implements TransformTranslator>, PCollection>>> { - - private final String prefix; - - ReshuffleTranslator(String prefix) { - this.prefix = prefix; - } - - ReshuffleTranslator() { - this("rshfl-"); - } - - @Override - public void translate( - PTransform>, PCollection>> transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - - final PCollection> input = ctx.getInput(transform); - final PCollection> output = ctx.getOutput(transform); - final MessageStream>> inputStream = ctx.getMessageStream(input); - // input will be OpMessage of Windowed>> - final KvCoder inputCoder = (KvCoder) input.getCoder(); - final Coder>> elementCoder = SamzaCoders.of(input); - - final MessageStream>> outputStream = - doTranslate( - inputStream, - inputCoder.getKeyCoder(), - elementCoder, - prefix + ctx.getTransformId(), - ctx.getPipelineOptions().getMaxSourceParallelism() > 1); - - ctx.registerMessageStream(output, outputStream); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - - final String inputId = ctx.getInputId(transform); - final MessageStream>> inputStream = ctx.getMessageStreamById(inputId); - final WindowedValues.WindowedValueCoder> windowedInputCoder = - WindowUtils.instantiateWindowedCoder(inputId, pipeline.getComponents()); - final String outputId = ctx.getOutputId(transform); - - final MessageStream>> outputStream = - doTranslate( - inputStream, - ((KvCoder) windowedInputCoder.getValueCoder()).getKeyCoder(), - windowedInputCoder, - prefix + ctx.getTransformId(), - ctx.getPipelineOptions().getMaxSourceParallelism() > 1); - - ctx.registerMessageStream(outputId, outputStream); - } - - private static MessageStream>> doTranslate( - MessageStream>> inputStream, - Coder keyCoder, - Coder>> valueCoder, - String partitionById, // will be used in the intermediate stream name - boolean needRepartition) { - - return needRepartition - ? inputStream - .filter(op -> OpMessage.Type.ELEMENT == op.getType()) - .partitionBy( - opMessage -> opMessage.getElement().getValue().getKey(), - OpMessage::getElement, // windowed value - KVSerde.of(SamzaCoders.toSerde(keyCoder), SamzaCoders.toSerde(valueCoder)), - partitionById) - // convert back to OpMessage - .map(kv -> OpMessage.ofElement(kv.getValue())) - : inputStream.filter(op -> OpMessage.Type.ELEMENT == op.getType()); - } - - /** Predicate to determine whether a URN is a Samza native transform. */ - @AutoService(NativeTransforms.IsNativeTransform.class) - public static class IsSamzaNativeTransform implements NativeTransforms.IsNativeTransform { - @Override - public boolean test(RunnerApi.PTransform pTransform) { - return false; - // Re-enable after https://github.com/apache/beam/issues/21188 is completed - // return PTransformTranslation.RESHUFFLE_URN.equals( - // PTransformTranslation.urnForTransformOrNull(pTransform)); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemFactory.java deleted file mode 100644 index 4035c1610e9f..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemFactory.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.samza.Partition; -import org.apache.samza.config.Config; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemAdmin; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemFactory; -import org.apache.samza.system.SystemProducer; -import org.apache.samza.system.SystemStreamMetadata; -import org.apache.samza.system.SystemStreamPartition; - -/** - * This is a trivial system for generating impulse event in Samza when translating IMPULSE transform - * in portable api. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaImpulseSystemFactory implements SystemFactory { - @Override - public SystemConsumer getConsumer( - String systemName, Config config, MetricsRegistry metricsRegistry) { - return new SamzaImpulseSystemConsumer(); - } - - @Override - public SystemProducer getProducer( - String systemName, Config config, MetricsRegistry metricsRegistry) { - throw new UnsupportedOperationException("SamzaImpulseSystem doesn't support producing"); - } - - @Override - public SystemAdmin getAdmin(String systemName, Config config) { - return new SamzaImpulseSystemAdmin(); - } - - private static final String DUMMY_OFFSET = "0"; - - /** System admin for ImpulseSystem. */ - public static class SamzaImpulseSystemAdmin implements SystemAdmin { - @Override - public Map getOffsetsAfter( - Map offset) { - return offset.keySet().stream() - .collect(Collectors.toMap(Function.identity(), k -> DUMMY_OFFSET)); - } - - @Override - public Map getSystemStreamMetadata(Set streamNames) { - return streamNames.stream() - .collect( - Collectors.toMap( - Function.identity(), - stream -> { - // Impulse system will always be single partition - Map - partitionMetadata = - Collections.singletonMap( - new Partition(0), - new SystemStreamMetadata.SystemStreamPartitionMetadata( - DUMMY_OFFSET, DUMMY_OFFSET, DUMMY_OFFSET)); - return new SystemStreamMetadata(stream, partitionMetadata); - })); - } - - @Override - public Integer offsetComparator(String offset1, String offset2) { - return 0; - } - } - - /** System consumer for ImpulseSystem. */ - public static class SamzaImpulseSystemConsumer implements SystemConsumer { - private AtomicBoolean isEnd = new AtomicBoolean(false); - - @Override - public void start() {} - - @Override - public void stop() {} - - @Override - public void register(SystemStreamPartition ssp, String offset) {} - - private static List constructMessages(SystemStreamPartition ssp) { - final IncomingMessageEnvelope impulseMessage = - new IncomingMessageEnvelope( - ssp, - DUMMY_OFFSET, - /* key */ null, - OpMessage.ofElement(WindowedValues.valueInGlobalWindow(new byte[0]))); - - final IncomingMessageEnvelope watermarkMessage = - IncomingMessageEnvelope.buildWatermarkEnvelope( - ssp, BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()); - - final IncomingMessageEnvelope endOfStreamMessage = - IncomingMessageEnvelope.buildEndOfStreamEnvelope(ssp); - - return Arrays.asList(impulseMessage, watermarkMessage, endOfStreamMessage); - } - - @Override - public Map> poll( - Set ssps, long timeout) throws InterruptedException { - if (isEnd.compareAndSet(false, true)) { - return ssps.stream() - .collect( - Collectors.toMap( - Function.identity(), SamzaImpulseSystemConsumer::constructMessages)); - } else { - return Collections.emptyMap(); - } - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPipelineTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPipelineTranslator.java deleted file mode 100644 index f29588d277ad..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPipelineTranslator.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; - -import com.google.auto.service.AutoService; -import java.util.HashMap; -import java.util.Map; -import java.util.ServiceLoader; -import org.apache.beam.runners.samza.metrics.SamzaMetricOpFactory; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.util.construction.TransformPayloadTranslatorRegistrar; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; - -/** This class knows all the translators from a primitive BEAM transform to a Samza operator. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaPipelineTranslator { - - private static final Map> TRANSLATORS = loadTranslators(); - - private static Map> loadTranslators() { - Map> translators = new HashMap<>(); - for (SamzaTranslatorRegistrar registrar : ServiceLoader.load(SamzaTranslatorRegistrar.class)) { - translators.putAll(registrar.getTransformTranslators()); - } - return ImmutableMap.copyOf(translators); - } - - private SamzaPipelineTranslator() {} - - public static void translate(Pipeline pipeline, TranslationContext ctx) { - final TransformVisitorFn translateFn = - new TransformVisitorFn() { - - @Override - public > void apply( - T transform, - TransformHierarchy.Node node, - Pipeline pipeline, - TransformTranslator translator) { - ctx.setCurrentTransform(node.toAppliedPTransform(pipeline)); - ctx.attachTransformMetricOp( - (PTransform) transform, - node, - SamzaMetricOpFactory.OpType.INPUT); - - translator.translate(transform, node, ctx); - - ctx.attachTransformMetricOp( - (PTransform) transform, - node, - SamzaMetricOpFactory.OpType.OUTPUT); - ctx.clearCurrentTransform(); - } - }; - final SamzaPipelineVisitor visitor = new SamzaPipelineVisitor(translateFn); - pipeline.traverseTopologically(visitor); - } - - public static void createConfig( - Pipeline pipeline, ConfigContext ctx, ConfigBuilder configBuilder) { - - final TransformVisitorFn configFn = - new TransformVisitorFn() { - @Override - public > void apply( - T transform, - TransformHierarchy.Node node, - Pipeline pipeline, - TransformTranslator translator) { - - ctx.setCurrentTransform(node.toAppliedPTransform(pipeline)); - - if (translator instanceof TransformConfigGenerator) { - TransformConfigGenerator configGenerator = - (TransformConfigGenerator) translator; - configBuilder.putAll(configGenerator.createConfig(transform, node, ctx)); - } - - ctx.clearCurrentTransform(); - } - }; - final SamzaPipelineVisitor visitor = new SamzaPipelineVisitor(configFn); - pipeline.traverseTopologically(visitor); - } - - public interface TransformVisitorFn { - > void apply( - T transform, - TransformHierarchy.Node node, - Pipeline pipeline, - TransformTranslator translator); - } - - public static class SamzaPipelineVisitor extends Pipeline.PipelineVisitor.Defaults { - private final TransformVisitorFn visitorFn; - - public SamzaPipelineVisitor(TransformVisitorFn visitorFn) { - this.visitorFn = visitorFn; - } - - @Override - public CompositeBehavior enterCompositeTransform(TransformHierarchy.Node node) { - final PTransform transform = node.getTransform(); - final String urn = getUrnForTransform(transform); - if (canTranslate(urn, transform)) { - applyTransform(transform, node, TRANSLATORS.get(urn)); - return CompositeBehavior.DO_NOT_ENTER_TRANSFORM; - } - return CompositeBehavior.ENTER_TRANSFORM; - } - - @Override - public void visitPrimitiveTransform(TransformHierarchy.Node node) { - final PTransform transform = node.getTransform(); - final String urn = getUrnForTransform(transform); - checkArgument( - canTranslate(urn, transform), - String.format("Unsupported transform class: %s. Node: %s", transform, node)); - - applyTransform(transform, node, TRANSLATORS.get(urn)); - } - - private > void applyTransform( - T transform, TransformHierarchy.Node node, TransformTranslator translator) { - - @SuppressWarnings("unchecked") - final TransformTranslator typedTranslator = (TransformTranslator) translator; - visitorFn.apply(transform, node, getPipeline(), typedTranslator); - } - - private static boolean canTranslate(String urn, PTransform transform) { - if (!TRANSLATORS.containsKey(urn)) { - return false; - } else if (urn.equals(PTransformTranslation.COMBINE_PER_KEY_TRANSFORM_URN)) { - // According to BEAM, Combines with side inputs are translated as generic composites - return ((Combine.PerKey) transform).getSideInputs().isEmpty(); - } else { - return true; - } - } - - private static String getUrnForTransform(PTransform transform) { - return transform == null ? null : PTransformTranslation.urnForTransformOrNull(transform); - } - } - - /** Registers Samza translators. */ - @AutoService(SamzaTranslatorRegistrar.class) - public static class SamzaTranslators implements SamzaTranslatorRegistrar { - - @Override - public Map> getTransformTranslators() { - return ImmutableMap.>builder() - .put(PTransformTranslation.READ_TRANSFORM_URN, new ReadTranslator<>()) - .put(PTransformTranslation.RESHUFFLE_URN, new ReshuffleTranslator<>()) - .put(PTransformTranslation.REDISTRIBUTE_BY_KEY_URN, new RedistributeByKeyTranslator<>()) - .put(PTransformTranslation.PAR_DO_TRANSFORM_URN, new ParDoBoundMultiTranslator<>()) - .put(PTransformTranslation.GROUP_BY_KEY_TRANSFORM_URN, new GroupByKeyTranslator<>()) - .put(PTransformTranslation.COMBINE_PER_KEY_TRANSFORM_URN, new GroupByKeyTranslator<>()) - .put(PTransformTranslation.ASSIGN_WINDOWS_TRANSFORM_URN, new WindowAssignTranslator<>()) - .put(PTransformTranslation.FLATTEN_TRANSFORM_URN, new FlattenPCollectionsTranslator<>()) - .put(SamzaPublishView.SAMZA_PUBLISH_VIEW_URN, new SamzaPublishViewTranslator<>()) - .put(PTransformTranslation.IMPULSE_TRANSFORM_URN, new ImpulseTranslator()) - .put(ExecutableStage.URN, new ParDoBoundMultiTranslator<>()) - .put(PTransformTranslation.TEST_STREAM_TRANSFORM_URN, new SamzaTestStreamTranslator()) - .put( - PTransformTranslation.SPLITTABLE_PROCESS_KEYED_URN, - new SplittableParDoTranslators.ProcessKeyedElements<>()) - .build(); - } - } - - /** Registers classes specialized to the Samza runner. */ - @AutoService(TransformPayloadTranslatorRegistrar.class) - public static class SamzaTransformsRegistrar implements TransformPayloadTranslatorRegistrar { - @Override - public Map< - ? extends Class, - ? extends PTransformTranslation.TransformPayloadTranslator> - getTransformPayloadTranslators() { - return ImmutableMap.of( - SamzaPublishView.class, new SamzaPublishView.SamzaPublishViewPayloadTranslator()); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortablePipelineTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortablePipelineTranslator.java deleted file mode 100644 index 150b1ce90902..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortablePipelineTranslator.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import com.google.auto.service.AutoService; -import java.util.HashMap; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Portable specific samza pipeline translator. This is the entry point for translating a portable - * pipeline - */ -@SuppressWarnings({ - "keyfor", - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaPortablePipelineTranslator { - private static final Logger LOG = LoggerFactory.getLogger(SamzaPortablePipelineTranslator.class); - - private static final Map> TRANSLATORS = loadTranslators(); - - private static Map> loadTranslators() { - Map> translators = new HashMap<>(); - for (SamzaPortableTranslatorRegistrar registrar : - ServiceLoader.load(SamzaPortableTranslatorRegistrar.class)) { - translators.putAll(registrar.getTransformTranslators()); - } - LOG.info("{} translators loaded.", translators.size()); - return ImmutableMap.copyOf(translators); - } - - private SamzaPortablePipelineTranslator() {} - - public static void translate(RunnerApi.Pipeline pipeline, PortableTranslationContext ctx) { - QueryablePipeline queryablePipeline = QueryablePipeline.forPipeline(pipeline); - - for (PipelineNode.PTransformNode transform : - queryablePipeline.getTopologicallyOrderedTransforms()) { - ctx.setCurrentTransform(transform); - - LOG.info("Translating transform urn: {}", transform.getTransform().getSpec().getUrn()); - TRANSLATORS - .get(transform.getTransform().getSpec().getUrn()) - .translatePortable(transform, queryablePipeline, ctx); - - ctx.clearCurrentTransform(); - } - } - - public static void createConfig( - RunnerApi.Pipeline pipeline, ConfigBuilder configBuilder, SamzaPipelineOptions options) { - QueryablePipeline queryablePipeline = QueryablePipeline.forPipeline(pipeline); - for (PipelineNode.PTransformNode transform : - queryablePipeline.getTopologicallyOrderedTransforms()) { - TransformTranslator translator = - TRANSLATORS.get(transform.getTransform().getSpec().getUrn()); - if (translator instanceof TransformConfigGenerator) { - TransformConfigGenerator configGenerator = (TransformConfigGenerator) translator; - configBuilder.putAll(configGenerator.createPortableConfig(transform, options)); - } - } - } - - public static Set knownUrns() { - return TRANSLATORS.keySet(); - } - - /** Registers Samza translators. */ - @AutoService(SamzaPortableTranslatorRegistrar.class) - public static class SamzaTranslators implements SamzaPortableTranslatorRegistrar { - - @Override - public Map> getTransformTranslators() { - return ImmutableMap.>builder() - // Re-enable after https://github.com/apache/beam/issues/21188 is completed - // .put(PTransformTranslation.RESHUFFLE_URN, new ReshuffleTranslator<>()) - .put(PTransformTranslation.GROUP_BY_KEY_TRANSFORM_URN, new GroupByKeyTranslator<>()) - .put(PTransformTranslation.FLATTEN_TRANSFORM_URN, new FlattenPCollectionsTranslator<>()) - .put(PTransformTranslation.IMPULSE_TRANSFORM_URN, new ImpulseTranslator()) - .put(PTransformTranslation.TEST_STREAM_TRANSFORM_URN, new SamzaTestStreamTranslator<>()) - .put(ExecutableStage.URN, new ParDoBoundMultiTranslator<>()) - .build(); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortableTranslatorRegistrar.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortableTranslatorRegistrar.java deleted file mode 100644 index 5eede8f65326..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPortableTranslatorRegistrar.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Map; - -/** A registrar of TransformTranslator in portable pipeline. */ -public interface SamzaPortableTranslatorRegistrar { - Map> getTransformTranslators(); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishView.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishView.java deleted file mode 100644 index 3c09474eda2c..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishView.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.List; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionView; - -/** - * Samza {@link PTransform} that creates a primitive output {@link PCollection}, as the results of a - * {@link PCollectionView}. - */ -class SamzaPublishView - extends PTransform>, PCollection>> { - static final String SAMZA_PUBLISH_VIEW_URN = "beam:transform:samza:publish-view:v1"; - - private final PCollectionView view; - - SamzaPublishView(PCollectionView view) { - this.view = view; - } - - @Override - public PCollection> expand(PCollection> input) { - return PCollection.>createPrimitiveOutputInternal( - input.getPipeline(), input.getWindowingStrategy(), input.isBounded(), input.getCoder()); - } - - public PCollectionView getView() { - return view; - } - - @Override - public String getName() { - return view.getName(); - } - - static class SamzaPublishViewPayloadTranslator - extends PTransformTranslation.TransformPayloadTranslator.NotSerializable< - SamzaPublishView> { - - SamzaPublishViewPayloadTranslator() {} - - @Override - public String getUrn() { - return SAMZA_PUBLISH_VIEW_URN; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTransformOverride.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTransformOverride.java deleted file mode 100644 index 89848147cef4..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTransformOverride.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import org.apache.beam.runners.core.Concatenate; -import org.apache.beam.sdk.runners.AppliedPTransform; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.transforms.View; -import org.apache.beam.sdk.util.construction.SingleInputOutputOverrideFactory; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; - -/** Samza override for {@link View} (side input) transforms. */ -class SamzaPublishViewTransformOverride - extends SingleInputOutputOverrideFactory< - PCollection, PCollection, View.CreatePCollectionView> { - @Override - public PTransformReplacement, PCollection> getReplacementTransform( - AppliedPTransform< - PCollection, PCollection, View.CreatePCollectionView> - transform) { - - @SuppressWarnings("unchecked") - PCollection input = - (PCollection) Iterables.getOnlyElement(transform.getInputs().values()); - - return PTransformReplacement.of( - input, new SamzaCreatePCollectionViewTransform<>(transform.getTransform().getView())); - } - - private static class SamzaCreatePCollectionViewTransform - extends PTransform, PCollection> { - private final PCollectionView view; - - public SamzaCreatePCollectionViewTransform(PCollectionView view) { - this.view = view; - } - - @Override - public PCollection expand(PCollection input) { - // This actually creates a branch in the graph that publishes the view but then returns - // the original input. This is copied from the Flink runner. - input - .apply(Combine.globally(new Concatenate()).withoutDefaults()) - .apply(new SamzaPublishView<>(view)); - return input; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTranslator.java deleted file mode 100644 index 08cf1057aabf..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaPublishViewTranslator.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.List; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.util.SamzaCoders; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.samza.operators.MessageStream; - -/** Translates {@link SamzaPublishView} to a view {@link MessageStream} as side input. */ -class SamzaPublishViewTranslator - implements TransformTranslator> { - - @Override - public void translate( - SamzaPublishView transform, - TransformHierarchy.Node node, - TranslationContext ctx) { - final PCollection> input = ctx.getInput(transform); - final MessageStream>> inputStream = ctx.getMessageStream(input); - @SuppressWarnings("unchecked") - final Coder>> elementCoder = (Coder) SamzaCoders.of(input); - final String viewId = ctx.getViewId(transform.getView()); - - final MessageStream>> outputStream = - doTranslate( - inputStream, elementCoder, ctx.getTransformId(), viewId, ctx.getPipelineOptions()); - - ctx.registerViewStream(transform.getView(), outputStream); - } - - /** - * This method is used to translate both native Java PublishView transform as well as portable - * side input broadcasting into Samza. - */ - static MessageStream>> doTranslate( - MessageStream>> inputStream, - Coder>> coder, - String transformId, - String viewId, - SamzaPipelineOptions options) { - - final MessageStream>> elementStream = - inputStream - .filter(msg -> msg.getType() == OpMessage.Type.ELEMENT) - .map(OpMessage::getElement); - - // TODO: once SAMZA-1580 is resolved, this optimization will go directly inside Samza - final MessageStream>> broadcastStream = - options.getMaxSourceParallelism() == 1 - ? elementStream - : elementStream.broadcast(SamzaCoders.toSerde(coder), "view-" + transformId); - - return broadcastStream.map(element -> OpMessage.ofSideInput(viewId, element)); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamSystemFactory.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamSystemFactory.java deleted file mode 100644 index 72cd711a6acc..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamSystemFactory.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.testing.TestStream; -import org.apache.beam.sdk.transforms.SerializableFunction; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.TimestampedValue; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.samza.Partition; -import org.apache.samza.SamzaException; -import org.apache.samza.config.Config; -import org.apache.samza.config.SystemConfig; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemAdmin; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemFactory; -import org.apache.samza.system.SystemProducer; -import org.apache.samza.system.SystemStreamMetadata; -import org.apache.samza.system.SystemStreamPartition; - -/** - * A Samza system factory that supports consuming from {@link TestStream} and translating events - * into messages according to the {@link org.apache.beam.sdk.testing.TestStream.EventType} of the - * events. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaTestStreamSystemFactory implements SystemFactory { - @Override - public SystemConsumer getConsumer(String systemName, Config config, MetricsRegistry registry) { - final String streamPrefix = String.format(SystemConfig.SYSTEM_ID_PREFIX, systemName); - final Config scopedConfig = config.subset(streamPrefix, true); - return new SamzaTestStreamSystemConsumer<>(getTestStream(scopedConfig)); - } - - @Override - public SystemProducer getProducer(String systemName, Config config, MetricsRegistry registry) { - throw new UnsupportedOperationException("SamzaTestStreamSystem doesn't support producing"); - } - - @Override - public SystemAdmin getAdmin(String systemName, Config config) { - return new SamzaTestStreamSystemAdmin(); - } - - /** A helper function to decode testStream from the config. */ - private static TestStream getTestStream(Config config) { - @SuppressWarnings("unchecked") - final SerializableFunction> testStreamDecoder = - Base64Serializer.deserializeUnchecked( - config.get(SamzaTestStreamTranslator.TEST_STREAM_DECODER), SerializableFunction.class); - return testStreamDecoder.apply(config.get(SamzaTestStreamTranslator.ENCODED_TEST_STREAM)); - } - - private static final String DUMMY_OFFSET = "0"; - - /** System admin for SamzaTestStreamSystem. */ - public static class SamzaTestStreamSystemAdmin implements SystemAdmin { - @Override - public Map getOffsetsAfter( - Map offsets) { - return offsets.keySet().stream() - .collect(Collectors.toMap(Function.identity(), k -> DUMMY_OFFSET)); - } - - @Override - public Map getSystemStreamMetadata(Set streamNames) { - return streamNames.stream() - .collect( - Collectors.toMap( - Function.identity(), - stream -> { - // TestStream will always be single partition - Map - partitionMetadata = - Collections.singletonMap( - new Partition(0), - new SystemStreamMetadata.SystemStreamPartitionMetadata( - DUMMY_OFFSET, DUMMY_OFFSET, DUMMY_OFFSET)); - return new SystemStreamMetadata(stream, partitionMetadata); - })); - } - - @Override - public Integer offsetComparator(String offset1, String offset2) { - return 0; - } - } - - /** System consumer for SamzaTestStreamSystem. */ - public static class SamzaTestStreamSystemConsumer implements SystemConsumer { - TestStream testStream; - - public SamzaTestStreamSystemConsumer(TestStream testStream) { - this.testStream = testStream; - } - - @Override - public void start() {} - - @Override - public void stop() {} - - @Override - public void register(SystemStreamPartition systemStreamPartition, String offset) {} - - @Override - public Map> poll( - Set systemStreamPartitions, long timeout) { - SystemStreamPartition ssp = systemStreamPartitions.iterator().next(); - ArrayList messages = new ArrayList<>(); - - for (TestStream.Event event : testStream.getEvents()) { - if (event.getType().equals(TestStream.EventType.ELEMENT)) { - // If event type is element, for each element, create a message with the element and - // timestamp. - for (TimestampedValue element : ((TestStream.ElementEvent) event).getElements()) { - WindowedValue windowedValue = - WindowedValues.timestampedValueInGlobalWindow( - element.getValue(), element.getTimestamp()); - final OpMessage opMessage = OpMessage.ofElement(windowedValue); - final IncomingMessageEnvelope envelope = - new IncomingMessageEnvelope(ssp, DUMMY_OFFSET, null, opMessage); - messages.add(envelope); - } - } else if (event.getType().equals(TestStream.EventType.WATERMARK)) { - // If event type is watermark, create a watermark message. - long watermarkMillis = ((TestStream.WatermarkEvent) event).getWatermark().getMillis(); - final IncomingMessageEnvelope envelope = - IncomingMessageEnvelope.buildWatermarkEnvelope(ssp, watermarkMillis); - messages.add(envelope); - if (watermarkMillis == BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()) { - // If watermark reached max watermark, also create a end-of-stream message - final IncomingMessageEnvelope endOfStreamMessage = - IncomingMessageEnvelope.buildEndOfStreamEnvelope(ssp); - messages.add(endOfStreamMessage); - break; - } - } else if (event.getType().equals(TestStream.EventType.PROCESSING_TIME)) { - throw new UnsupportedOperationException( - "Advancing Processing time is not supported by the Samza Runner."); - } else { - throw new SamzaException("Unknown event type " + event.getType()); - } - } - - return ImmutableMap.of(ssp, messages); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamTranslator.java deleted file mode 100644 index eb4f8658e5fe..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTestStreamTranslator.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.io.IOException; -import java.util.Map; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.core.serialization.Base64Serializer; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.util.SamzaPipelineTranslatorUtils; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.CoderException; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.testing.TestStream; -import org.apache.beam.sdk.transforms.SerializableFunction; -import org.apache.beam.sdk.util.CoderUtils; -import org.apache.beam.sdk.util.construction.RehydratedComponents; -import org.apache.beam.sdk.util.construction.TestStreamTranslation; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.samza.operators.KV; -import org.apache.samza.serializers.KVSerde; -import org.apache.samza.serializers.NoOpSerde; -import org.apache.samza.serializers.Serde; -import org.apache.samza.system.descriptors.GenericInputDescriptor; -import org.apache.samza.system.descriptors.GenericSystemDescriptor; - -/** - * Translate {@link org.apache.beam.sdk.testing.TestStream} to a samza message stream produced by - * {@link SamzaTestStreamSystemFactory.SamzaTestStreamSystemConsumer}. - */ -@SuppressWarnings({"rawtypes"}) -public class SamzaTestStreamTranslator implements TransformTranslator> { - public static final String ENCODED_TEST_STREAM = "encodedTestStream"; - public static final String TEST_STREAM_DECODER = "testStreamDecoder"; - - @Override - public void translate( - TestStream testStream, TransformHierarchy.Node node, TranslationContext ctx) { - final PCollection output = ctx.getOutput(testStream); - final String outputId = ctx.getIdForPValue(output); - final Coder valueCoder = testStream.getValueCoder(); - final TestStream.TestStreamCoder testStreamCoder = TestStream.TestStreamCoder.of(valueCoder); - - // encode testStream as a string - final String encodedTestStream; - try { - encodedTestStream = CoderUtils.encodeToBase64(testStreamCoder, testStream); - } catch (CoderException e) { - throw new RuntimeException("Could not encode TestStream.", e); - } - - // the decoder for encodedTestStream - SerializableFunction> testStreamDecoder = - string -> { - try { - return CoderUtils.decodeFromBase64(TestStream.TestStreamCoder.of(valueCoder), string); - } catch (CoderException e) { - throw new RuntimeException("Could not decode TestStream.", e); - } - }; - - ctx.registerInputMessageStream( - output, createInputDescriptor(outputId, encodedTestStream, testStreamDecoder)); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - final ByteString bytes = transform.getTransform().getSpec().getPayload(); - final SerializableFunction> testStreamDecoder = - createTestStreamDecoder(pipeline.getComponents(), bytes); - - final String outputId = ctx.getOutputId(transform); - final String escapedOutputId = SamzaPipelineTranslatorUtils.escape(outputId); - - ctx.registerInputMessageStream( - outputId, - createInputDescriptor( - escapedOutputId, Base64Serializer.serializeUnchecked(bytes), testStreamDecoder)); - } - - @SuppressWarnings("unchecked") - private static GenericInputDescriptor>> createInputDescriptor( - String id, - String encodedTestStream, - SerializableFunction> testStreamDecoder) { - final Map systemConfig = - ImmutableMap.of( - ENCODED_TEST_STREAM, - encodedTestStream, - TEST_STREAM_DECODER, - Base64Serializer.serializeUnchecked(testStreamDecoder)); - final GenericSystemDescriptor systemDescriptor = - new GenericSystemDescriptor(id, SamzaTestStreamSystemFactory.class.getName()) - .withSystemConfigs(systemConfig); - - // The KvCoder is needed here for Samza not to crop the key. - final Serde>> kvSerde = KVSerde.of(new NoOpSerde(), new NoOpSerde<>()); - return systemDescriptor.getInputDescriptor(id, kvSerde); - } - - @SuppressWarnings("unchecked") - private static SerializableFunction> createTestStreamDecoder( - RunnerApi.Components components, ByteString payload) { - Coder coder; - try { - coder = - (Coder) - RehydratedComponents.forComponents(components) - .getCoder(RunnerApi.TestStreamPayload.parseFrom(payload).getCoderId()); - } catch (IOException e) { - throw new RuntimeException(e); - } - - // the decoder for encodedTestStream - return encodedTestStream -> { - try { - return TestStreamTranslation.testStreamFromProtoPayload( - RunnerApi.TestStreamPayload.parseFrom( - Base64Serializer.deserializeUnchecked(encodedTestStream, ByteString.class)), - coder); - } catch (IOException e) { - throw new RuntimeException("Could not decode TestStream.", e); - } - }; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTransformOverrides.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTransformOverrides.java deleted file mode 100644 index 3eb169337329..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTransformOverrides.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.List; -import org.apache.beam.sdk.runners.PTransformOverride; -import org.apache.beam.sdk.util.construction.PTransformMatchers; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.util.construction.SplittableParDo; -import org.apache.beam.sdk.util.construction.SplittableParDoNaiveBounded; -import org.apache.beam.sdk.util.construction.UnsupportedOverrideFactory; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; - -/** {@link org.apache.beam.sdk.transforms.PTransform} overrides for Samza runner. */ -@SuppressWarnings({ - "rawtypes" // TODO(https://github.com/apache/beam/issues/20447) -}) -public class SamzaTransformOverrides { - public static List getDefaultOverrides() { - return ImmutableList.builder() - .add( - PTransformOverride.of( - PTransformMatchers.urnEqualTo(PTransformTranslation.CREATE_VIEW_TRANSFORM_URN), - new SamzaPublishViewTransformOverride())) - - // Note that we have a direct replacement for SplittableParDo.ProcessKeyedElements - // for unbounded splittable DoFns and do not need to rely on - // SplittableParDoViaKeyedWorkItems override. Once this direct replacement supports side - // inputs we can remove the SplittableParDoNaiveBounded override. - .add( - PTransformOverride.of( - PTransformMatchers.splittableParDo(), new SplittableParDo.OverrideFactory())) - .add( - PTransformOverride.of( - PTransformMatchers.splittableProcessKeyedBounded(), - new SplittableParDoNaiveBounded.OverrideFactory())) - - // TODO: [https://github.com/apache/beam/issues/19132] Support @RequiresStableInput on Samza - // runner - .add( - PTransformOverride.of( - PTransformMatchers.requiresStableInputParDoMulti(), - UnsupportedOverrideFactory.withMessage( - "Samza runner currently doesn't support @RequiresStableInput annotation."))) - .build(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTranslatorRegistrar.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTranslatorRegistrar.java deleted file mode 100644 index c3eae9a2f9af..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SamzaTranslatorRegistrar.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Map; - -/** A registrar of TransformTranslator. */ -public interface SamzaTranslatorRegistrar { - Map> getTransformTranslators(); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SplittableParDoTranslators.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SplittableParDoTranslators.java deleted file mode 100644 index 2bd6674700c8..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/SplittableParDoTranslators.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.apache.beam.runners.samza.util.SamzaPipelineTranslatorUtils.escape; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.runners.samza.runtime.DoFnOp; -import org.apache.beam.runners.samza.runtime.KvToKeyedWorkItemOp; -import org.apache.beam.runners.samza.runtime.OpAdapter; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.runtime.SplittableParDoProcessKeyedElementsOp; -import org.apache.beam.runners.samza.translation.ParDoBoundMultiTranslator.RawUnionValueToValue; -import org.apache.beam.runners.samza.util.SamzaCoders; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.runners.TransformHierarchy.Node; -import org.apache.beam.sdk.transforms.join.RawUnionValue; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.construction.SplittableParDo; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.serializers.KVSerde; - -/** A set of translators for {@link SplittableParDo}. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SplittableParDoTranslators { - - /** - * Translates {@link SplittableParDo.ProcessKeyedElements} to Samza {@link - * SplittableParDoProcessKeyedElementsOp}. - */ - static class ProcessKeyedElements - implements TransformTranslator< - SplittableParDo.ProcessKeyedElements< - InputT, OutputT, RestrictionT, WatermarkEstimatorStateT>> { - - @Override - public void translate( - SplittableParDo.ProcessKeyedElements< - InputT, OutputT, RestrictionT, WatermarkEstimatorStateT> - transform, - Node node, - TranslationContext ctx) { - final PCollection>> input = ctx.getInput(transform); - - final ArrayList, PCollection>> outputs = - new ArrayList<>(node.getOutputs().entrySet()); - - final Map, Integer> tagToIndexMap = new HashMap<>(); - final Map> indexToPCollectionMap = new HashMap<>(); - - for (int index = 0; index < outputs.size(); ++index) { - final Map.Entry, PCollection> taggedOutput = outputs.get(index); - tagToIndexMap.put(taggedOutput.getKey(), index); - - if (taggedOutput.getValue() == null) { - throw new IllegalArgumentException( - "Expected side output to be PCollection, but was: " + taggedOutput.getValue()); - } - final PCollection sideOutputCollection = (PCollection) taggedOutput.getValue(); - indexToPCollectionMap.put(index, sideOutputCollection); - } - - @SuppressWarnings("unchecked") - final WindowingStrategy windowingStrategy = - (WindowingStrategy) input.getWindowingStrategy(); - - final MessageStream>>> inputStream = - ctx.getMessageStream(input); - - final KvCoder> kvInputCoder = - (KvCoder>) input.getCoder(); - final Coder>>> elementCoder = - SamzaCoders.of(input); - - final MessageStream>>> filteredInputStream = - inputStream.filter(msg -> msg.getType() == OpMessage.Type.ELEMENT); - - final MessageStream>>> partitionedInputStream; - if (!needRepartition(ctx)) { - partitionedInputStream = filteredInputStream; - } else { - partitionedInputStream = - filteredInputStream - .partitionBy( - msg -> msg.getElement().getValue().getKey(), - msg -> msg.getElement(), - KVSerde.of( - SamzaCoders.toSerde(kvInputCoder.getKeyCoder()), - SamzaCoders.toSerde(elementCoder)), - "sdf-" + escape(ctx.getTransformId())) - .map(kv -> OpMessage.ofElement(kv.getValue())); - } - - final MessageStream> taggedOutputStream = - partitionedInputStream - .flatMapAsync(OpAdapter.adapt(new KvToKeyedWorkItemOp<>(), ctx)) - .flatMapAsync( - OpAdapter.adapt( - new SplittableParDoProcessKeyedElementsOp<>( - transform.getMainOutputTag(), - transform, - windowingStrategy, - new DoFnOp.MultiOutputManagerFactory(tagToIndexMap), - ctx.getTransformFullName(), - ctx.getTransformId(), - input.isBounded()), - ctx)); - - for (int outputIndex : tagToIndexMap.values()) { - @SuppressWarnings("unchecked") - final MessageStream> outputStream = - taggedOutputStream - .filter( - message -> - message.getType() != OpMessage.Type.ELEMENT - || message.getElement().getValue().getUnionTag() == outputIndex) - .flatMapAsync(OpAdapter.adapt(new RawUnionValueToValue(), ctx)); - - ctx.registerMessageStream(indexToPCollectionMap.get(outputIndex), outputStream); - } - } - - private static boolean needRepartition(TranslationContext ctx) { - if (ctx.getPipelineOptions().getMaxSourceParallelism() == 1) { - // Only one task will be created, no need for repartition - return false; - } - return true; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/StateIdParser.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/StateIdParser.java deleted file mode 100644 index 05135a4d97dd..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/StateIdParser.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import org.apache.beam.runners.samza.util.StateUtils; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.reflect.DoFnSignature; -import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; - -/** - * This class identifies the set of non-unique state ids by scanning the BEAM {@link Pipeline} with - * a topological traversal. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class StateIdParser extends Pipeline.PipelineVisitor.Defaults { - private final Set nonUniqueStateIds = new HashSet<>(); - private final Set usedStateIds = new HashSet<>(); - - public static Set scan(Pipeline pipeline) { - final StateIdParser parser = new StateIdParser(); - pipeline.traverseTopologically(parser); - return parser.getNonUniqueStateIds(); - } - - private StateIdParser() {} - - @Override - public void visitPrimitiveTransform(TransformHierarchy.Node node) { - if (node.getTransform() instanceof ParDo.MultiOutput) { - final DoFn doFn = ((ParDo.MultiOutput) node.getTransform()).getFn(); - if (StateUtils.isStateful(doFn)) { - final DoFnSignature signature = DoFnSignatures.getSignature(doFn.getClass()); - for (String stateId : signature.stateDeclarations().keySet()) { - if (!usedStateIds.add(stateId)) { - nonUniqueStateIds.add(stateId); - } - } - } - } - } - - public Set getNonUniqueStateIds() { - return Collections.unmodifiableSet(nonUniqueStateIds); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformConfigGenerator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformConfigGenerator.java deleted file mode 100644 index aa956e4b28e0..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformConfigGenerator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collections; -import java.util.Map; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; - -/** Generates config for a BEAM PTransform (regular java api or portable api). */ -public interface TransformConfigGenerator> { - /** Generate config for regular java api PTransform. */ - default Map createConfig( - T transform, TransformHierarchy.Node node, ConfigContext ctx) { - return Collections.emptyMap(); - } - - /** Generate config for portable api PTransform. */ - default Map createPortableConfig( - PipelineNode.PTransformNode transform, SamzaPipelineOptions options) { - return Collections.emptyMap(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformTranslator.java deleted file mode 100644 index 477b5e3a91e2..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TransformTranslator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; - -/** Interface of Samza translator for BEAM {@link PTransform}. */ -public interface TransformTranslator> { - - /** Translates the Java {@link PTransform} into Samza API. */ - default void translate(T transform, TransformHierarchy.Node node, TranslationContext ctx) { - throw new UnsupportedOperationException( - "Java translation is not supported for " + this.getClass().getSimpleName()); - } - - /** - * Translates the portable {@link org.apache.beam.model.pipeline.v1.RunnerApi.PTransform} into - * Samza API. - */ - default void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - throw new UnsupportedOperationException( - "Portable translation is not supported for " + this.getClass().getSimpleName()); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TranslationContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TranslationContext.java deleted file mode 100644 index 4da5b8708f18..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/TranslationContext.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.metrics.SamzaMetricOpFactory; -import org.apache.beam.runners.samza.metrics.SamzaTransformMetricRegistry; -import org.apache.beam.runners.samza.runtime.OpAdapter; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.util.HashIdGenerator; -import org.apache.beam.runners.samza.util.StoreIdGenerator; -import org.apache.beam.sdk.runners.AppliedPTransform; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.util.construction.TransformInputs; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionView; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.samza.application.descriptors.StreamApplicationDescriptor; -import org.apache.samza.config.Config; -import org.apache.samza.config.MapConfig; -import org.apache.samza.operators.KV; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.operators.OutputStream; -import org.apache.samza.serializers.NoOpSerde; -import org.apache.samza.system.EndOfStreamMessage; -import org.apache.samza.system.OutgoingMessageEnvelope; -import org.apache.samza.system.StreamSpec; -import org.apache.samza.system.SystemFactory; -import org.apache.samza.system.SystemProducer; -import org.apache.samza.system.SystemStream; -import org.apache.samza.system.SystemStreamMetadata; -import org.apache.samza.system.WatermarkMessage; -import org.apache.samza.system.descriptors.GenericInputDescriptor; -import org.apache.samza.system.descriptors.GenericSystemDescriptor; -import org.apache.samza.system.descriptors.InputDescriptor; -import org.apache.samza.system.descriptors.OutputDescriptor; -import org.apache.samza.system.inmemory.InMemorySystemFactory; -import org.apache.samza.table.Table; -import org.apache.samza.table.descriptors.TableDescriptor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper that keeps the mapping from BEAM {@link PValue}/{@link PCollectionView} to Samza {@link - * MessageStream}. It also provides other context data such as input and output of a {@link - * PTransform}. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://issues.apache.org/jira/browse/BEAM-10556) - "keyfor", - "nullness" -}) // TODO(https://issues.apache.org/jira/browse/BEAM-10402) -public class TranslationContext { - private static final Logger LOG = LoggerFactory.getLogger(TranslationContext.class); - private final StreamApplicationDescriptor appDescriptor; - private final Map> messsageStreams = new HashMap<>(); - private final Map, MessageStream> viewStreams = new HashMap<>(); - private final Map idMap; - private final Map registeredInputStreams = new HashMap<>(); - private final Map registeredTables = new HashMap<>(); - private final SamzaPipelineOptions options; - private final HashIdGenerator idGenerator = new HashIdGenerator(); - private final StoreIdGenerator storeIdGenerator; - private final SamzaTransformMetricRegistry samzaTransformMetricRegistry; - private AppliedPTransform currentTransform; - - public TranslationContext( - StreamApplicationDescriptor appDescriptor, - Map idMap, - Set nonUniqueStateIds, - SamzaPipelineOptions options) { - this.appDescriptor = appDescriptor; - this.idMap = idMap; - this.options = options; - this.storeIdGenerator = new StoreIdGenerator(nonUniqueStateIds); - this.samzaTransformMetricRegistry = new SamzaTransformMetricRegistry(); - } - - public void registerInputMessageStream( - PValue pvalue, InputDescriptor>, ?> inputDescriptor) { - registerInputMessageStreams(pvalue, Collections.singletonList(inputDescriptor)); - } - - /** - * Function to register a merged messageStream of all input messageStreams to a PCollection. - * - * @param pvalue output of a transform - * @param inputDescriptors a list of Samza InputDescriptors - */ - public void registerInputMessageStreams( - PValue pvalue, List>, ?>> inputDescriptors) { - registerInputMessageStreams(pvalue, inputDescriptors, this::registerMessageStream); - } - - protected void registerInputMessageStreams( - KeyT key, - List>, ?>> inputDescriptors, - BiConsumer>> registerFunction) { - final Set>> streamsToMerge = new HashSet<>(); - for (InputDescriptor>, ?> inputDescriptor : inputDescriptors) { - final String streamId = inputDescriptor.getStreamId(); - // each streamId registered in map should already be add in messageStreamMap - if (registeredInputStreams.containsKey(streamId)) { - @SuppressWarnings("unchecked") - MessageStream> messageStream = registeredInputStreams.get(streamId); - LOG.info( - "Stream id {} has already been mapped to {} stream. Mapping {} to the same message stream.", - streamId, - messageStream, - key); - streamsToMerge.add(messageStream); - } else { - final MessageStream> typedStream = - getValueStream(appDescriptor.getInputStream(inputDescriptor)); - registeredInputStreams.put(streamId, typedStream); - streamsToMerge.add(typedStream); - } - } - - registerFunction.accept(key, MessageStream.mergeAll(streamsToMerge)); - } - - public void registerMessageStream(PValue pvalue, MessageStream> stream) { - if (messsageStreams.containsKey(pvalue)) { - throw new IllegalArgumentException("Stream already registered for pvalue: " + pvalue); - } - messsageStreams.put(pvalue, stream); - } - - // Add a dummy stream for use in special cases (TestStream, empty flatten) - public MessageStream> getDummyStream() { - InputDescriptor, ?> dummyInput = - createDummyStreamDescriptor(UUID.randomUUID().toString()); - return appDescriptor.getInputStream(dummyInput); - } - - public MessageStream> getMessageStream(PValue pvalue) { - @SuppressWarnings("unchecked") - final MessageStream> stream = - (MessageStream>) messsageStreams.get(pvalue); - if (stream == null) { - throw new IllegalArgumentException("No stream registered for pvalue: " + pvalue); - } - return stream; - } - - public void attachTransformMetricOp( - PTransform transform, - TransformHierarchy.Node node, - SamzaMetricOpFactory.OpType opType) { - final Boolean enableTransformMetrics = getPipelineOptions().getEnableTransformMetrics(); - final String transformURN = PTransformTranslation.urnForTransformOrNull(transform); - - // skip attach transform if user override is false or transform is not registered - if (!enableTransformMetrics || transformURN == null) { - return; - } - - // skip attach transform if transform is reading from external sources - if (isIOTransform(node, opType)) { - return; - } - - for (PValue pValue : getPValueForTransform(opType, transform, node)) { - // skip attach transform if pValue is not registered i.e. if not translated with a samza - // translator - if (!messsageStreams.containsKey(pValue)) { - LOG.debug( - "Skip attach transform metric op for pValue: {} for transform: {}", - pValue, - getTransformFullName()); - continue; - } - - // add another step for default metric computation - getMessageStream(pValue) - .flatMapAsync( - OpAdapter.adapt( - SamzaMetricOpFactory.createMetricOp( - transformURN, - pValue.getName(), - getTransformFullName(), - opType, - samzaTransformMetricRegistry), - this)); - } - } - - // Get the input or output PValue for a transform - private List getPValueForTransform( - SamzaMetricOpFactory.OpType opType, - @NonNull PTransform transform, - TransformHierarchy.@NonNull Node node) { - switch (opType) { - case INPUT: - { - if (node.getInputs().size() > 1) { - return node.getInputs().entrySet().stream() - .map(Map.Entry::getValue) - .collect(Collectors.toList()); - } else { - return ImmutableList.of(getInput(transform)); - } - } - case OUTPUT: - if (node.getOutputs().size() > 1) { - return node.getOutputs().entrySet().stream() - .map(Map.Entry::getValue) - .collect(Collectors.toList()); - } - return ImmutableList.of(getOutput(transform)); - default: - throw new IllegalArgumentException("Unknown opType: " + opType); - } - } - - // Transforms that read or write to/from external sources are not supported - private static boolean isIOTransform( - TransformHierarchy.@NonNull Node node, SamzaMetricOpFactory.OpType opType) { - switch (opType) { - case INPUT: - return node.getInputs().size() == 0; - case OUTPUT: - return node.getOutputs().size() == 0; - default: - throw new IllegalArgumentException("Unknown opType: " + opType); - } - } - - public void registerViewStream( - PCollectionView view, MessageStream>> stream) { - if (viewStreams.containsKey(view)) { - throw new IllegalArgumentException("Stream already registered for view: " + view); - } - - viewStreams.put(view, stream); - } - - public MessageStream> getViewStream(PCollectionView view) { - @SuppressWarnings("unchecked") - final MessageStream> stream = - (MessageStream>) viewStreams.get(view); - if (stream == null) { - throw new IllegalArgumentException("No stream registered for view: " + view); - } - return stream; - } - - public String getViewId(PCollectionView view) { - return getIdForPValue(view); - } - - public void setCurrentTransform(AppliedPTransform currentTransform) { - this.currentTransform = currentTransform; - } - - public void clearCurrentTransform() { - this.currentTransform = null; - } - - public AppliedPTransform getCurrentTransform() { - return currentTransform; - } - - @SuppressWarnings("unchecked") - public InT getInput(PTransform transform) { - return (InT) - Iterables.getOnlyElement(TransformInputs.nonAdditionalInputs(this.currentTransform)); - } - - @SuppressWarnings("unchecked") - public OutT getOutput(PTransform transform) { - return (OutT) Iterables.getOnlyElement(this.currentTransform.getOutputs().values()); - } - - @SuppressWarnings("unchecked") - public TupleTag getOutputTag(PTransform> transform) { - return (TupleTag) Iterables.getOnlyElement(this.currentTransform.getOutputs().keySet()); - } - - public SamzaPipelineOptions getPipelineOptions() { - return this.options; - } - - public OutputStream getOutputStream(OutputDescriptor outputDescriptor) { - return appDescriptor.getOutputStream(outputDescriptor); - } - - @SuppressWarnings("unchecked") - public Table> getTable(TableDescriptor tableDesc) { - return registeredTables.computeIfAbsent( - tableDesc.getTableId(), id -> appDescriptor.getTable(tableDesc)); - } - - private static MessageStream getValueStream(MessageStream> input) { - return input.map(KV::getValue); - } - - public String getIdForPValue(PValue pvalue) { - final String id = idMap.get(pvalue); - if (id == null) { - throw new IllegalArgumentException("No id mapping for value: " + pvalue); - } - return id; - } - - public String getTransformFullName() { - return currentTransform.getFullName(); - } - - public String getTransformId() { - return idGenerator.getId(getTransformFullName()); - } - - public StoreIdGenerator getStoreIdGenerator() { - return storeIdGenerator; - } - - /** The dummy stream created will only be used in Beam tests. */ - private static InputDescriptor, ?> createDummyStreamDescriptor(String id) { - final GenericSystemDescriptor dummySystem = - new GenericSystemDescriptor(id, InMemorySystemFactory.class.getName()); - final GenericInputDescriptor> dummyInput = - dummySystem.getInputDescriptor(id, new NoOpSerde<>()); - dummyInput.withOffsetDefault(SystemStreamMetadata.OffsetType.OLDEST); - final Config config = new MapConfig(dummyInput.toConfig(), dummySystem.toConfig()); - final SystemFactory factory = new InMemorySystemFactory(); - final StreamSpec dummyStreamSpec = new StreamSpec(id, id, id, 1); - factory.getAdmin(id, config).createStream(dummyStreamSpec); - - final SystemProducer producer = factory.getProducer(id, config, null); - final SystemStream sysStream = new SystemStream(id, id); - final Consumer sendFn = - (msg) -> { - producer.send(id, new OutgoingMessageEnvelope(sysStream, 0, null, msg)); - }; - final WindowedValue windowedValue = - WindowedValues.timestampedValueInGlobalWindow("dummy", new Instant()); - - sendFn.accept(OpMessage.ofElement(windowedValue)); - sendFn.accept(new WatermarkMessage(BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis())); - sendFn.accept(new EndOfStreamMessage(null)); - return dummyInput; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/WindowAssignTranslator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/WindowAssignTranslator.java deleted file mode 100644 index cad9d4dc23d1..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/WindowAssignTranslator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.samza.runtime.OpAdapter; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.runners.samza.runtime.WindowAssignOp; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.windowing.Window; -import org.apache.beam.sdk.transforms.windowing.WindowFn; -import org.apache.beam.sdk.util.construction.WindowingStrategyTranslation; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.util.construction.graph.QueryablePipeline; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.InvalidProtocolBufferException; -import org.apache.samza.operators.MessageStream; - -/** - * Translates {@link org.apache.beam.sdk.transforms.windowing.Window.Assign} to Samza {@link - * WindowAssignOp}. - */ -class WindowAssignTranslator implements TransformTranslator> { - @Override - public void translate( - Window.Assign transform, TransformHierarchy.Node node, TranslationContext ctx) { - final PCollection output = ctx.getOutput(transform); - - @SuppressWarnings("unchecked") - final WindowFn windowFn = (WindowFn) output.getWindowingStrategy().getWindowFn(); - - final MessageStream> inputStream = ctx.getMessageStream(ctx.getInput(transform)); - - final MessageStream> outputStream = - inputStream.flatMapAsync(OpAdapter.adapt(new WindowAssignOp<>(windowFn), ctx)); - - ctx.registerMessageStream(output, outputStream); - } - - @Override - public void translatePortable( - PipelineNode.PTransformNode transform, - QueryablePipeline pipeline, - PortableTranslationContext ctx) { - final RunnerApi.WindowIntoPayload payload; - try { - payload = - RunnerApi.WindowIntoPayload.parseFrom(transform.getTransform().getSpec().getPayload()); - } catch (InvalidProtocolBufferException e) { - throw new IllegalArgumentException( - String.format("failed to parse WindowIntoPayload: %s", transform.getId()), e); - } - - @SuppressWarnings("unchecked") - final WindowFn windowFn = - (WindowFn) WindowingStrategyTranslation.windowFnFromProto(payload.getWindowFn()); - - final MessageStream> inputStream = ctx.getOneInputMessageStream(transform); - - final MessageStream> outputStream = - inputStream.flatMapAsync(OpAdapter.adapt(new WindowAssignOp<>(windowFn), ctx)); - - ctx.registerMessageStream(ctx.getOutputId(transform), outputStream); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/package-info.java deleted file mode 100644 index 6c25cbbf6055..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/translation/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.translation; diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/ConfigUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/ConfigUtils.java deleted file mode 100644 index e1ce72679252..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/ConfigUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Map; -import org.apache.samza.config.ApplicationConfig; -import org.apache.samza.config.Config; -import org.apache.samza.config.JobConfig; -import org.apache.samza.config.MapConfig; - -/** Util class to operate Samza config. * */ -public class ConfigUtils { - - /** Convert a {@link Map} of config Strings into {@link Config}. */ - public static Config asSamzaConfig(Map config) { - return new MapConfig(config); - } - - /** Convert a {@link Map} of config Strings into {@link JobConfig}. */ - public static JobConfig asJobConfig(Map config) { - return new JobConfig(asSamzaConfig(config)); - } - - /** Convert a {@link Map} of config Strings into {@link JobConfig}. */ - public static ApplicationConfig asApplicationConfig(Map config) { - return new ApplicationConfig(asSamzaConfig(config)); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/DoFnUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/DoFnUtils.java deleted file mode 100644 index 6fd6a6370a1e..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/DoFnUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Set; -import java.util.stream.Collectors; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Splitter; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.commons.collections.CollectionUtils; - -/** Utils for {@link org.apache.beam.runners.samza.runtime.DoFnOp}. */ -public class DoFnUtils { - - public static String toStepName(ExecutableStage executableStage) { - /* - * Look for the first/input ParDo/DoFn in this executable stage by - * matching ParDo/DoFn's input PCollection with executable stage's - * input PCollection - */ - Set inputs = - executableStage.getTransforms().stream() - .filter( - transform -> - transform - .getTransform() - .getInputsMap() - .containsValue(executableStage.getInputPCollection().getId())) - .collect(Collectors.toSet()); - - Set outputIds = - executableStage.getOutputPCollections().stream() - .map(PipelineNode.PCollectionNode::getId) - .collect(Collectors.toSet()); - - /* - * Look for the last/output ParDo/DoFn in this executable stage by - * matching ParDo/DoFn's output PCollection(s) with executable stage's - * out PCollection(s) - */ - Set outputs = - executableStage.getTransforms().stream() - .filter( - transform -> - CollectionUtils.containsAny( - transform.getTransform().getOutputsMap().values(), outputIds)) - .collect(Collectors.toSet()); - - return String.format("[%s-%s]", toStepName(inputs), toStepName(outputs)); - } - - private static String toStepName(Set nodes) { - // TODO: format name when there are multiple input/output PTransform(s) in the ExecutableStage - return nodes.isEmpty() - ? "" - : Iterables.get( - Splitter.on('/').split(nodes.iterator().next().getTransform().getUniqueName()), 0); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/FutureUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/FutureUtils.java deleted file mode 100644 index a6ca03512558..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/FutureUtils.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** A util class to handle java 8 {@link CompletableFuture} and {@link CompletionStage}. */ -@SuppressWarnings({"rawtypes"}) -public final class FutureUtils { - /** - * Flattens the input future collection and returns a single future comprising the results of all - * the futures. - * - * @param inputFutures input future collection - * @param result type of the input future - * @return a single {@link CompletionStage} that contains the results of all the input futures. - */ - public static CompletionStage> flattenFutures( - Collection> inputFutures) { - CompletableFuture[] futures = inputFutures.toArray(new CompletableFuture[0]); - - return CompletableFuture.allOf(futures) - .thenApply( - ignored -> { - final List result = - Stream.of(futures).map(CompletableFuture::join).collect(Collectors.toList()); - return result; - }); - } - - public static CompletionStage> combineFutures( - CompletionStage> future1, CompletionStage> future2) { - - if (future1 == null) { - return future2; - } else if (future2 == null) { - return future1; - } else { - return future1.thenCombine( - future2, - (c1, c2) -> { - c1.addAll(c2); - return c1; - }); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/HashIdGenerator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/HashIdGenerator.java deleted file mode 100644 index ecf2bc6de596..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/HashIdGenerator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.HashSet; -import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class generates hash-based unique ids from String. The id length is the hash length and the - * suffix length combined. Ids generated are guaranteed to be unique, such that same names will be - * hashed to different ids. - */ -public class HashIdGenerator { - private static final Logger LOG = LoggerFactory.getLogger(HashIdGenerator.class); - - private static final int DEFAULT_MAX_HASH_LENGTH = 5; - private final int maxHashLength; - private final Set usedIds = new HashSet<>(); - - public HashIdGenerator(int maxHashLength) { - this.maxHashLength = maxHashLength; - } - - public HashIdGenerator() { - this(DEFAULT_MAX_HASH_LENGTH); - } - - public String getId(String name) { - // Use the id directly if it is unique and the length is less than max - if (name.length() <= maxHashLength && usedIds.add(name)) { - return name; - } - - // Pick the last bytes of hashcode and use hex format - final String hexString = Integer.toHexString(name.hashCode()); - final String origId = - hexString.length() <= maxHashLength - ? hexString - : hexString.substring(Math.max(0, hexString.length() - maxHashLength)); - String id = origId; - int suffixNum = 2; - while (!usedIds.add(id)) { - // A duplicate! Retry. - id = origId + "-" + suffixNum++; - } - LOG.info("Name {} is mapped to id {}", name, id); - return id; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PipelineJsonRenderer.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PipelineJsonRenderer.java deleted file mode 100644 index 1063b053e7a8..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PipelineJsonRenderer.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; - -import com.google.errorprone.annotations.DoNotCall; -import com.google.errorprone.annotations.FormatMethod; -import com.google.errorprone.annotations.FormatString; -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.samza.SamzaRunner; -import org.apache.beam.runners.samza.translation.ConfigContext; -import org.apache.beam.runners.samza.translation.SamzaPipelineTranslator; -import org.apache.beam.runners.samza.translation.TransformTranslator; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.runners.TransformHierarchy; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.gson.JsonArray; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.gson.JsonObject; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.gson.JsonParser; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.samza.config.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A JSON renderer for BEAM {@link Pipeline} DAG. This can help us with visualization of the Beam - * DAG. - */ -public class PipelineJsonRenderer implements Pipeline.PipelineVisitor { - private static final Logger LOG = LoggerFactory.getLogger(PipelineJsonRenderer.class); - private static final String TRANSFORM_IO_MAP_DELIMITER = ","; - - /** - * Interface to get I/O information for a Beam job. This will help add I/O information to the Beam - * DAG. - */ - public interface SamzaIOInfo { - - /** Get I/O topic name and cluster. */ - Optional getIOInfo(TransformHierarchy.Node node); - } - - /** A registrar for {@link SamzaIOInfo}. */ - public interface SamzaIORegistrar { - - SamzaIOInfo getSamzaIO(); - } - - private static final String OUTERMOST_NODE = "OuterMostNode"; - @Nullable private static final SamzaIOInfo SAMZA_IO_INFO = loadSamzaIOInfo(); - - /** - * This method creates a JSON representation of the Beam pipeline. - * - * @param pipeline The beam pipeline - * @param ctx Config context of the pipeline - * @return JSON string representation of the pipeline - */ - public static String toJsonString(Pipeline pipeline, ConfigContext ctx) { - final PipelineJsonRenderer visitor = new PipelineJsonRenderer(ctx); - pipeline.traverseTopologically(visitor); - return visitor.jsonBuilder.toString(); - } - - /** - * This method creates a JSON representation for Beam Portable Pipeline. - * - * @param pipeline The beam portable pipeline - * @return JSON string representation of the pipeline - */ - @DoNotCall("JSON DAG for portable pipeline is not supported yet.") - public static String toJsonString(RunnerApi.Pipeline pipeline) { - throw new UnsupportedOperationException("JSON DAG for portable pipeline is not supported yet."); - } - - private final StringBuilder jsonBuilder = new StringBuilder(); - private final StringBuilder graphLinks = new StringBuilder(); - - private final StringBuilder transformIoInfo = new StringBuilder(); - private final Map valueToProducerNodeName = new HashMap<>(); - private final ConfigContext ctx; - private int indent; - - private PipelineJsonRenderer(ConfigContext ctx) { - this.ctx = ctx; - } - - @Nullable - private static SamzaIOInfo loadSamzaIOInfo() { - final Iterator beamIORegistrarIterator = - ServiceLoader.load(SamzaIORegistrar.class).iterator(); - return beamIORegistrarIterator.hasNext() - ? Iterators.getOnlyElement(beamIORegistrarIterator).getSamzaIO() - : null; - } - - @Override - public void enterPipeline(Pipeline p) { - writeLine("{ \n \"RootNode\": ["); - graphLinks.append(",\"graphLinks\": ["); - - // Do a pre-scan and build transformIoInfo for input and output PValues of each transform - // TODO: Refactor PipelineJsonRenderer to use SamzaPipelineVisitor instead of PipelineVisitor to - // build Beam_JSON_GRAPH - final Map> transformIOMap = buildTransformIOMap(p, ctx); - buildTransformIoJson(transformIOMap); - - enterBlock(); - } - - @Override - public CompositeBehavior enterCompositeTransform(TransformHierarchy.Node node) { - String fullName = node.getFullName(); - writeLine("{ \"fullName\":\"%s\",", assignNodeName(fullName)); - if (node.getEnclosingNode() != null) { - String enclosingNodeName = node.getEnclosingNode().getFullName(); - writeLine(" \"enclosingNode\":\"%s\",", assignNodeName(enclosingNodeName)); - } - - Optional ioInfo = getIOInfo(node); - if (ioInfo.isPresent() && !ioInfo.get().isEmpty()) { - writeLine(" \"ioInfo\":\"%s\",", escapeString(ioInfo.get())); - } - - writeLine(" \"ChildNodes\":["); - enterBlock(); - return CompositeBehavior.ENTER_TRANSFORM; - } - - @Override - public void leaveCompositeTransform(TransformHierarchy.Node node) { - exitBlock(); - writeLine("]},"); - } - - @Override - public void visitPrimitiveTransform(TransformHierarchy.Node node) { - String fullName = node.getFullName(); - writeLine("{ \"fullName\":\"%s\",", escapeString(fullName)); - String enclosingNodeName = node.getEnclosingNode().getFullName(); - writeLine(" \"enclosingNode\":\"%s\"},", assignNodeName(enclosingNodeName)); - - node.getOutputs().values().forEach(x -> valueToProducerNodeName.put(x, fullName)); - node.getInputs() - .forEach( - (key, value) -> { - final String producerName = valueToProducerNodeName.get(value); - graphLinks.append( - String.format("{\"from\":\"%s\"," + "\"to\":\"%s\"},", producerName, fullName)); - }); - } - - @Override - public void visitValue(PValue value, TransformHierarchy.Node producer) {} - - @Override - public void leavePipeline(Pipeline pipeline) { - exitBlock(); - writeLine("]"); - // delete the last comma - int lastIndex = graphLinks.length() - 1; - if (graphLinks.charAt(lastIndex) == ',') { - graphLinks.deleteCharAt(lastIndex); - } - graphLinks.append("]"); - jsonBuilder.append(graphLinks); - // Attach transformIoInfo - transformName to input and output PCollection(PValues) - jsonBuilder.append(transformIoInfo); - jsonBuilder.append("}"); - } - - private void buildTransformIoJson(Map> transformIOMap) { - transformIoInfo.append(",\"transformIOInfo\": ["); - transformIOMap.forEach( - (transform, ioInfo) -> { - transformIoInfo.append( - String.format( - "{\"transformName\":\"%s\"," + "\"inputs\":\"%s\"," + "\"outputs\":\"%s\"},", - transform, ioInfo.getKey(), ioInfo.getValue())); - }); - // delete the last extra comma - int lastIndex = transformIoInfo.length() - 1; - if (transformIoInfo.charAt(lastIndex) == ',') { - transformIoInfo.deleteCharAt(lastIndex); - } - transformIoInfo.append("]"); - } - - private void enterBlock() { - indent += 4; - } - - private void exitBlock() { - indent -= 4; - } - - @FormatMethod - private void writeLine(@FormatString String format, Object... args) { - // Since we append a comma after every entry to the graph, we will need to remove that one extra - // comma towards the end of the JSON. - int secondLastCharIndex = jsonBuilder.length() - 2; - if (jsonBuilder.length() > 1 - && jsonBuilder.charAt(secondLastCharIndex) == ',' - && (format.startsWith("}") || format.startsWith("]"))) { - jsonBuilder.deleteCharAt(secondLastCharIndex); - } - if (indent != 0) { - jsonBuilder.append(String.format("%-" + indent + "s", "")); - } - jsonBuilder.append(String.format(format, args)); - jsonBuilder.append("\n"); - } - - private static String escapeString(String x) { - return x.replace("\"", "\\\""); - } - - private String assignNodeName(String nodeName) { - return escapeString(nodeName.isEmpty() ? OUTERMOST_NODE : nodeName); - } - - private Optional getIOInfo(TransformHierarchy.Node node) { - if (SAMZA_IO_INFO == null) { - return Optional.empty(); - } - return SAMZA_IO_INFO.getIOInfo(node); - } - - /** - * Builds a map from PTransform to its input and output PValues. The map is serialized as part of - * Beam_JSON_GRAPH - * - *

Please note this map needs to be built using SamzaPipelineVisitor instead of generic - * PipelineVisitor used here, reason being SamzaPipelineVisitor traverses the pipeline differently - * i.e. if a composite transform can be translated directly it won't further expand it. - * PipelineVisitor used here is not runner dependent visitor, its just used here for rendering - * purposes - */ - @VisibleForTesting - static Map> buildTransformIOMap( - Pipeline pipeline, ConfigContext ctx) { - final Map> pTransformToInputOutputMap = new HashMap<>(); - final SamzaPipelineTranslator.TransformVisitorFn configFn = - new SamzaPipelineTranslator.TransformVisitorFn() { - @Override - public > void apply( - T transform, - TransformHierarchy.Node node, - Pipeline pipeline, - TransformTranslator translator) { - ctx.setCurrentTransform(node.toAppliedPTransform(pipeline)); - List inputs = getIOPValueList(node.getInputs()).get(); - List outputs = getIOPValueList(node.getOutputs()).get(); - pTransformToInputOutputMap.put( - node.getFullName(), - new AbstractMap.SimpleEntry<>( - String.join(TRANSFORM_IO_MAP_DELIMITER, inputs), - String.join(TRANSFORM_IO_MAP_DELIMITER, outputs))); - ctx.clearCurrentTransform(); - } - }; - - final SamzaPipelineTranslator.SamzaPipelineVisitor visitor = - new SamzaPipelineTranslator.SamzaPipelineVisitor(configFn); - pipeline.traverseTopologically(visitor); - return pTransformToInputOutputMap; - } - - private static Supplier> getIOPValueList(Map, PCollection> map) { - return () -> map.values().stream().map(pColl -> pColl.getName()).collect(Collectors.toList()); - } - - // Reads the config to build transformIOMap, i.e. map of inputs & output PValues for each - // PTransform - public static Map, List>> getTransformIOMap( - Config config) { - checkNotNull(config, "Config cannot be null"); - final Map, List>> result = new HashMap<>(); - final String pipelineJsonGraph = config.get(SamzaRunner.BEAM_JSON_GRAPH); - if (pipelineJsonGraph == null) { - LOG.warn( - "Cannot build transformIOMap since Config: {} is found null ", - SamzaRunner.BEAM_JSON_GRAPH); - return result; - } - JsonObject jsonObject = JsonParser.parseString(pipelineJsonGraph).getAsJsonObject(); - JsonArray transformIOInfo = jsonObject.getAsJsonArray("transformIOInfo"); - transformIOInfo.forEach( - transform -> { - final String transformName = - transform.getAsJsonObject().get("transformName").getAsString(); - final String inputs = transform.getAsJsonObject().get("inputs").getAsString(); - final String outputs = transform.getAsJsonObject().get("outputs").getAsString(); - result.put(transformName, new AbstractMap.SimpleEntry<>(ioFunc(inputs), ioFunc(outputs))); - }); - return result; - } - - private static List ioFunc(String ioList) { - return Arrays.stream(ioList.split(PipelineJsonRenderer.TRANSFORM_IO_MAP_DELIMITER)) - .filter(item -> !item.isEmpty()) - .collect(Collectors.toList()); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java deleted file mode 100644 index ee0bd7b72bb0..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Map; -import org.apache.beam.runners.samza.SamzaPipelineOptions; - -/** A utility class to encapsulate. */ -public class PortableConfigUtils { - public static final String BEAM_PORTABLE_MODE = "beam.portable.mode"; - - private PortableConfigUtils() {} - - /** - * A helper method to distinguish if a pipeline is run using portable mode or classic mode. - * - * @param options pipeline options - * @return true if the pipeline is run in portable mode - */ - public static boolean isPortable(SamzaPipelineOptions options) { - Map override = options.getConfigOverride(); - if (override == null) { - return false; - } - - return Boolean.parseBoolean(override.getOrDefault(BEAM_PORTABLE_MODE, "false")); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaCoders.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaCoders.java deleted file mode 100644 index f546303b9b9b..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaCoders.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.samza.serializers.Serde; - -/** Utils for Coders creation/conversion in Samza. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaCoders { - - private SamzaCoders() {} - - public static Coder> of(PCollection pCollection) { - final Coder coder = pCollection.getCoder(); - final Coder windowCoder = - pCollection.getWindowingStrategy().getWindowFn().windowCoder(); - return WindowedValues.FullWindowedValueCoder.of(coder, windowCoder); - } - - public static Serde toSerde(final Coder coder) { - return new Serde() { - @Override - public T fromBytes(byte[] bytes) { - if (bytes != null) { - final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - try { - return (T) coder.decode(bais); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - return null; - } - } - - @Override - public byte[] toBytes(T t) { - if (t != null) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - coder.encode(t, baos); - } catch (IOException e) { - throw new RuntimeException(e); - } - return baos.toByteArray(); - } else { - return null; - } - } - }; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineExceptionListener.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineExceptionListener.java deleted file mode 100644 index 7715b183b5c9..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineExceptionListener.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import org.apache.beam.runners.samza.SamzaPipelineExceptionContext; -import org.apache.beam.runners.samza.SamzaPipelineOptions; - -/** - * An ExceptionListener following Observer pattern. Any runtime exception caught by {@code - * OpAdapter} will be notified to any concrete SamzaPipelineExceptionListener at Runtime - */ -public interface SamzaPipelineExceptionListener { - - void onException(SamzaPipelineExceptionContext exceptionContext); - - interface Registrar { - SamzaPipelineExceptionListener getExceptionListener(SamzaPipelineOptions samzaPipelineOptions); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineTranslatorUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineTranslatorUtils.java deleted file mode 100644 index 999506e1e021..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/SamzaPipelineTranslatorUtils.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.io.IOException; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.wire.WireCoders; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowedValues; - -/** Utilities for pipeline translation. */ -@SuppressWarnings({ - "rawtypes" // TODO(https://github.com/apache/beam/issues/20447) -}) -public final class SamzaPipelineTranslatorUtils { - private SamzaPipelineTranslatorUtils() {} - - public static WindowedValues.WindowedValueCoder instantiateCoder( - String collectionId, RunnerApi.Components components) { - PipelineNode.PCollectionNode collectionNode = - PipelineNode.pCollection(collectionId, components.getPcollectionsOrThrow(collectionId)); - try { - return (WindowedValues.WindowedValueCoder) - WireCoders.instantiateRunnerWireCoder(collectionNode, components); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Escape the non-alphabet chars in the name so we can create a physical stream out of it. - * - *

This escape will replace any non-alphanumeric characters other than "-" and "_" with "_" - * including whitespace. - */ - public static String escape(String name) { - return name.replaceFirst(".*:([a-zA-Z#0-9]+).*", "$1").replaceAll("[^A-Za-z0-9_-]", "_"); - } - - public static PCollection.IsBounded isBounded(RunnerApi.PCollection pCollection) { - return pCollection.getIsBounded() == RunnerApi.IsBounded.Enum.BOUNDED - ? PCollection.IsBounded.BOUNDED - : PCollection.IsBounded.UNBOUNDED; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/StateUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/StateUtils.java deleted file mode 100644 index 1db9db74074e..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/StateUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; - -/** Utils for determining stateful operators. */ -public class StateUtils { - - public static boolean isStateful(DoFn doFn) { - return DoFnSignatures.isStateful(doFn); - } - - public static boolean isStateful(RunnerApi.ExecutableStagePayload stagePayload) { - return stagePayload.getUserStatesCount() > 0 || stagePayload.getTimersCount() > 0; - } - - public static boolean isStateful(ExecutableStage executableStage) { - return executableStage.getUserStates().size() > 0 || executableStage.getTimers().size() > 0; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/StoreIdGenerator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/StoreIdGenerator.java deleted file mode 100644 index aefba0186c8d..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/StoreIdGenerator.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Set; - -/** - * This class encapsulates the logic to generate unique store id. For unique state ids across the - * Beam pipeline, store id is the same as the state id. For non-unique state ids, join the state id - * with an escaped transform name to generate a unique store id. - */ -public class StoreIdGenerator { - - private final Set nonUniqueStateIds; - - public StoreIdGenerator(Set nonUniqueStateId) { - this.nonUniqueStateIds = nonUniqueStateId; - } - - public String getId(String stateId, String transformFullName) { - String storeId = stateId; - if (nonUniqueStateIds.contains(stateId)) { - final String escapedName = SamzaPipelineTranslatorUtils.escape(transformFullName); - storeId = toUniqueStoreId(stateId, escapedName); - } - return storeId; - } - - /** Join state id and escaped PTransform name to uniquify store id. */ - private static String toUniqueStoreId(String stateId, String escapedPTransformName) { - return String.join("-", stateId, escapedPTransformName); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/WindowUtils.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/WindowUtils.java deleted file mode 100644 index 7f39a9739c59..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/WindowUtils.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.io.IOException; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.wire.WireCoders; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.construction.RehydratedComponents; -import org.apache.beam.sdk.util.construction.WindowingStrategyTranslation; -import org.apache.beam.sdk.util.construction.graph.PipelineNode; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.sdk.values.WindowingStrategy; - -/** Utils for window operations. */ -public class WindowUtils { - - /** Get {@link WindowingStrategy} of given collection id from {@link RunnerApi.Components}. */ - public static WindowingStrategy getWindowStrategy( - String collectionId, RunnerApi.Components components) { - RehydratedComponents rehydratedComponents = RehydratedComponents.forComponents(components); - - RunnerApi.WindowingStrategy windowingStrategyProto = - components.getWindowingStrategiesOrThrow( - components.getPcollectionsOrThrow(collectionId).getWindowingStrategyId()); - - WindowingStrategy windowingStrategy; - try { - windowingStrategy = - WindowingStrategyTranslation.fromProto(windowingStrategyProto, rehydratedComponents); - } catch (Exception e) { - throw new IllegalStateException( - String.format( - "Unable to hydrate GroupByKey windowing strategy %s.", windowingStrategyProto), - e); - } - - @SuppressWarnings("unchecked") - WindowingStrategy ret = - (WindowingStrategy) windowingStrategy; - return ret; - } - - /** - * Instantiate {@link WindowedValues.WindowedValueCoder} for given collection id from {@link - * RunnerApi.Components}. - */ - public static WindowedValues.WindowedValueCoder instantiateWindowedCoder( - String collectionId, RunnerApi.Components components) { - PipelineNode.PCollectionNode collectionNode = - PipelineNode.pCollection(collectionId, components.getPcollectionsOrThrow(collectionId)); - try { - return (WindowedValues.WindowedValueCoder) - WireCoders.instantiateRunnerWireCoder(collectionNode, components); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java deleted file mode 100644 index 23290fd4ffbb..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.util; diff --git a/runners/samza/src/main/resources/log4j.properties b/runners/samza/src/main/resources/log4j.properties deleted file mode 100644 index 3ad91be1b55a..000000000000 --- a/runners/samza/src/main/resources/log4j.properties +++ /dev/null @@ -1,23 +0,0 @@ -################################################################################ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -################################################################################ - -# Update root logger to WARN and add log4j.category.org.apache.beam=INFO if executing in Intellij -log4j.rootLogger=INFO,console -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.layout=org.apache.log4j.PatternLayout -log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n diff --git a/runners/samza/src/main/resources/samza-conf.properties b/runners/samza/src/main/resources/samza-conf.properties deleted file mode 100644 index 80ec36826797..000000000000 --- a/runners/samza/src/main/resources/samza-conf.properties +++ /dev/null @@ -1,37 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# License); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# job config -processor.id=1 - -# default kafka system config -job.default.system=default -systems.default.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory -systems.default.consumer.zookeeper.connect=localhost:2181 -systems.default.producer.bootstrap.servers=localhost:9092 -systems.default.default.stream.replication.factor=1 - -# local deployment -app.runner.class=org.apache.samza.runtime.LocalApplicationRunner -job.coordinator.factory=org.apache.samza.standalone.PassthroughJobCoordinatorFactory -job.coordination.utils.factory=org.apache.samza.standalone.PassthroughCoordinationUtilsFactory -task.name.grouper.factory=org.apache.samza.container.grouper.task.SingleContainerGrouperFactory -task.commit.ms=-1 - -# jmx metrics reporter -metrics.reporters=jmx -metrics.reporter.jmx.class=org.apache.samza.metrics.reporter.JmxReporterFactory \ No newline at end of file diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidatorTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidatorTest.java deleted file mode 100644 index 2ef56c7ce63c..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidatorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import static org.apache.beam.runners.samza.SamzaPipelineOptionsValidator.validateBundlingRelatedOptions; -import static org.apache.samza.config.JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.Map; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.junit.Test; - -/** Test for {@link SamzaPipelineOptionsValidator}. */ -public class SamzaPipelineOptionsValidatorTest { - - @Test(expected = IllegalArgumentException.class) - public void testBundleEnabledInMultiThreadedModeThrowsException() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - Map config = ImmutableMap.of(JOB_CONTAINER_THREAD_POOL_SIZE, "10"); - - when(mockOptions.getMaxBundleSize()).thenReturn(2L); - when(mockOptions.getConfigOverride()).thenReturn(config); - validateBundlingRelatedOptions(mockOptions); - } - - @Test - public void testBundleEnabledInSingleThreadedMode() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - when(mockOptions.getMaxBundleSize()).thenReturn(2L); - - try { - Map config = ImmutableMap.of(JOB_CONTAINER_THREAD_POOL_SIZE, "1"); - when(mockOptions.getConfigOverride()).thenReturn(config); - validateBundlingRelatedOptions(mockOptions); - - // In the absence of configuration make sure it is treated as single threaded mode. - when(mockOptions.getConfigOverride()).thenReturn(Collections.emptyMap()); - validateBundlingRelatedOptions(mockOptions); - } catch (Exception e) { - throw new AssertionError("Bundle size > 1 should be supported in single threaded mode"); - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystemTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystemTest.java deleted file mode 100644 index a6bc9940a745..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/BoundedSourceSystemTest.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.createElementMessage; -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.createEndOfStreamMessage; -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.createWatermarkMessage; -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.expectWrappedException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.sdk.io.BoundedSource; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; -import org.apache.samza.Partition; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemStreamPartition; -import org.joda.time.Instant; -import org.junit.Test; - -/** Tests for {@link BoundedSourceSystem}. */ -public class BoundedSourceSystemTest { - private static final SystemStreamPartition DEFAULT_SSP = - new SystemStreamPartition("default-system", "default-system", new Partition(0)); - - // A reasonable time to wait to get all messages from the bounded source assuming no blocking. - private static final long DEFAULT_TIMEOUT_MILLIS = 1000; - private static final String NULL_STRING = null; - - @Test - public void testConsumerStartStop() throws IOException, InterruptedException { - final TestBoundedSource source = TestBoundedSource.createBuilder().build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - assertEquals( - Arrays.asList( - createWatermarkMessage(DEFAULT_SSP, BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(DEFAULT_SSP)), - consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testConsumeOneMessage() throws IOException, InterruptedException { - final TestBoundedSource source = - TestBoundedSource.createBuilder().addElements("test").build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, "0", "test", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(DEFAULT_SSP, BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(DEFAULT_SSP)), - consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testAdvanceTimestamp() throws InterruptedException { - final Instant timestamp = Instant.now(); - - final TestBoundedSource source = - TestBoundedSource.createBuilder() - .addElements("before") - .setTimestamp(timestamp) - .addElements("after") - .build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, "0", "before", BoundedWindow.TIMESTAMP_MIN_VALUE), - createElementMessage(DEFAULT_SSP, "1", "after", timestamp), - createWatermarkMessage(DEFAULT_SSP, BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(DEFAULT_SSP)), - consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testConsumeMultipleMessages() throws IOException, InterruptedException { - final TestBoundedSource source = - TestBoundedSource.createBuilder() - .addElements("test", "a", "few", "messages") - .build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, "0", "test", BoundedWindow.TIMESTAMP_MIN_VALUE), - createElementMessage(DEFAULT_SSP, "1", "a", BoundedWindow.TIMESTAMP_MIN_VALUE), - createElementMessage(DEFAULT_SSP, "2", "few", BoundedWindow.TIMESTAMP_MIN_VALUE), - createElementMessage(DEFAULT_SSP, "3", "messages", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(DEFAULT_SSP, BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(DEFAULT_SSP)), - consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testReaderThrowsAtStart() throws Exception { - final IOException exception = new IOException("Expected exception"); - - final TestBoundedSource source = - TestBoundedSource.createBuilder().addException(exception).build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - expectWrappedException( - exception, () -> consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testReaderThrowsAtAdvance() throws Exception { - final IOException exception = new IOException("Expected exception"); - - final TestBoundedSource source = - TestBoundedSource.createBuilder() - .addElements("test", "a", "few", "good", "messages", "then", "...") - .addException(exception) - .build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - expectWrappedException( - exception, () -> consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testTimeout() throws Exception { - final CountDownLatch advanceLatch = new CountDownLatch(1); - - final TestBoundedSource source = - TestBoundedSource.createBuilder() - .addElements("before") - .addLatch(advanceLatch) - .addElements("after") - .build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source); - - consumer.register(DEFAULT_SSP, "0"); - consumer.start(); - assertEquals( - Collections.singletonList( - createElementMessage(DEFAULT_SSP, "0", "before", BoundedWindow.TIMESTAMP_MIN_VALUE)), - consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - - advanceLatch.countDown(); - - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, "1", "after", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(DEFAULT_SSP, BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(DEFAULT_SSP)), - consumeUntilTimeoutOrEos(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testSplit() throws IOException, InterruptedException { - final TestBoundedSource.SplittableBuilder builder = - TestBoundedSource.createSplits(3); - builder.forSplit(0).addElements("split-0"); - builder.forSplit(1).addElements("split-1"); - builder.forSplit(2).addElements("split-2"); - final TestBoundedSource source = builder.build(); - - final BoundedSourceSystem.Consumer consumer = createConsumer(source, 3); - - consumer.register(ssp(0), NULL_STRING); - consumer.register(ssp(1), NULL_STRING); - consumer.register(ssp(2), NULL_STRING); - consumer.start(); - - final Set offsets = new HashSet<>(); - - // check split0 - List envelopes = - consumeUntilTimeoutOrEos(consumer, ssp(0), DEFAULT_TIMEOUT_MILLIS); - assertEquals( - Arrays.asList( - createElementMessage( - ssp(0), envelopes.get(0).getOffset(), "split-0", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(ssp(0), BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(ssp(0))), - envelopes); - offsets.add(envelopes.get(0).getOffset()); - - // check split1 - envelopes = consumeUntilTimeoutOrEos(consumer, ssp(1), DEFAULT_TIMEOUT_MILLIS); - assertEquals( - Arrays.asList( - createElementMessage( - ssp(1), envelopes.get(0).getOffset(), "split-1", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(ssp(1), BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(ssp(1))), - envelopes); - offsets.add(envelopes.get(0).getOffset()); - - // check split2 - envelopes = consumeUntilTimeoutOrEos(consumer, ssp(2), DEFAULT_TIMEOUT_MILLIS); - assertEquals( - Arrays.asList( - createElementMessage( - ssp(2), envelopes.get(0).getOffset(), "split-2", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(ssp(2), BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(ssp(2))), - envelopes); - offsets.add(envelopes.get(0).getOffset()); - - // check offsets - assertEquals(Sets.newHashSet("0", "1", "2"), offsets); - consumer.stop(); - } - - private static List consumeUntilTimeoutOrEos( - SystemConsumer consumer, SystemStreamPartition ssp, long timeoutMillis) - throws InterruptedException { - assertTrue("Expected timeoutMillis (" + timeoutMillis + ") >= 0", timeoutMillis >= 0); - - final List accumulator = new ArrayList<>(); - final long start = System.currentTimeMillis(); - long now = start; - while (timeoutMillis + start >= now) { - accumulator.addAll(pollOnce(consumer, ssp, now - start - timeoutMillis)); - if (!accumulator.isEmpty() && accumulator.get(accumulator.size() - 1).isEndOfStream()) { - break; - } - now = System.currentTimeMillis(); - } - return accumulator; - } - - private static List pollOnce( - SystemConsumer consumer, SystemStreamPartition ssp, long timeoutMillis) - throws InterruptedException { - final Set sspSet = Collections.singleton(ssp); - final Map> pollResult = - consumer.poll(sspSet, timeoutMillis); - assertEquals(sspSet, pollResult.keySet()); - assertNotNull(pollResult.get(ssp)); - return pollResult.get(ssp); - } - - private static BoundedSourceSystem.Consumer createConsumer(BoundedSource source) { - return createConsumer(source, 1); - } - - private static BoundedSourceSystem.Consumer createConsumer( - BoundedSource source, int splitNum) { - SamzaPipelineOptions pipelineOptions = PipelineOptionsFactory.as(SamzaPipelineOptions.class); - pipelineOptions.setMaxSourceParallelism(splitNum); - return new BoundedSourceSystem.Consumer<>( - source, pipelineOptions, new SamzaMetricsContainer(new MetricsRegistryMap()), "test-step"); - } - - private static SystemStreamPartition ssp(int partition) { - return new SystemStreamPartition("default-system", "default-system", new Partition(partition)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestBoundedSource.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestBoundedSource.java deleted file mode 100644 index 928d2887eec2..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestBoundedSource.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.stream.Collectors; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.ElementEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.Event; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.ExceptionEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.LatchEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.SourceBuilder; -import org.apache.beam.sdk.io.BoundedSource; -import org.apache.beam.sdk.options.PipelineOptions; -import org.joda.time.Instant; - -/** A bounded source that can be used for test purposes. */ -public class TestBoundedSource extends BoundedSource { - // each list of events is a split - private final List>> events; - - public static Builder createBuilder() { - return new Builder<>(); - } - - public static SplittableBuilder createSplits(int numSplits) { - return new SplittableBuilder<>(numSplits); - } - - private TestBoundedSource(List>> events) { - this.events = Collections.unmodifiableList(new ArrayList<>(events)); - } - - @Override - public List> split( - long desiredBundleSizeBytes, PipelineOptions options) throws Exception { - return events.stream() - .map(ev -> new TestBoundedSource<>(Collections.singletonList(ev))) - .collect(Collectors.toList()); - } - - @Override - public long getEstimatedSizeBytes(PipelineOptions options) throws Exception { - return events.size(); - } - - @Override - public BoundedReader createReader(PipelineOptions options) throws IOException { - assert events.size() == 1; - - return new Reader(events.get(0)); - } - - @Override - public void validate() {} - - /** A builder used to populate the events emitted by {@link TestBoundedSource}. */ - public static class Builder extends SourceBuilder> { - @Override - public TestBoundedSource build() { - return new TestBoundedSource<>(Collections.singletonList(getEvents())); - } - } - - /** - * A SplittableBuilder supports multiple splits and each split {@link TestUnboundedSource} can be - * built separately from the above Builder. - */ - public static class SplittableBuilder extends SourceBuilder> { - private final List> builders = new ArrayList<>(); - - private SplittableBuilder(int splits) { - while (splits != 0) { - builders.add(new Builder()); - --splits; - } - } - - @Override - public TestBoundedSource build() { - final List>> events = new ArrayList<>(); - builders.forEach(builder -> events.add(builder.getEvents())); - return new TestBoundedSource<>(events); - } - - public Builder forSplit(int split) { - return builders.get(split); - } - } - - private class Reader extends BoundedReader { - private final List> events; - private boolean started; - private boolean finished; - private int index = -1; - - private Reader(List> events) { - this.events = events; - } - - @Override - public boolean start() throws IOException { - if (started) { - throw new IllegalStateException("Start called when reader was already started"); - } - started = true; - return advance(); - } - - @Override - public boolean advance() throws IOException { - if (!started) { - throw new IllegalStateException("Advance called when reader was not started"); - } - - if (finished) { - return false; - } - - for (++index; index < events.size(); ++index) { - final Event event = events.get(index); - if (event instanceof ExceptionEvent) { - throw ((ExceptionEvent) event).exception; - } else if (event instanceof LatchEvent) { - try { - ((LatchEvent) event).latch.await(); - } catch (InterruptedException e) { - // Propagate interrupt - Thread.currentThread().interrupt(); - } - } else { - return true; - } - } - - finished = true; - return false; - } - - @Override - public T getCurrent() throws NoSuchElementException { - if (!started || finished) { - throw new NoSuchElementException(); - } - - final Event event = events.get(index); - assert event instanceof ElementEvent; - return ((ElementEvent) event).element; - } - - @Override - public Instant getCurrentTimestamp() throws NoSuchElementException { - if (!started || finished) { - throw new NoSuchElementException(); - } - - final Event event = events.get(index); - assert event instanceof ElementEvent; - return ((ElementEvent) event).timestamp; - } - - @Override - public void close() throws IOException {} - - @Override - public BoundedSource getCurrentSource() { - return TestBoundedSource.this; - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestCheckpointMark.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestCheckpointMark.java deleted file mode 100644 index d526d859dea4..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestCheckpointMark.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import java.io.IOException; -import java.io.Serializable; -import org.apache.beam.sdk.io.UnboundedSource; - -/** A integer CheckpointMark for testing. */ -public class TestCheckpointMark implements UnboundedSource.CheckpointMark, Serializable { - final int checkpoint; - - private TestCheckpointMark(int checkpoint) { - this.checkpoint = checkpoint; - } - - @Override - public void finalizeCheckpoint() throws IOException { - // DO NOTHING - } - - static TestCheckpointMark of(int checkpoint) { - return new TestCheckpointMark(checkpoint); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestSourceHelpers.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestSourceHelpers.java deleted file mode 100644 index 60c0c27b8d1f..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestSourceHelpers.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.io.Source; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemStreamPartition; -import org.joda.time.Instant; - -/** Helper classes and functions to build source for testing. */ -public class TestSourceHelpers { - - private TestSourceHelpers() {} - - interface Event {} - - static class ElementEvent implements Event { - final T element; - final Instant timestamp; - - private ElementEvent(T element, Instant timestamp) { - this.element = element; - this.timestamp = timestamp; - } - } - - static class WatermarkEvent implements Event { - final Instant watermark; - - private WatermarkEvent(Instant watermark) { - this.watermark = watermark; - } - } - - static class ExceptionEvent implements Event { - final IOException exception; - - private ExceptionEvent(IOException exception) { - this.exception = exception; - } - } - - static class LatchEvent implements Event { - final CountDownLatch latch; - - private LatchEvent(CountDownLatch latch) { - this.latch = latch; - } - } - - static class NoElementEvent implements Event {} - - /** A builder used to populate the events emitted by {@link TestBoundedSource}. */ - abstract static class SourceBuilder> { - private final List> events = new ArrayList<>(); - private Instant currentTimestamp = BoundedWindow.TIMESTAMP_MIN_VALUE; - - @SafeVarargs - public final SourceBuilder addElements(T... elements) { - for (T element : elements) { - events.add(new ElementEvent<>(element, currentTimestamp)); - } - return this; - } - - public SourceBuilder addException(IOException exception) { - events.add(new ExceptionEvent<>(exception)); - return this; - } - - public SourceBuilder addLatch(CountDownLatch latch) { - events.add(new LatchEvent<>(latch)); - return this; - } - - public SourceBuilder setTimestamp(Instant timestamp) { - assertTrue( - "Expected " + timestamp + " to be greater than or equal to " + currentTimestamp, - timestamp.isEqual(currentTimestamp) || timestamp.isAfter(currentTimestamp)); - currentTimestamp = timestamp; - return this; - } - - public SourceBuilder advanceWatermarkTo(Instant watermark) { - events.add(new WatermarkEvent<>(watermark)); - return this; - } - - public SourceBuilder noElements() { - events.add(new NoElementEvent()); - return this; - } - - protected List> getEvents() { - return this.events; - } - - public abstract W build(); - } - - static IncomingMessageEnvelope createElementMessage( - SystemStreamPartition ssp, String offset, String element, Instant timestamp) { - return new IncomingMessageEnvelope( - ssp, - offset, - null, - OpMessage.ofElement(WindowedValues.timestampedValueInGlobalWindow(element, timestamp))); - } - - static IncomingMessageEnvelope createWatermarkMessage( - SystemStreamPartition ssp, Instant watermark) { - return IncomingMessageEnvelope.buildWatermarkEnvelope(ssp, watermark.getMillis()); - } - - static IncomingMessageEnvelope createEndOfStreamMessage(SystemStreamPartition ssp) { - return IncomingMessageEnvelope.buildEndOfStreamEnvelope(ssp); - } - - static void expectWrappedException(Exception expectedException, Callable callable) - throws Exception { - try { - callable.call(); - fail("Expected exception (" + expectedException + "), but no exception was thrown"); - } catch (Exception e) { - Throwable currentException = e; - while (currentException != null) { - if (currentException.equals(expectedException)) { - return; - } - currentException = currentException.getCause(); - } - assertEquals(expectedException, e); - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestUnboundedSource.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestUnboundedSource.java deleted file mode 100644 index 0439354d67ea..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/TestUnboundedSource.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.stream.Collectors; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.ElementEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.Event; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.ExceptionEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.LatchEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.NoElementEvent; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.SourceBuilder; -import org.apache.beam.runners.samza.adapter.TestSourceHelpers.WatermarkEvent; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.SerializableCoder; -import org.apache.beam.sdk.io.UnboundedSource; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Instant; - -/** - * A unbounded source that can be used for test purposes. - * - * @param element type - */ -public class TestUnboundedSource extends UnboundedSource { - // each list of events is a split - private final List>> events; - - public static Builder createBuilder() { - return new Builder<>(); - } - - public static SplittableBuilder createSplits(int numSplits) { - return new SplittableBuilder<>(numSplits); - } - - private TestUnboundedSource(List>> events) { - this.events = Collections.unmodifiableList(new ArrayList<>(events)); - } - - @Override - public List> split( - int desiredNumSplits, PipelineOptions options) throws Exception { - return events.stream() - .map(ev -> new TestUnboundedSource<>(Collections.singletonList(ev))) - .collect(Collectors.toList()); - } - - @Override - public UnboundedReader createReader( - PipelineOptions options, @Nullable TestCheckpointMark checkpointMark) throws IOException { - assert events.size() == 1; - - return new Reader(events.get(0), checkpointMark); - } - - @Override - public Coder getCheckpointMarkCoder() { - return SerializableCoder.of(TestCheckpointMark.class); - } - - @Override - public void validate() {} - - /** A builder used to populate the events emitted by {@link TestUnboundedSource}. */ - public static class Builder extends SourceBuilder> { - - @Override - public TestUnboundedSource build() { - return new TestUnboundedSource<>(Collections.singletonList(getEvents())); - } - } - - /** - * A SplittableBuilder supports multiple splits and each split {@link TestUnboundedSource} can be - * built separately from the above Builder. - */ - public static class SplittableBuilder extends SourceBuilder> { - private final List> builders = new ArrayList<>(); - - private SplittableBuilder(int splits) { - while (splits != 0) { - builders.add(new Builder()); - --splits; - } - } - - @Override - public TestUnboundedSource build() { - List>> events = new ArrayList<>(); - builders.forEach(builder -> events.add(builder.getEvents())); - return new TestUnboundedSource<>(events); - } - - public Builder forSplit(int split) { - return builders.get(split); - } - } - - private class Reader extends UnboundedReader { - private final List> events; - private Instant curTime = BoundedWindow.TIMESTAMP_MIN_VALUE; - private Instant watermark = BoundedWindow.TIMESTAMP_MIN_VALUE; - private boolean started; - private int index = -1; - private int offset; - - private Reader(List> events, TestCheckpointMark checkpointMark) { - this.events = events; - this.offset = checkpointMark == null ? -1 : checkpointMark.checkpoint; - } - - @Override - public boolean start() throws IOException { - if (started) { - throw new IllegalStateException("Start called when reader was already started"); - } - started = true; - return advance(); - } - - @Override - public boolean advance() throws IOException { - if (!started) { - throw new IllegalStateException("Advance called when reader was not started"); - } - - for (++index; index < events.size(); ++index) { - final Event event = events.get(index); - if (event instanceof ExceptionEvent) { - throw ((ExceptionEvent) event).exception; - } else if (event instanceof LatchEvent) { - try { - ((LatchEvent) event).latch.await(); - } catch (InterruptedException e) { - // Propagate interrupt - Thread.currentThread().interrupt(); - } - } else if (event instanceof WatermarkEvent) { - watermark = ((WatermarkEvent) event).watermark; - } else if (event instanceof NoElementEvent) { - return false; - } else { - curTime = ((ElementEvent) event).timestamp; - ++offset; - return true; - } - } - - return false; - } - - @Override - public T getCurrent() throws NoSuchElementException { - if (!started) { - throw new NoSuchElementException(); - } - - final Event event = events.get(index); - assert event instanceof ElementEvent; - return ((ElementEvent) event).element; - } - - @Override - public Instant getCurrentTimestamp() throws NoSuchElementException { - return curTime; - } - - @Override - public Instant getWatermark() { - return watermark; - } - - @Override - public CheckpointMark getCheckpointMark() { - return TestCheckpointMark.of(offset); - } - - @Override - public void close() throws IOException {} - - @Override - public UnboundedSource getCurrentSource() { - return TestUnboundedSource.this; - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystemTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystemTest.java deleted file mode 100644 index b171a5475c2b..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/adapter/UnboundedSourceSystemTest.java +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.adapter; - -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.createElementMessage; -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.createEndOfStreamMessage; -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.createWatermarkMessage; -import static org.apache.beam.runners.samza.adapter.TestSourceHelpers.expectWrappedException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.adapter.TestUnboundedSource.SplittableBuilder; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.samza.Partition; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.MessageType; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemStreamPartition; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.Ignore; -import org.junit.Test; - -/** Tests for {@link UnboundedSourceSystem}. */ -public class UnboundedSourceSystemTest { - - // A reasonable time to wait to get all messages from the source assuming no blocking. - private static final long DEFAULT_TIMEOUT_MILLIS = 1000; - private static final long DEFAULT_WATERMARK_TIMEOUT_MILLIS = 1000; - private static final String NULL_STRING = null; - - private static final SystemStreamPartition DEFAULT_SSP = - new SystemStreamPartition("default-system", "default-system", new Partition(0)); - - private static final Coder CHECKPOINT_MARK_CODER = - TestUnboundedSource.createBuilder().build().getCheckpointMarkCoder(); - - @Test - public void testConsumerStartStop() throws IOException, InterruptedException { - final TestUnboundedSource source = TestUnboundedSource.createBuilder().build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, offset(0)); - consumer.start(); - assertEquals( - Collections.EMPTY_LIST, - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testConsumeOneMessage() throws IOException, InterruptedException { - final TestUnboundedSource source = - TestUnboundedSource.createBuilder().addElements("test").build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage( - DEFAULT_SSP, offset(0), "test", BoundedWindow.TIMESTAMP_MIN_VALUE)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testMaxWatermarkTriggersEndOfStreamMessage() - throws IOException, InterruptedException { - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .addElements("test") - .advanceWatermarkTo(BoundedWindow.TIMESTAMP_MAX_VALUE) - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - List actualList = - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS); - actualList.addAll( - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, offset(0), "test", BoundedWindow.TIMESTAMP_MIN_VALUE), - createWatermarkMessage(DEFAULT_SSP, BoundedWindow.TIMESTAMP_MAX_VALUE), - createEndOfStreamMessage(DEFAULT_SSP)), - actualList); - consumer.stop(); - } - - @Test - public void testAdvanceTimestamp() throws IOException, InterruptedException { - final Instant timestamp = Instant.now(); - - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .addElements("before") - .setTimestamp(timestamp) - .addElements("after") - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage( - DEFAULT_SSP, offset(0), "before", BoundedWindow.TIMESTAMP_MIN_VALUE), - createElementMessage(DEFAULT_SSP, offset(1), "after", timestamp)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testConsumeMultipleMessages() throws IOException, InterruptedException { - final Instant timestamp = Instant.now(); - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .setTimestamp(timestamp) - .addElements("test", "a", "few", "messages") - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, offset(0), "test", timestamp), - createElementMessage(DEFAULT_SSP, offset(1), "a", timestamp), - createElementMessage(DEFAULT_SSP, offset(2), "few", timestamp), - createElementMessage(DEFAULT_SSP, offset(3), "messages", timestamp)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testAdvanceWatermark() throws IOException, InterruptedException { - final Instant now = Instant.now(); - final Instant nowPlusOne = now.plus(Duration.millis(1L)); - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .setTimestamp(now) - .addElements("first") - .setTimestamp(nowPlusOne) - .addElements("second") - .advanceWatermarkTo(now) - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, offset(0), "first", now), - createElementMessage(DEFAULT_SSP, offset(1), "second", nowPlusOne), - createWatermarkMessage(DEFAULT_SSP, now)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_WATERMARK_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - @Ignore("https://github.com/apache/beam/issues/20376") - public void testMultipleAdvanceWatermark() throws IOException, InterruptedException { - final Instant now = Instant.now(); - final Instant nowPlusOne = now.plus(Duration.millis(1L)); - final Instant nowPlusTwo = now.plus(Duration.millis(2L)); - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .setTimestamp(now) - .addElements("first") - .advanceWatermarkTo(now) - .noElements() // will output the first watermark - .setTimestamp(nowPlusOne) - .addElements("second") - .setTimestamp(nowPlusTwo) - .addElements("third") - .advanceWatermarkTo(nowPlusOne) - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - // consume to the first watermark - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, offset(0), "first", now), - createWatermarkMessage(DEFAULT_SSP, now)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_WATERMARK_TIMEOUT_MILLIS)); - - // consume to the second watermark - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, offset(1), "second", nowPlusOne), - createElementMessage(DEFAULT_SSP, offset(2), "third", nowPlusTwo), - createWatermarkMessage(DEFAULT_SSP, nowPlusOne)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_WATERMARK_TIMEOUT_MILLIS)); - - consumer.stop(); - } - - @Test - public void testReaderThrowsAtStart() throws Exception { - final IOException exception = new IOException("Expected exception"); - - final TestUnboundedSource source = - TestUnboundedSource.createBuilder().addException(exception).build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - expectWrappedException( - exception, - () -> consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testReaderThrowsAtAdvance() throws Exception { - final IOException exception = new IOException("Expected exception"); - - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .addElements("test", "a", "few", "good", "messages", "then", "...") - .addException(exception) - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, offset(0)); - consumer.start(); - expectWrappedException( - exception, - () -> consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testTimeout() throws Exception { - final CountDownLatch advanceLatch = new CountDownLatch(1); - final Instant now = Instant.now(); - final Instant nowPlusOne = now.plus(Duration.millis(1)); - - final TestUnboundedSource source = - TestUnboundedSource.createBuilder() - .setTimestamp(now) - .addElements("before") - .addLatch(advanceLatch) - .setTimestamp(nowPlusOne) - .addElements("after") - .advanceWatermarkTo(nowPlusOne) - .build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source); - - consumer.register(DEFAULT_SSP, NULL_STRING); - consumer.start(); - assertEquals( - Collections.singletonList(createElementMessage(DEFAULT_SSP, offset(0), "before", now)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - - advanceLatch.countDown(); - - assertEquals( - Arrays.asList( - createElementMessage(DEFAULT_SSP, offset(1), "after", nowPlusOne), - createWatermarkMessage(DEFAULT_SSP, nowPlusOne)), - consumeUntilTimeoutOrWatermark(consumer, DEFAULT_SSP, DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - @Test - public void testRestartFromCheckpoint() throws IOException, InterruptedException { - final SplittableBuilder builder = TestUnboundedSource.createSplits(3); - builder.forSplit(0).addElements("split-0"); - builder.forSplit(1).addElements("split-1"); - builder.forSplit(2).addElements("split-2"); - final TestUnboundedSource source = builder.build(); - - final UnboundedSourceSystem.Consumer consumer = - createConsumer(source, 3); - - consumer.register(ssp(0), offset(10)); - consumer.register(ssp(1), offset(5)); - consumer.register(ssp(2), offset(8)); - consumer.start(); - assertEquals( - Arrays.asList( - createElementMessage(ssp(0), offset(11), "split-0", BoundedWindow.TIMESTAMP_MIN_VALUE)), - consumeUntilTimeoutOrWatermark(consumer, ssp(0), DEFAULT_TIMEOUT_MILLIS)); - assertEquals( - Arrays.asList( - createElementMessage(ssp(1), offset(6), "split-1", BoundedWindow.TIMESTAMP_MIN_VALUE)), - consumeUntilTimeoutOrWatermark(consumer, ssp(1), DEFAULT_TIMEOUT_MILLIS)); - assertEquals( - Arrays.asList( - createElementMessage(ssp(2), offset(9), "split-2", BoundedWindow.TIMESTAMP_MIN_VALUE)), - consumeUntilTimeoutOrWatermark(consumer, ssp(2), DEFAULT_TIMEOUT_MILLIS)); - consumer.stop(); - } - - private static UnboundedSourceSystem.Consumer createConsumer( - TestUnboundedSource source) { - return createConsumer(source, 1); - } - - private static UnboundedSourceSystem.Consumer createConsumer( - TestUnboundedSource source, int splitNum) { - SamzaPipelineOptions pipelineOptions = PipelineOptionsFactory.as(SamzaPipelineOptions.class); - pipelineOptions.setWatermarkInterval(0L); // emit immediately - pipelineOptions.setMaxSourceParallelism(splitNum); - return new UnboundedSourceSystem.Consumer<>( - source, pipelineOptions, new SamzaMetricsContainer(new MetricsRegistryMap()), "test-step"); - } - - private static List consumeUntilTimeoutOrWatermark( - SystemConsumer consumer, SystemStreamPartition ssp, long timeoutMillis) - throws InterruptedException { - assertTrue("Expected timeoutMillis (" + timeoutMillis + ") >= 0", timeoutMillis >= 0); - - final List accumulator = new ArrayList<>(); - final long start = System.currentTimeMillis(); - long now = start; - while (timeoutMillis + start >= now) { - accumulator.addAll(pollOnce(consumer, ssp, now - start - timeoutMillis)); - if (!accumulator.isEmpty() - && MessageType.of(accumulator.get(accumulator.size() - 1).getMessage()) - == MessageType.WATERMARK) { - break; - } - now = System.currentTimeMillis(); - } - return accumulator; - } - - private static List pollOnce( - SystemConsumer consumer, SystemStreamPartition ssp, long timeoutMillis) - throws InterruptedException { - final Set sspSet = Collections.singleton(ssp); - final Map> pollResult = - consumer.poll(sspSet, timeoutMillis); - assertEquals(sspSet, pollResult.keySet()); - assertNotNull(pollResult.get(ssp)); - return pollResult.get(ssp); - } - - private static String offset(int offset) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - CHECKPOINT_MARK_CODER.encode(TestCheckpointMark.of(offset), baos); - return Base64.getEncoder().encodeToString(baos.toByteArray()); - } - - private static SystemStreamPartition ssp(int partition) { - return new SystemStreamPartition("default-system", "default-system", new Partition(partition)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaRunnerWithTransformMetrics.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaRunnerWithTransformMetrics.java deleted file mode 100644 index 49143a5b10bb..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaRunnerWithTransformMetrics.java +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import java.util.Map; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.TestSamzaRunner; -import org.apache.beam.runners.samza.runtime.OpEmitter; -import org.apache.beam.runners.samza.util.InMemoryMetricsReporter; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.testing.PAssert; -import org.apache.beam.sdk.transforms.Count; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.Values; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.Window; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.samza.context.Context; -import org.apache.samza.metrics.Counter; -import org.apache.samza.metrics.Gauge; -import org.apache.samza.metrics.Metric; -import org.apache.samza.metrics.Timer; -import org.apache.samza.system.WatermarkMessage; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.Test; - -public class TestSamzaRunnerWithTransformMetrics { - @Test - public void testSamzaRunnerWithDefaultMetrics() { - // TODO(https://github.com/apache/beam/issues/32208) - assumeTrue(System.getProperty("java.version").startsWith("1.")); - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - InMemoryMetricsReporter inMemoryMetricsReporter = new InMemoryMetricsReporter(); - options.setMetricsReporters(ImmutableList.of(inMemoryMetricsReporter)); - options.setRunner(TestSamzaRunner.class); - options.setEnableTransformMetrics(true); - TestSamzaRunner testSamzaRunner = TestSamzaRunner.fromOptions(options); - Pipeline pipeline = Pipeline.create(options); - // Create a pipeline - PCollection> output = - pipeline - .apply( - "Mock data", - Create.of( - KV.of("bad-key", KV.of("a", 97)), - KV.of("hello", KV.of("a", 97)), - KV.of("hello", KV.of("b", 42)), - KV.of("hello", KV.of("c", 12)))) - .apply("Filter valid keys", Filter.by(x -> x.getKey().equals("hello"))) - .apply(Values.create()) - .apply("Fixed-window", Window.into(FixedWindows.of(Duration.standardSeconds(10)))) - .apply(Count.perKey()); - - // check pipeline is working fine - PAssert.that(output).containsInAnyOrder(KV.of("a", 1L), KV.of("b", 1L), KV.of("c", 1L)); - testSamzaRunner.run(pipeline); - - Map pTransformContainerMetrics = - inMemoryMetricsReporter - .getMetricsRegistry("samza-container-1") - .getGroup("SamzaBeamTransformMetrics"); - Map pTransformTaskMetrics = - inMemoryMetricsReporter - .getMetricsRegistry("TaskName-Partition 0") - .getGroup("SamzaBeamTransformMetrics"); - - // SamzaTransformMetrics group must be initialized - assertFalse(pTransformTaskMetrics.isEmpty()); - assertFalse(pTransformContainerMetrics.isEmpty()); - - // Throughput Metrics are Per container by default - assertEquals( - 4, - ((Counter) - pTransformContainerMetrics.get("Mock_data_Read_CreateSource_-num-output-messages")) - .getCount()); - assertEquals( - 4, - ((Counter) - pTransformContainerMetrics.get( - "Filter_valid_keys_ParDo_Anonymous__ParMultiDo_Anonymous_-num-input-messages")) - .getCount()); - // One message is dropped from filter - assertEquals( - 3, - ((Counter) - pTransformContainerMetrics.get( - "Filter_valid_keys_ParDo_Anonymous__ParMultiDo_Anonymous_-num-output-messages")) - .getCount()); - assertEquals( - 3, - ((Counter) - pTransformContainerMetrics.get( - "Values_Values_Map_ParMultiDo_Anonymous_-num-input-messages")) - .getCount()); - assertEquals( - 3, - ((Counter) - pTransformContainerMetrics.get( - "Values_Values_Map_ParMultiDo_Anonymous_-num-output-messages")) - .getCount()); - assertEquals( - 3, - ((Counter) pTransformContainerMetrics.get("Fixed_window_Window_Assign-num-input-messages")) - .getCount()); - assertEquals( - 3, - ((Counter) pTransformContainerMetrics.get("Fixed_window_Window_Assign-num-output-messages")) - .getCount()); - assertEquals( - 3, - ((Counter) pTransformContainerMetrics.get("Combine_perKey_Count_-num-input-messages")) - .getCount()); - assertEquals( - 3, - ((Counter) pTransformContainerMetrics.get("Combine_perKey_Count_-num-output-messages")) - .getCount()); - - // Throughput Metrics are per container by default - assertNotNull( - pTransformContainerMetrics.get( - "Filter_valid_keys_ParDo_Anonymous__ParMultiDo_Anonymous_-handle-message-ns")); - assertNotNull( - pTransformContainerMetrics.get( - "Values_Values_Map_ParMultiDo_Anonymous_-handle-message-ns")); - assertNotNull(pTransformContainerMetrics.get("Combine_perKey_Count_-handle-message-ns")); - - // Watermark Metrics are Per task by default - assertTrue( - ((Gauge) - pTransformTaskMetrics.get("Mock_data_Read_CreateSource_-output-watermark-ms")) - .getValue() - >= 0); - assertNotNull( - pTransformTaskMetrics.get( - "Filter_valid_keys_ParDo_Anonymous__ParMultiDo_Anonymous_-output-watermark-ms")); - assertNotNull( - pTransformTaskMetrics.get("Values_Values_Map_ParMultiDo_Anonymous_-output-watermark-ms")); - assertNotNull(pTransformTaskMetrics.get("Combine_perKey_Count_-output-watermark-ms")); - } - - @Test - public void testSamzaInputAndOutputMetricOp() { - final WindowedValue windowedValue = - WindowedValues.timestampedValueInGlobalWindow("value-1", new Instant()); - final WindowedValue windowedValue2 = - WindowedValues.timestampedValueInGlobalWindow("value-2", new Instant()); - final WatermarkMessage watermarkMessage = - new WatermarkMessage(BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()); - - OpEmitter opEmitter = mock(OpEmitter.class); - doNothing().when(opEmitter).emitElement(any()); - doNothing().when(opEmitter).emitWatermark(any()); - - Counter inputCounter = new Counter("filter-input-counter"); - Counter outputCounter = new Counter("filter-output-counter"); - Gauge watermarkProgress = new Gauge<>("filter-output-watermark", 0L); - Gauge cacheSize = new Gauge<>("filter-arrival-time-cache-size", 0L); - Timer latency = new Timer("filter-latency"); - - SamzaTransformMetrics samzaTransformMetrics = mock(SamzaTransformMetrics.class); - doNothing().when(samzaTransformMetrics).register(any(), any()); - when(samzaTransformMetrics.getTransformInputThroughput("filter")).thenReturn(inputCounter); - when(samzaTransformMetrics.getTransformOutputThroughput("filter")).thenReturn(outputCounter); - when(samzaTransformMetrics.getTransformWatermarkProgress("filter")) - .thenReturn(watermarkProgress); - when(samzaTransformMetrics.getTransformLatencyMetric("filter")).thenReturn(latency); - when(samzaTransformMetrics.getTransformCacheSize("filter")).thenReturn(cacheSize); - - SamzaTransformMetricRegistry samzaTransformMetricRegistry = - spy(new SamzaTransformMetricRegistry(samzaTransformMetrics)); - samzaTransformMetricRegistry.register("filter", "dummy-pvalue.in", mock(Context.class)); - samzaTransformMetricRegistry.register("filter", "dummy-pvalue.out", mock(Context.class)); - - SamzaMetricOp inputMetricOp = - new SamzaMetricOp<>( - "dummy-pvalue.in", - "filter", - SamzaMetricOpFactory.OpType.INPUT, - samzaTransformMetricRegistry); - - inputMetricOp.processElement(windowedValue, opEmitter); - inputMetricOp.processElement(windowedValue2, opEmitter); - inputMetricOp.processWatermark(new Instant(watermarkMessage.getTimestamp()), opEmitter); - - // Input throughput must be updated - assertEquals(2, inputCounter.getCount()); - // Avg arrival time for the PValue must be updated - assertTrue( - samzaTransformMetricRegistry - .getAverageArrivalTimeMap("filter") - .get("dummy-pvalue.in") - .containsKey(watermarkMessage.getTimestamp())); - - SamzaMetricOp outputMetricOp = - new SamzaMetricOp<>( - "dummy-pvalue.out", - "filter", - SamzaMetricOpFactory.OpType.OUTPUT, - samzaTransformMetricRegistry); - outputMetricOp.init(ImmutableList.of("dummy-pvalue.in"), ImmutableList.of("dummy-pvalue.out")); - - outputMetricOp.processElement(windowedValue, opEmitter); - outputMetricOp.processElement(windowedValue2, opEmitter); - outputMetricOp.processWatermark(new Instant(watermarkMessage.getTimestamp()), opEmitter); - - // Output throughput must be updated - assertEquals(2, outputCounter.getCount()); - // Output watermark must be updated - assertEquals(watermarkMessage.getTimestamp(), watermarkProgress.getValue().longValue()); - // Latency must be positive - assertTrue(latency.getSnapshot().getAverage() > 0); - // Cache size must be 0 - assertEquals(0L, cacheSize.getValue().longValue()); - } - - @Test - public void testSamzaInputAndOutputGBKMetricOp() { - final WindowedValue windowedValue = - WindowedValues.timestampedValueInGlobalWindow("value-1", new Instant()); - final WindowedValue windowedValue2 = - WindowedValues.timestampedValueInGlobalWindow("value-2", new Instant()); - final WatermarkMessage watermarkMessage = - new WatermarkMessage(BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()); - - OpEmitter opEmitter = mock(OpEmitter.class); - doNothing().when(opEmitter).emitElement(any()); - doNothing().when(opEmitter).emitWatermark(any()); - - Counter inputCounter = new Counter("Count-perKey-input-counter"); - Counter outputCounter = new Counter("Count-perKey-output-counter"); - Gauge watermarkProgress = new Gauge<>("Count-perKey-output-watermark", 0L); - Timer latency = new Timer("Count-perKey-latency"); - Gauge cacheSize = new Gauge<>("Count-perKey-arrival-time-cache-size", 0L); - - SamzaTransformMetrics samzaTransformMetrics = mock(SamzaTransformMetrics.class); - doNothing().when(samzaTransformMetrics).register(any(), any()); - when(samzaTransformMetrics.getTransformInputThroughput("Count-perKey")) - .thenReturn(inputCounter); - when(samzaTransformMetrics.getTransformOutputThroughput("Count-perKey")) - .thenReturn(outputCounter); - when(samzaTransformMetrics.getTransformWatermarkProgress("Count-perKey")) - .thenReturn(watermarkProgress); - when(samzaTransformMetrics.getTransformLatencyMetric("Count-perKey")).thenReturn(latency); - when(samzaTransformMetrics.getTransformCacheSize("Count-perKey")).thenReturn(cacheSize); - - SamzaTransformMetricRegistry samzaTransformMetricRegistry = - spy(new SamzaTransformMetricRegistry(samzaTransformMetrics)); - samzaTransformMetricRegistry.register("Count-perKey", "window-assign.in", mock(Context.class)); - samzaTransformMetricRegistry.register("Count-perKey", "window-assign.out", mock(Context.class)); - - SamzaGBKMetricOp inputMetricOp = - new SamzaGBKMetricOp<>( - "window-assign.in", - "Count-perKey", - SamzaMetricOpFactory.OpType.INPUT, - samzaTransformMetricRegistry); - - inputMetricOp.processElement(windowedValue, opEmitter); - inputMetricOp.processElement(windowedValue2, opEmitter); - inputMetricOp.processWatermark(new Instant(watermarkMessage.getTimestamp()), opEmitter); - - // Input throughput must be updated - assertEquals(2, inputCounter.getCount()); - // Avg arrival time for must be present for one Global Window - assertEquals( - 1, samzaTransformMetricRegistry.getAverageArrivalTimeMapForGBK("Count-perKey").size()); - - SamzaGBKMetricOp outputMetricOp = - new SamzaGBKMetricOp<>( - "window-assign.out", - "Count-perKey", - SamzaMetricOpFactory.OpType.OUTPUT, - samzaTransformMetricRegistry); - - outputMetricOp.processElement(windowedValue, opEmitter); - outputMetricOp.processElement(windowedValue2, opEmitter); - outputMetricOp.processWatermark(new Instant(watermarkMessage.getTimestamp()), opEmitter); - - // Output throughput must be updated - assertEquals(2, outputCounter.getCount()); - // Output watermark must be updated - assertEquals(watermarkMessage.getTimestamp(), watermarkProgress.getValue().longValue()); - // Latency must be positive - assertTrue(latency.getSnapshot().getAverage() > 0); - // Cache size must be 0 - assertEquals(0L, cacheSize.getValue().longValue()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaTransformMetricsRegistry.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaTransformMetricsRegistry.java deleted file mode 100644 index 75f6ddd5fefa..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/metrics/TestSamzaTransformMetricsRegistry.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.metrics; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.samza.context.Context; -import org.apache.samza.metrics.Gauge; -import org.apache.samza.metrics.Timer; -import org.apache.samza.system.WatermarkMessage; -import org.joda.time.Instant; -import org.junit.Test; - -public class TestSamzaTransformMetricsRegistry { - - @Test - public void testSamzaTransformMetricsRegistryForNonShuffleOperators() { - final WatermarkMessage watermarkMessage = - new WatermarkMessage(BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()); - final long avgInputArrivalTime = System.currentTimeMillis(); - - Timer latency = new Timer("filter-latency"); - Gauge cacheSize = new Gauge("filter-cache-size", 0L); - SamzaTransformMetrics samzaTransformMetrics = mock(SamzaTransformMetrics.class); - doNothing().when(samzaTransformMetrics).register(any(), any()); - when(samzaTransformMetrics.getTransformLatencyMetric("filter")).thenReturn(latency); - when(samzaTransformMetrics.getTransformCacheSize("filter")).thenReturn(cacheSize); - - SamzaTransformMetricRegistry samzaTransformMetricRegistry = - spy(new SamzaTransformMetricRegistry(samzaTransformMetrics)); - - samzaTransformMetricRegistry.register("filter", "dummy-pvalue.in", mock(Context.class)); - samzaTransformMetricRegistry.register("filter", "dummy-pvalue.out", mock(Context.class)); - - // Update the avg arrival time - samzaTransformMetricRegistry.updateArrivalTimeMap( - "filter", "dummy-pvalue.in", watermarkMessage.getTimestamp(), avgInputArrivalTime); - samzaTransformMetricRegistry.updateArrivalTimeMap( - "filter", "dummy-pvalue.out", watermarkMessage.getTimestamp(), avgInputArrivalTime + 100); - - // Check the avg arrival time is updated - assertEquals( - avgInputArrivalTime, - samzaTransformMetricRegistry - .getAverageArrivalTimeMap("filter") - .get("dummy-pvalue.in") - .get(watermarkMessage.getTimestamp()) - .longValue()); - assertEquals( - avgInputArrivalTime + 100, - samzaTransformMetricRegistry - .getAverageArrivalTimeMap("filter") - .get("dummy-pvalue.out") - .get(watermarkMessage.getTimestamp()) - .longValue()); - - // Emit the latency metric - samzaTransformMetricRegistry.emitLatencyMetric( - "filter", - ImmutableList.of("dummy-pvalue.in"), - ImmutableList.of("dummy-pvalue.out"), - watermarkMessage.getTimestamp(), - "task 0"); - - // Check the latency metric is updated - assertTrue(100 == latency.getSnapshot().getAverage()); - - // Check the avg arrival time is cleared - assertFalse( - samzaTransformMetricRegistry - .getAverageArrivalTimeMap("filter") - .get("dummy-pvalue.in") - .containsKey(watermarkMessage.getTimestamp())); - assertFalse( - samzaTransformMetricRegistry - .getAverageArrivalTimeMap("filter") - .get("dummy-pvalue.out") - .containsKey(watermarkMessage.getTimestamp())); - // Cache size must be 0 - assertEquals(0L, cacheSize.getValue().longValue()); - } - - @Test - public void testSamzaTransformMetricsRegistryForDataShuffleOperators() { - Timer latency = new Timer("Count-perKey-latency"); - Gauge cacheSize = new Gauge("Count-perKey-cache-size", 0L); - - SamzaTransformMetrics samzaTransformMetrics = mock(SamzaTransformMetrics.class); - doNothing().when(samzaTransformMetrics).register(any(), any()); - when(samzaTransformMetrics.getTransformLatencyMetric("Count.perKey")).thenReturn(latency); - when(samzaTransformMetrics.getTransformCacheSize("Count.perKey")).thenReturn(cacheSize); - - SamzaTransformMetricRegistry samzaTransformMetricRegistry = - spy(new SamzaTransformMetricRegistry(samzaTransformMetrics)); - - samzaTransformMetricRegistry.register("Count.perKey", "window-assign.in", mock(Context.class)); - samzaTransformMetricRegistry.register("Count.perKey", "window-assign.out", mock(Context.class)); - - final BoundedWindow first = - new BoundedWindow() { - @Override - public Instant maxTimestamp() { - return new Instant(2048L); - } - }; - final BoundedWindow second = - new BoundedWindow() { - @Override - public Instant maxTimestamp() { - return new Instant(689743L); - } - }; - - // Update the avg arrival time - samzaTransformMetricRegistry.updateArrivalTimeMap("Count.perKey", first, 1048L); - samzaTransformMetricRegistry.updateArrivalTimeMap("Count.perKey", second, 4897L); - - // Check the avg arrival time is updated - assertEquals( - 1048L, - samzaTransformMetricRegistry - .getAverageArrivalTimeMapForGBK("Count.perKey") - .get(first) - .longValue()); - assertEquals( - 4897L, - samzaTransformMetricRegistry - .getAverageArrivalTimeMapForGBK("Count.perKey") - .get(second) - .longValue()); - - // Emit the latency metric - samzaTransformMetricRegistry.emitLatencyMetric("Count.perKey", first, 2048L, "task 0"); - samzaTransformMetricRegistry.emitLatencyMetric("Count.perKey", second, 5897L, "task 0"); - - // Check the latency metric is updated - assertTrue(1000 == latency.getSnapshot().getAverage()); - - // Check the avg arrival time is cleared - assertFalse( - samzaTransformMetricRegistry - .getAverageArrivalTimeMapForGBK("Count.perKey") - .containsKey(first)); - assertFalse( - samzaTransformMetricRegistry - .getAverageArrivalTimeMapForGBK("Count.perKey") - .containsKey(second)); - - // Failure testing - samzaTransformMetricRegistry.updateArrivalTimeMap("random-transform", first, 1048L); - samzaTransformMetricRegistry.updateArrivalTimeMap("random-transform", first, 1048L); - // No data updated - assertFalse( - samzaTransformMetricRegistry - .getAverageArrivalTimeMapForGBK("Count.perKey") - .containsKey(first)); - assertNull(samzaTransformMetricRegistry.getAverageArrivalTimeMapForGBK("random-transform")); - // Emit the bad latency metric - samzaTransformMetricRegistry.emitLatencyMetric("random-transform", first, 2048L, "task 0"); - samzaTransformMetricRegistry.emitLatencyMetric("random-transform", first, 0, "task 0"); - // Check the latency metric is same - assertTrue(1000 == latency.getSnapshot().getAverage()); - // Cache size must be 0 - assertEquals(0L, cacheSize.getValue().longValue()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java deleted file mode 100644 index 4040040d3e04..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.sdk.coders.VarIntCoder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.CombiningState; -import org.apache.beam.sdk.state.StateSpec; -import org.apache.beam.sdk.state.StateSpecs; -import org.apache.beam.sdk.testing.PAssert; -import org.apache.beam.sdk.testing.TestPipeline; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.MapElements; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TypeDescriptors; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.sdk.values.WindowedValues; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; - -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - // TODO(https://github.com/apache/beam/issues/21230): Remove when new version of - // errorprone is released (2.11.0) - "unused" -}) -/** - * Tests for {@link AsyncDoFnRunner}. - * - *

Note due to the bug in SAMZA-2761, end-of-stream can cause shutdown while there are still - * messages in process in asynchronous mode. As a temporary solution, we add more bundles to process - * in the test inputs. - */ -public class AsyncDoFnRunnerTest implements Serializable { - - @Rule - public final transient TestPipeline pipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs( - "--runner=TestSamzaRunner", - "--maxBundleSize=5", - "--numThreadsForProcessElement=5") - .create()); - - @Test - @Ignore("https://github.com/apache/beam/issues/23745") - public void testSimplePipeline() { - List input = new ArrayList<>(); - for (int i = 1; i < 20; i++) { - input.add(i); - } - PCollection square = - pipeline - .apply(Create.of(input)) - .apply(Filter.by(x -> x <= 5)) - .apply(MapElements.into(TypeDescriptors.integers()).via(x -> x * x)); - - PAssert.that(square).containsInAnyOrder(Arrays.asList(1, 4, 9, 16, 25)); - - pipeline.run(); - } - - @Test - @Ignore("https://github.com/apache/beam/issues/23745") - public void testPipelineWithState() { - final List> input = - new ArrayList<>( - Arrays.asList( - KV.of("apple", "red"), - KV.of("banana", "yellow"), - KV.of("apple", "yellow"), - KV.of("grape", "purple"), - KV.of("banana", "yellow"))); - final Map expectedCount = ImmutableMap.of("apple", 2, "banana", 2, "grape", 1); - - // TODO: remove after SAMZA-2761 fix - for (int i = 0; i < 20; i++) { - input.add(KV.of("*", "*")); - } - - final DoFn, KV> fn = - new DoFn, KV>() { - - @StateId("cc") - private final StateSpec> countState = - StateSpecs.combiningFromInputInternal(VarIntCoder.of(), Sum.ofIntegers()); - - @ProcessElement - public void processElement( - ProcessContext c, @StateId("cc") CombiningState countState) { - - if (c.element().getKey().equals("*")) { - return; - } - - countState.add(1); - String key = c.element().getKey(); - int n = countState.read(); - if (n >= expectedCount.get(key)) { - c.output(KV.of(key, n)); - } - } - }; - - PCollection> counts = pipeline.apply(Create.of(input)).apply(ParDo.of(fn)); - - PAssert.that(counts) - .containsInAnyOrder( - expectedCount.entrySet().stream() - .map(entry -> KV.of(entry.getKey(), entry.getValue())) - .collect(Collectors.toList())); - - pipeline.run(); - } - - @Test - @Ignore("https://github.com/apache/beam/issues/23745") - public void testPipelineWithAggregation() { - final List> input = - new ArrayList<>( - Arrays.asList( - KV.of("apple", 2L), - KV.of("banana", 5L), - KV.of("apple", 8L), - KV.of("grape", 10L), - KV.of("banana", 5L))); - - // TODO: remove after SAMZA-2761 fix - for (int i = 0; i < 50; i++) { - input.add(KV.of("*", 0L)); - } - - PCollection> sums = - pipeline - .apply(Create.of(input)) - .apply(Filter.by(x -> !x.getKey().equals("*"))) - .apply(Sum.longsPerKey()); - - PAssert.that(sums) - .containsInAnyOrder( - Arrays.asList(KV.of("apple", 10L), KV.of("banana", 10L), KV.of("grape", 10L))); - - pipeline.run(); - } - - @Test - public void testKeyedOutputFutures() { - // We test the scenario that two elements of the same key needs to be processed in order. - final DoFnRunner, Void> doFnRunner = mock(DoFnRunner.class); - final AtomicInteger prev = new AtomicInteger(0); - final CountDownLatch latch = new CountDownLatch(1); - doAnswer( - invocation -> { - latch.await(); - WindowedValue> wv = invocation.getArgument(0); - Integer val = wv.getValue().getValue(); - - // Verify the previous element has been fully processed by checking the prev value - assertEquals(val - 1, prev.get()); - - prev.set(val); - return null; - }) - .when(doFnRunner) - .processElement(any()); - - SamzaPipelineOptions options = PipelineOptionsFactory.as(SamzaPipelineOptions.class); - options.setNumThreadsForProcessElement(4); - - final OpEmitter opEmitter = new OpAdapter.OpEmitterImpl<>(); - final FutureCollector futureCollector = new FutureCollectorImpl<>(); - futureCollector.prepare(); - - final AsyncDoFnRunner, Void> asyncDoFnRunner = - AsyncDoFnRunner.create(doFnRunner, opEmitter, futureCollector, true, options); - - final String appleKey = "apple"; - - final WindowedValue> input1 = - WindowedValues.valueInGlobalWindow(KV.of(appleKey, 1)); - - final WindowedValue> input2 = - WindowedValues.valueInGlobalWindow(KV.of(appleKey, 2)); - - asyncDoFnRunner.processElement(input1); - asyncDoFnRunner.processElement(input2); - // Resume input1 process afterwards - latch.countDown(); - - // Waiting for the futures to be resolved - try { - futureCollector.finish().toCompletableFuture().get(); - } catch (Exception e) { - // ignore interruption here. - } - - // The final val should be the last element value - assertEquals(2, prev.get()); - // The appleKey in keyedOutputFutures map should be removed - assertFalse(asyncDoFnRunner.hasOutputFuturesForKey(appleKey)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java deleted file mode 100644 index 09b51349aa49..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -/** Unit tests for {@linkplain ClassicBundleManager}. */ -public final class ClassicBundleManagerTest { - private static final long MAX_BUNDLE_SIZE = 3; - private static final long MAX_BUNDLE_TIME_MS = 2000; - private static final String BUNDLE_CHECK_TIMER_ID = "bundle-check-test-timer"; - - private FutureCollector mockFutureCollector; - private ClassicBundleManager bundleManager; - private BundleManager.BundleProgressListener bundleProgressListener; - private Scheduler> mockScheduler; - - @Before - public void setUp() { - mockFutureCollector = mock(FutureCollector.class); - bundleProgressListener = mock(ClassicBundleManager.BundleProgressListener.class); - mockScheduler = mock(Scheduler.class); - bundleManager = - new ClassicBundleManager<>( - bundleProgressListener, - mockFutureCollector, - MAX_BUNDLE_SIZE, - MAX_BUNDLE_TIME_MS, - mockScheduler, - BUNDLE_CHECK_TIMER_ID); - } - - @Test - public void testWhenFirstTryStartBundleThenStartsBundle() { - bundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryStartBundle() did not start the bundle", bundleManager.isBundleStarted()); - } - - @Test(expected = IllegalArgumentException.class) - public void testWhenCurrentBundleDoneFutureIsNotNullThenStartBundleFails() { - bundleManager.setCurrentBundleDoneFuture(CompletableFuture.completedFuture(null)); - bundleManager.tryStartBundle(); - } - - @Test - public void testWhenSignalFailureThenResetCurrentBundle() { - doThrow(new RuntimeException("User start bundle threw an exception")) - .when(bundleProgressListener) - .onBundleStarted(); - - try { - bundleManager.tryStartBundle(); - } catch (RuntimeException e) { - bundleManager.signalFailure(e); - } - - // verify if the signal failure only resets appropriate attributes of bundle - verify(mockFutureCollector, times(1)).prepare(); - verify(mockFutureCollector, times(1)).discard(); - assertEquals( - "Expected the number of element in the current bundle to 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("Error didn't reset the bundle as expected.", bundleManager.isBundleStarted()); - } - - @Test - public void testWhenMultipleTryStartThenOnlyStartBundleOnce() { - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - - // second invocation should not start the bundle - verify(bundleProgressListener, times(1)).onBundleStarted(); - assertEquals( - "Expected the number of element in the current bundle to be 2", - 2L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryStartBundle() did not start the bundle", bundleManager.isBundleStarted()); - } - - /* - * Setup the bundle manager with default max bundle size as 3 and max bundle close timeout to 2 seconds. - * The test verifies the following - * 1. Bundle gets closed on tryFinishBundle() - * a. pending bundle count == 0 - * b. element in current bundle == 0 - * c. isBundleStarted == false - * 2. onBundleFinished callback is invoked on the progress listener - */ - @Test - public void testWhenTryFinishBundleThenBundleIsReset() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testTryFinishBundleClosesBundleOnMaxWatermark() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - bundleManager.setBundleWatermarkHold(BoundedWindow.TIMESTAMP_MAX_VALUE); - - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - /* - * Set up the bundle manager with defaults and ensure the bundle manager doesn't close the current active bundle. - */ - @Test - public void testTryFinishBundleShouldNotCloseBundle() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - - bundleManager.tryStartBundle(); - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockFutureCollector, times(1)).finish(); - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(0)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testTryFinishBundleWhenNoBundleInProgress() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn(CompletableFuture.completedFuture(Collections.emptyList())); - - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - assertNull( - "tryFinishBundle() should not set the future when no bundle in progress", - bundleManager.getCurrentBundleDoneFuture()); - } - - @Test - public void testProcessWatermarkWhenNoBundleInProgress() { - Instant now = Instant.now(); - OpEmitter mockEmitter = mock(OpEmitter.class); - bundleManager.processWatermark(now, mockEmitter); - verify(bundleProgressListener, times(1)).onWatermark(now, mockEmitter); - } - - /* - * The test validates processing watermark during an active bundle in progress and also validates - * if the watermark hold is propagated down stream after the output futures are resolved. - */ - @Test - public void testProcessWatermarkWithPendingBundles() { - CountDownLatch latch = new CountDownLatch(1); - Instant watermark = Instant.now(); - OpEmitter mockEmitter = mock(OpEmitter.class); - - // We need to capture the finish bundle future to know if we can check for output watermark - // and verify other callbacks get invoked. - Class>>> outputFutureClass = - (Class>>>) (Class) CompletionStage.class; - ArgumentCaptor>>> captor = - ArgumentCaptor.forClass(outputFutureClass); - - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.supplyAsync( - () -> { - try { - latch.await(); - } catch (InterruptedException e) { - throw new AssertionError("Test interrupted when waiting for latch"); - } - - return Collections.singleton(mock(WindowedValue.class)); - })); - - testWatermarkHoldWhenPendingBundleInProgress(mockEmitter, captor, watermark); - testWatermarkHoldPropagatesAfterFutureResolution(mockEmitter, captor, latch, watermark); - } - - @Test - public void testMaxWatermarkPropagationForPendingBundle() { - Instant watermark = BoundedWindow.TIMESTAMP_MAX_VALUE; - OpEmitter mockEmitter = mock(OpEmitter.class); - bundleManager.setPendingBundleCount(1); - bundleManager.processWatermark(watermark, mockEmitter); - verify(bundleProgressListener, times(1)).onWatermark(watermark, mockEmitter); - } - - @Test - public void testMaxWatermarkWithBundleInProgress() { - Instant watermark = BoundedWindow.TIMESTAMP_MAX_VALUE; - OpEmitter mockEmitter = mock(OpEmitter.class); - - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - - // should force close bundle - bundleManager.processWatermark(watermark, mockEmitter); - verify(bundleProgressListener, times(1)).onWatermark(watermark, mockEmitter); - } - - @Test - public void testProcessTimerWithBundleTimeElapsed() { - ClassicBundleManager bundleManager = - new ClassicBundleManager<>( - bundleProgressListener, - mockFutureCollector, - MAX_BUNDLE_SIZE, - 0, - mockScheduler, - BUNDLE_CHECK_TIMER_ID); - OpEmitter mockEmitter = mock(OpEmitter.class); - KeyedTimerData mockTimer = mock(KeyedTimerData.class); - TimerInternals.TimerData mockTimerData = mock(TimerInternals.TimerData.class); - - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - when(mockTimerData.getTimerId()).thenReturn(BUNDLE_CHECK_TIMER_ID); - when(mockTimer.getTimerData()).thenReturn(mockTimerData); - - bundleManager.tryStartBundle(); - bundleManager.processTimer(mockTimer, mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testProcessTimerWithTimeLessThanMaxBundleTime() { - OpEmitter mockEmitter = mock(OpEmitter.class); - KeyedTimerData mockTimer = mock(KeyedTimerData.class); - TimerInternals.TimerData mockTimerData = mock(TimerInternals.TimerData.class); - - when(mockTimerData.getTimerId()).thenReturn(BUNDLE_CHECK_TIMER_ID); - when(mockTimer.getTimerData()).thenReturn(mockTimerData); - - when(mockFutureCollector.finish()) - .thenReturn(CompletableFuture.completedFuture(Collections.emptyList())); - - bundleManager.tryStartBundle(); - bundleManager.processTimer(mockTimer, mockEmitter); - - verify(mockFutureCollector, times(1)).finish(); - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(0)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryFinishBundle() closed the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testProcessTimerIgnoresNonBundleTimers() { - OpEmitter mockEmitter = mock(OpEmitter.class); - KeyedTimerData mockTimer = mock(KeyedTimerData.class); - TimerInternals.TimerData mockTimerData = mock(TimerInternals.TimerData.class); - - when(mockTimerData.getTimerId()).thenReturn("NotBundleTimer"); - when(mockTimer.getTimerData()).thenReturn(mockTimerData); - - bundleManager.tryStartBundle(); - bundleManager.processTimer(mockTimer, mockEmitter); - - verify(mockFutureCollector, times(0)).finish(); - verify(mockEmitter, times(0)).emitFuture(any()); - verify(bundleProgressListener, times(0)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryFinishBundle() closed the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testSignalFailureResetsTheBundleAndCollector() { - bundleManager.tryStartBundle(); - - bundleManager.signalFailure(mock(Throwable.class)); - verify(mockFutureCollector, times(1)).prepare(); - verify(mockFutureCollector, times(1)).discard(); - assertEquals( - "Expected the number of element in the current bundle to 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("Error didn't reset the bundle as expected.", bundleManager.isBundleStarted()); - } - - /* - * We validate the following - * 1. Process watermark is held since there is a pending bundle. - * 2. Watermark propagates down stream once the output future is resolved. - * 3. The watermark propagated is the one that was held before closing the bundle - * 4. onBundleFinished and onWatermark callbacks are triggered - * 5. Pending bundle count is decremented once the future is resolved - */ - private void testWatermarkHoldPropagatesAfterFutureResolution( - OpEmitter mockEmitter, - ArgumentCaptor>>> captor, - CountDownLatch latch, - Instant sealedWatermark) { - Instant higherWatermark = Instant.now(); - - // Process watermark should result in watermark hold again since pending bundle count > 1 - bundleManager.processWatermark(higherWatermark, mockEmitter); - verify(bundleProgressListener, times(0)).onWatermark(higherWatermark, mockEmitter); - - // Resolving the process output futures should result in watermark propagation - latch.countDown(); - CompletionStage validationFuture = - captor - .getValue() - .thenAccept( - results -> { - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - verify(bundleProgressListener, times(1)) - .onWatermark(sealedWatermark, mockEmitter); - assertEquals( - "Expected the pending bundle count to be 0", - 0L, - bundleManager.getPendingBundleCount()); - }); - - validationFuture.toCompletableFuture().join(); - } - - /* - * We validate the following - * 1. Watermark is held since there is a bundle in progress - * 2. Callbacks are not invoked when tryFinishBundle() is invoked since the future is unresolved - * 3. Watermark hold is sealed and output future is emitted - */ - private void testWatermarkHoldWhenPendingBundleInProgress( - OpEmitter mockEmitter, - ArgumentCaptor>>> captor, - Instant watermark) { - // Starts the bundle and reach the max bundle size so that tryFinishBundle() seals the current - // bundle - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - - bundleManager.processWatermark(watermark, mockEmitter); - verify(bundleProgressListener, times(0)).onWatermark(watermark, mockEmitter); - - // Bundle is still unresolved although sealed since count down the latch is not yet decremented. - bundleManager.tryFinishBundle(mockEmitter); - verify(mockFutureCollector, times(1)).finish(); - verify(mockEmitter, times(1)).emitFuture(captor.capture()); - assertFalse("tryFinishBundle() closed the bundle", bundleManager.isBundleStarted()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java deleted file mode 100644 index b0b9b5450f23..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -/** Unit tests for {@linkplain FutureCollectorImpl}. */ -public final class FutureCollectorImplTest { - private static final List RESULTS = ImmutableList.of("hello", "world"); - private FutureCollector futureCollector = new FutureCollectorImpl<>(); - - @Before - public void setup() { - futureCollector = new FutureCollectorImpl<>(); - } - - @Test(expected = IllegalStateException.class) - public void testAddWithoutPrepareCallThrowsException() { - futureCollector.add(mock(CompletionStage.class)); - } - - @Test - public void testFinishWithoutPrepareReturnsEmptyCollection() { - CompletionStage>> resultFuture = futureCollector.finish(); - CompletionStage validationFuture = - resultFuture.thenAccept( - result -> { - Assert.assertTrue("Expected the result to be empty", result.isEmpty()); - }); - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testFinishReturnsExpectedResults() { - WindowedValue mockWindowedValue = mock(WindowedValue.class); - - when(mockWindowedValue.getValue()).thenReturn("hello").thenReturn("world"); - - futureCollector.prepare(); - futureCollector.add(CompletableFuture.completedFuture(mockWindowedValue)); - futureCollector.add(CompletableFuture.completedFuture(mockWindowedValue)); - - CompletionStage>> resultFuture = futureCollector.finish(); - CompletionStage validationFuture = - resultFuture.thenAccept( - results -> { - List actualResults = - results.stream().map(WindowedValue::getValue).collect(Collectors.toList()); - Assert.assertEquals( - "Expected the result to be {hello, world}", RESULTS, actualResults); - }); - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testMultiplePrepareCallsWithoutFinishThrowsException() { - futureCollector.prepare(); - - try { - futureCollector.prepare(); - Assert.fail("Second invocation of prepare should throw IllegalStateException"); - } catch (IllegalStateException ex) { - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java deleted file mode 100644 index 73454cc95421..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assume.assumeTrue; - -import java.io.Serializable; -import java.util.Arrays; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.VarIntCoder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.testing.PAssert; -import org.apache.beam.sdk.testing.TestPipeline; -import org.apache.beam.sdk.testing.TestStream; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.Window; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TimestampedValue; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; - -/** Tests for GroupByKeyOp. */ -public class GroupByKeyOpTest implements Serializable { - - @BeforeClass - public static void beforeClass() { - // TODO(https://github.com/apache/beam/issues/32208) - assumeTrue(System.getProperty("java.version").startsWith("1.")); - } - - @Rule - public final transient TestPipeline pipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs("--runner=TestSamzaRunner").create()); - - @Rule - public final transient TestPipeline dropLateDataPipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs("--runner=TestSamzaRunner", "--dropLateData=true") - .create()); - - @Test - public void testDefaultGbk() { - TestStream.Builder testStream = - TestStream.create(VarIntCoder.of()) - .addElements(TimestampedValue.of(1, new Instant(1000))) - .addElements(TimestampedValue.of(2, new Instant(2000))) - .advanceWatermarkTo(new Instant(3000)) - .addElements(TimestampedValue.of(10, new Instant(1000))) - .advanceWatermarkTo(new Instant(10000)); - - PCollection aggregated = - pipeline - .apply(testStream.advanceWatermarkToInfinity()) - .apply( - Window.into(FixedWindows.of(Duration.standardSeconds(3))) - .accumulatingFiredPanes()) - .apply(Combine.globally(Sum.ofIntegers()).withoutDefaults()); - - PAssert.that(aggregated).containsInAnyOrder(Arrays.asList(3, 10)); - - pipeline.run().waitUntilFinish(); - } - - @Test - public void testDropLateDataNonKeyed() { - TestStream.Builder testStream = - TestStream.create(VarIntCoder.of()) - .addElements(TimestampedValue.of(1, new Instant(1000))) - .addElements(TimestampedValue.of(2, new Instant(2000))) - .advanceWatermarkTo(new Instant(3000)) - .addElements(TimestampedValue.of(10, new Instant(1000))) - .advanceWatermarkTo(new Instant(10000)); - - PCollection aggregated = - dropLateDataPipeline - .apply(testStream.advanceWatermarkToInfinity()) - .apply( - Window.into(FixedWindows.of(Duration.standardSeconds(3))) - .accumulatingFiredPanes()) - .apply(Combine.globally(Sum.ofIntegers()).withoutDefaults()); - - PAssert.that(aggregated).containsInAnyOrder(3); - - dropLateDataPipeline.run().waitUntilFinish(); - } - - @Test - public void testDropLateDataKeyed() { - TestStream.Builder> testStream = - TestStream.create(KvCoder.of(StringUtf8Coder.of(), VarIntCoder.of())) - .addElements(TimestampedValue.of(KV.of("a", 1), new Instant(1000))) - .addElements(TimestampedValue.of(KV.of("b", 2), new Instant(2000))) - .addElements(TimestampedValue.of(KV.of("a", 3), new Instant(2500))) - .advanceWatermarkTo(new Instant(3000)) - .addElements(TimestampedValue.of(KV.of("a", 10), new Instant(1000))) - .advanceWatermarkTo(new Instant(10000)); - - PCollection> aggregated = - dropLateDataPipeline - .apply(testStream.advanceWatermarkToInfinity()) - .apply( - Window.>into(FixedWindows.of(Duration.standardSeconds(3))) - .accumulatingFiredPanes()) - .apply(Sum.integersPerKey()); - - PAssert.that(aggregated).containsInAnyOrder(Arrays.asList(KV.of("a", 4), KV.of("b", 2))); - - dropLateDataPipeline.run().waitUntilFinish(); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java deleted file mode 100644 index 1a2f82b1a0d7..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.ByteArrayOutputStream; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.testing.CoderProperties; -import org.apache.beam.sdk.transforms.windowing.GlobalWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.Instant; -import org.junit.Test; - -/** Tests for {@link KeyedTimerData}. */ -public class KeyedTimerDataTest { - private static final Coder STRING_CODER = StringUtf8Coder.of(); - private static final Instant TIMESTAMP = - new DateTime(2020, 8, 11, 13, 42, 9, DateTimeZone.UTC).toInstant(); - // TODO: LISAMZA-19205 Test OUTPUT_TIMESTAMP after outputTimestamp is encoded - // private static final Instant OUTPUT_TIMESTAMP = TIMESTAMP.plus(Duration.standardSeconds(30)); - - @Test - public void testCoder() throws Exception { - final TimerInternals.TimerData td = - TimerInternals.TimerData.of( - "timer", - StateNamespaces.global(), - TIMESTAMP, - TIMESTAMP, - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - - final String key = "timer-key"; - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - STRING_CODER.encode(key, baos); - final byte[] keyBytes = baos.toByteArray(); - final KeyedTimerData ktd = new KeyedTimerData<>(keyBytes, key, td); - - final KeyedTimerData.KeyedTimerDataCoder ktdCoder = - new KeyedTimerData.KeyedTimerDataCoder<>(STRING_CODER, GlobalWindow.Coder.INSTANCE); - - // TODO: LISAMZA-19205: use CoderProperties.coderDecodeEncodeEqual - CoderProperties.coderDecodeEncodeEqualInContext(ktdCoder, Coder.Context.OUTER, ktd); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java deleted file mode 100644 index 522e146d21e6..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.apache.beam.runners.core.TimerInternals; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -public class PortableBundleManagerTest { - - @Mock BundleManager.BundleProgressListener bundleProgressListener; - @Mock Scheduler> bundleTimerScheduler; - @Mock OpEmitter emitter; - - PortableBundleManager portableBundleManager; - - private static final String TIMER_ID = "timerId"; - - private static final long MAX_BUNDLE_TIME_MS = 100000; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void test() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 1, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - } - - @Test - public void testWhen() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 4, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - } - - @Test - public void testWhenElementCountNotReachedTHenBundleDoesntFinish() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 4, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryFinishBundle(emitter); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - verify(bundleProgressListener, times(0)).onBundleFinished(any()); - } - - @Test - public void testWhenElementCountReachedThenFinishBundle() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 4, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryFinishBundle(emitter); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - verify(bundleProgressListener, times(1)).onBundleFinished(any()); - } - - @Test - public void testWhenBundleTimeReachedThenFinishBundle() throws Exception { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 4, 1, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - Thread.sleep(2); - portableBundleManager.tryFinishBundle(emitter); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - verify(bundleProgressListener, times(1)).onBundleFinished(any()); - } - - @Test - public void testWhenSignalFailureThenResetBundle() throws Exception { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 4, 1, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.signalFailure(new Exception()); - portableBundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(2)).onBundleStarted(); - } - - @Test - public void testProcessWatermarkWhenBundleNotStarted() { - Instant watermark = new Instant(); - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 4, 1, bundleTimerScheduler, TIMER_ID); - portableBundleManager.processWatermark(watermark, emitter); - verify(bundleProgressListener, times(1)).onWatermark(eq(watermark), eq(emitter)); - } - - @Test - public void testQueueWatermarkWhenBundleStarted() { - Instant watermark = new Instant(); - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 1, 1, bundleTimerScheduler, TIMER_ID); - - portableBundleManager.tryStartBundle(); - portableBundleManager.processWatermark(watermark, emitter); - verify(bundleProgressListener, times(0)).onWatermark(eq(watermark), eq(emitter)); - - portableBundleManager.tryFinishBundle(emitter); - verify(bundleProgressListener, times(1)).onWatermark(eq(watermark), eq(emitter)); - } - - @Test - public void testProcessTimerTriesFinishBundle() { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 1, 1, bundleTimerScheduler, TIMER_ID); - - portableBundleManager.tryStartBundle(); - KeyedTimerData keyedTimerData = mock(KeyedTimerData.class); - TimerInternals.TimerData timerData = mock(TimerInternals.TimerData.class); - when(keyedTimerData.getTimerData()).thenReturn(timerData); - when(timerData.getTimerId()).thenReturn(TIMER_ID); - - portableBundleManager.processTimer(keyedTimerData, emitter); - verify(bundleProgressListener, times(1)).onBundleFinished(any()); - verify(bundleTimerScheduler).schedule(any(KeyedTimerData.class), anyLong()); - } - - @Test - public void testDifferentTimerIdIsIgnored() { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 1, 1, bundleTimerScheduler, TIMER_ID); - - portableBundleManager.tryStartBundle(); - KeyedTimerData keyedTimerData = mock(KeyedTimerData.class); - TimerInternals.TimerData timerData = mock(TimerInternals.TimerData.class); - when(keyedTimerData.getTimerData()).thenReturn(timerData); - when(timerData.getTimerId()).thenReturn("NOT_TIMER_ID"); - - portableBundleManager.processTimer(keyedTimerData, emitter); - verify(bundleProgressListener, times(0)).onBundleFinished(any()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java deleted file mode 100644 index 7fde83c02e5f..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.DISTRIBUTION_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.LATEST_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.SUM_INT64_TYPE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.model.fnexecution.v1.BeamFnApi; -import org.apache.beam.model.pipeline.v1.MetricsApi; -import org.apache.beam.runners.core.metrics.CounterCell; -import org.apache.beam.runners.core.metrics.DistributionCell; -import org.apache.beam.runners.core.metrics.GaugeCell; -import org.apache.beam.runners.core.metrics.MonitoringInfoConstants; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.sdk.metrics.MetricName; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.Before; -import org.junit.Test; - -public class SamzaMetricsBundleProgressHandlerTest { - - public static final String EXPECTED_NAMESPACE = "namespace"; - public static final String EXPECTED_COUNTER_NAME = "counterName"; - private MetricsRegistryMap metricsRegistryMap; - private SamzaMetricsContainer samzaMetricsContainer; - - private SamzaMetricsBundleProgressHandler samzaMetricsBundleProgressHandler; - private String stepName = "stepName"; - Map transformIdToUniqueName = new HashMap<>(); - - @Before - public void setup() { - metricsRegistryMap = new MetricsRegistryMap(); - samzaMetricsContainer = new SamzaMetricsContainer(metricsRegistryMap); - samzaMetricsBundleProgressHandler = - new SamzaMetricsBundleProgressHandler( - stepName, samzaMetricsContainer, transformIdToUniqueName); - } - - @Test - public void testCounter() { - // Hex for 123 - byte[] payload = "\173".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(SUM_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder().addMonitoringInfos(monitoringInfo).build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - CounterCell counter = - (CounterCell) samzaMetricsContainer.getContainer(stepName).getCounter(metricName); - - assertEquals(counter.getCumulative(), (Long) 123L); - } - - @Test - public void testGauge() { - // TimeStamp = 0, Value = 123 - byte[] payload = "\000\173".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(LATEST_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder().addMonitoringInfos(monitoringInfo).build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - GaugeCell gauge = (GaugeCell) samzaMetricsContainer.getContainer(stepName).getGauge(metricName); - - assertEquals(123L, gauge.getCumulative().value()); - assertTrue( - gauge.getCumulative().timestamp().isBefore(Instant.now().plus(Duration.millis(500)))); - assertTrue( - gauge.getCumulative().timestamp().isAfter(Instant.now().minus(Duration.millis(500)))); - } - - @Test - public void testDistribution() { - // Count = 123, sum = 124, min = 125, max = 126 - byte[] payload = "\173\174\175\176".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(DISTRIBUTION_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder().addMonitoringInfos(monitoringInfo).build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - DistributionCell gauge = - (DistributionCell) samzaMetricsContainer.getContainer(stepName).getDistribution(metricName); - - assertEquals(123L, gauge.getCumulative().count()); - assertEquals(124L, gauge.getCumulative().sum()); - assertEquals(125L, gauge.getCumulative().min()); - assertEquals(126L, gauge.getCumulative().max()); - } - - @Test - public void testEmptyPayload() { - - byte[] emptyPayload = "".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo emptyMonitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(SUM_INT64_TYPE) - .setPayload(ByteString.copyFrom(emptyPayload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - // Hex for 123 - byte[] payload = "\173".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(SUM_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder() - .addMonitoringInfos(emptyMonitoringInfo) - .addMonitoringInfos(monitoringInfo) - .addMonitoringInfos(emptyMonitoringInfo) - .build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - CounterCell counter = - (CounterCell) samzaMetricsContainer.getContainer(stepName).getCounter(metricName); - - assertEquals(counter.getCumulative(), (Long) 123L); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java deleted file mode 100644 index 9409efbcf394..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; - -import java.io.File; -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.TestSamzaRunner; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValue; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValueSerdeFactory; -import org.apache.beam.runners.samza.state.SamzaMapState; -import org.apache.beam.runners.samza.state.SamzaSetState; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.VarIntCoder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.CombiningState; -import org.apache.beam.sdk.state.MapState; -import org.apache.beam.sdk.state.ReadableState; -import org.apache.beam.sdk.state.SetState; -import org.apache.beam.sdk.state.StateSpec; -import org.apache.beam.sdk.state.StateSpecs; -import org.apache.beam.sdk.state.ValueState; -import org.apache.beam.sdk.testing.PAssert; -import org.apache.beam.sdk.testing.TestPipeline; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; -import org.apache.samza.context.ContainerContext; -import org.apache.samza.context.JobContext; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.serializers.Serde; -import org.apache.samza.storage.StorageEngineFactory; -import org.apache.samza.storage.kv.Entry; -import org.apache.samza.storage.kv.KeyValueIterator; -import org.apache.samza.storage.kv.KeyValueStore; -import org.apache.samza.storage.kv.KeyValueStoreMetrics; -import org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStorageEngineFactory; -import org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStore; -import org.apache.samza.system.SystemStreamPartition; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; - -/** Tests for SamzaStoreStateInternals. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - // TODO(https://github.com/apache/beam/issues/21230): Remove when new version of - // errorprone is released (2.11.0) - "unused" -}) -public class SamzaStoreStateInternalsTest implements Serializable { - @Rule - public final transient TestPipeline pipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs("--runner=TestSamzaRunner").create()); - - @BeforeClass - public static void beforeClass() { - // TODO(https://github.com/apache/beam/issues/32208) - assumeTrue(System.getProperty("java.version").startsWith("1.")); - } - - @Test - public void testMapStateIterator() { - final String stateId = "foo"; - final String countStateId = "count"; - - DoFn>, KV> fn = - new DoFn>, KV>() { - - @StateId(stateId) - private final StateSpec> mapState = - StateSpecs.map(StringUtf8Coder.of(), VarIntCoder.of()); - - @StateId(countStateId) - private final StateSpec> countState = - StateSpecs.combiningFromInputInternal(VarIntCoder.of(), Sum.ofIntegers()); - - @ProcessElement - public void processElement( - ProcessContext c, - @StateId(stateId) MapState mapState, - @StateId(countStateId) CombiningState count) { - SamzaMapState state = (SamzaMapState) mapState; - KV value = c.element().getValue(); - state.put(value.getKey(), value.getValue()); - count.add(1); - if (count.read() >= 4) { - final List> content = new ArrayList<>(); - final Iterator> iterator = state.readIterator().read(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - content.add(KV.of(entry.getKey(), entry.getValue())); - c.output(KV.of(entry.getKey(), entry.getValue())); - } - - assertEquals( - content, ImmutableList.of(KV.of("a", 97), KV.of("b", 42), KV.of("c", 12))); - } - } - }; - - PCollection> output = - pipeline - .apply( - Create.of( - KV.of("hello", KV.of("a", 97)), - KV.of("hello", KV.of("b", 42)), - KV.of("hello", KV.of("b", 42)), - KV.of("hello", KV.of("c", 12)))) - .apply(ParDo.of(fn)); - - PAssert.that(output).containsInAnyOrder(KV.of("a", 97), KV.of("b", 42), KV.of("c", 12)); - - pipeline.run(); - } - - @Test - public void testSetStateIterator() { - final String stateId = "foo"; - final String countStateId = "count"; - - DoFn, Set> fn = - new DoFn, Set>() { - - @StateId(stateId) - private final StateSpec> setState = StateSpecs.set(VarIntCoder.of()); - - @StateId(countStateId) - private final StateSpec> countState = - StateSpecs.combiningFromInputInternal(VarIntCoder.of(), Sum.ofIntegers()); - - @ProcessElement - public void processElement( - ProcessContext c, - @StateId(stateId) SetState setState, - @StateId(countStateId) CombiningState count) { - SamzaSetState state = (SamzaSetState) setState; - ReadableState isEmpty = state.isEmpty(); - state.add(c.element().getValue()); - assertFalse(isEmpty.read()); - count.add(1); - if (count.read() >= 4) { - final Set content = new HashSet<>(); - final Iterator iterator = state.readIterator().read(); - while (iterator.hasNext()) { - Integer value = iterator.next(); - content.add(value); - } - c.output(content); - - assertEquals(content, Sets.newHashSet(97, 42, 12)); - } - } - }; - - PCollection> output = - pipeline - .apply( - Create.of( - KV.of("hello", 97), KV.of("hello", 42), KV.of("hello", 42), KV.of("hello", 12))) - .apply(ParDo.of(fn)); - - PAssert.that(output).containsInAnyOrder(Sets.newHashSet(97, 42, 12)); - - pipeline.run(); - } - - @Test - public void testValueStateSameIdAcrossParDo() { - final String stateId = "foo"; - - DoFn, KV> fn = - new DoFn, KV>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver> r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 0); - r.output(KV.of("sizzle", currentValue)); - state.write(currentValue + 1); - } - }; - - DoFn, Integer> fn2 = - new DoFn, Integer>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 13); - r.output(currentValue); - state.write(currentValue + 13); - } - }; - - PCollection> intermediate = - pipeline - .apply(Create.of(KV.of("hello", 42), KV.of("hello", 97), KV.of("hello", 84))) - .apply("First stateful ParDo", ParDo.of(fn)); - - PCollection output = intermediate.apply("Second stateful ParDo", ParDo.of(fn2)); - - PAssert.that(intermediate) - .containsInAnyOrder(KV.of("sizzle", 0), KV.of("sizzle", 1), KV.of("sizzle", 2)); - PAssert.that(output).containsInAnyOrder(13, 26, 39); - pipeline.run(); - } - - @Test - public void testValueStateSameIdAcrossParDoWithSameName() { - final String stateId = "foo"; - - DoFn, KV> fn = - new DoFn, KV>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver> r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 0); - r.output(KV.of("hello", currentValue)); - state.write(currentValue + 1); - } - }; - - DoFn, Integer> fn2 = - new DoFn, Integer>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 13); - r.output(currentValue); - state.write(currentValue + 13); - } - }; - - PCollection> intermediate = - pipeline - .apply(Create.of(KV.of("hello", 42), KV.of("hello", 97), KV.of("hello", 84))) - .apply("Stateful ParDo with Same Name", ParDo.of(fn)); - - PCollection output = - intermediate.apply("Stateful ParDo with Same Name", ParDo.of(fn2)); - - PAssert.that(intermediate) - .containsInAnyOrder(KV.of("hello", 0), KV.of("hello", 1), KV.of("hello", 2)); - - PAssert.that(output).containsInAnyOrder(13, 26, 39); - pipeline.run(); - } - - /** A storage engine to create test stores. */ - public static class TestStorageEngine extends InMemoryKeyValueStorageEngineFactory { - - @Override - public KeyValueStore getKVStore( - String storeName, - File storeDir, - MetricsRegistry registry, - SystemStreamPartition changeLogSystemStreamPartition, - JobContext jobContext, - ContainerContext containerContext, - StorageEngineFactory.StoreMode readWrite) { - KeyValueStoreMetrics metrics = new KeyValueStoreMetrics(storeName, registry); - return new TestStore(metrics); - } - } - - /** A test store based on InMemoryKeyValueStore. */ - public static class TestStore extends InMemoryKeyValueStore { - static List iterators = Collections.synchronizedList(new ArrayList<>()); - - public TestStore(KeyValueStoreMetrics metrics) { - super(metrics); - } - - @Override - public KeyValueIterator range(byte[] from, byte[] to) { - TestKeyValueIteraor iter = new TestKeyValueIteraor(super.range(from, to)); - iterators.add(iter); - return iter; - } - - static class TestKeyValueIteraor implements KeyValueIterator { - private final KeyValueIterator iter; - boolean closed = false; - - TestKeyValueIteraor(KeyValueIterator iter) { - this.iter = iter; - } - - @Override - public void close() { - iter.close(); - closed = true; - } - - @Override - public boolean hasNext() { - return iter.hasNext(); - } - - @Override - public Entry next() { - return iter.next(); - } - } - } - - @Test - public void testIteratorClosed() { - final String stateId = "foo"; - - DoFn, Set> fn = - new DoFn, Set>() { - - @StateId(stateId) - private final StateSpec> setState = StateSpecs.set(VarIntCoder.of()); - - @ProcessElement - public void processElement( - ProcessContext c, @StateId(stateId) SetState setState) { - SamzaSetState state = (SamzaSetState) setState; - state.add(c.element().getValue()); - - // the iterator for size needs to be closed - int size = Iterators.size(state.readIterator().read()); - - if (size > 1) { - final Iterator iterator = state.readIterator().read(); - assertTrue(iterator.hasNext()); - // this iterator should be closed too - iterator.next(); - } - } - }; - - pipeline - .apply( - Create.of( - KV.of("hello", 97), KV.of("hello", 42), KV.of("hello", 42), KV.of("hello", 12))) - .apply(ParDo.of(fn)); - - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setRunner(TestSamzaRunner.class); - Map configs = new HashMap<>(ConfigBuilder.localRunConfig()); - configs.put("stores.foo.factory", TestStorageEngine.class.getName()); - pipeline.getOptions().as(SamzaPipelineOptions.class).setConfigOverride(configs); - pipeline.run(); - - // The test code creates 7 underlying iterators, and 1 more is created during state.clear() - // Verify all of them are closed - assertEquals(8, TestStore.iterators.size()); - TestStore.iterators.forEach(iter -> assertTrue(iter.closed)); - } - - @Test - public void testStateValueSerde() throws IOException { - StateValueSerdeFactory stateValueSerdeFactory = new StateValueSerdeFactory(); - Serde> serde = (Serde) stateValueSerdeFactory.getSerde("Test", null); - int value = 123; - Coder coder = VarIntCoder.of(); - - byte[] valueBytes = serde.toBytes(StateValue.of(value, coder)); - StateValue stateValue1 = serde.fromBytes(valueBytes); - StateValue stateValue2 = StateValue.of(valueBytes); - assertEquals(stateValue1.getValue(coder).intValue(), value); - assertEquals(stateValue2.getValue(coder).intValue(), value); - - Integer nullValue = null; - byte[] nullBytes = serde.toBytes(StateValue.of(nullValue, coder)); - StateValue nullStateValue = serde.fromBytes(nullBytes); - assertNull(nullBytes); - assertNull(nullStateValue.getValue(coder)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java deleted file mode 100644 index ecedd7ae79f1..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java +++ /dev/null @@ -1,752 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.ByteArray; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.ByteArraySerdeFactory; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValue; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValueSerdeFactory; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.samza.config.MapConfig; -import org.apache.samza.context.TaskContext; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.operators.Scheduler; -import org.apache.samza.serializers.Serde; -import org.apache.samza.storage.kv.KeyValueStore; -import org.apache.samza.storage.kv.KeyValueStoreMetrics; -import org.apache.samza.storage.kv.RocksDbKeyValueStore; -import org.apache.samza.storage.kv.SerializedKeyValueStore; -import org.apache.samza.storage.kv.SerializedKeyValueStoreMetrics; -import org.joda.time.Instant; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.rocksdb.FlushOptions; -import org.rocksdb.Options; -import org.rocksdb.WriteOptions; - -/** - * Tests for {@link SamzaTimerInternalsFactory}. Covers both event-time timers and processing-timer - * timers. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaTimerInternalsFactoryTest { - @Rule public transient TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private KeyValueStore> createStore() { - final Options options = new Options(); - options.setCreateIfMissing(true); - - RocksDbKeyValueStore rocksStore = - new RocksDbKeyValueStore( - temporaryFolder.getRoot(), - options, - new MapConfig(), - false, - "beamStore", - new WriteOptions(), - new FlushOptions(), - new KeyValueStoreMetrics("beamStore", new MetricsRegistryMap())); - - return new SerializedKeyValueStore<>( - rocksStore, - new ByteArraySerdeFactory.ByteArraySerde(), - new StateValueSerdeFactory.StateValueSerde(), - new SerializedKeyValueStoreMetrics("beamStore", new MetricsRegistryMap())); - } - - private static SamzaStoreStateInternals.Factory createNonKeyedStateInternalsFactory( - SamzaPipelineOptions pipelineOptions, KeyValueStore> store) { - final TaskContext context = mock(TaskContext.class); - when(context.getStore(anyString())).thenReturn((KeyValueStore) store); - - return SamzaStoreStateInternals.createNonKeyedStateInternalsFactory( - "42", context, pipelineOptions); - } - - private static SamzaTimerInternalsFactory createTimerInternalsFactory( - Scheduler> timerRegistry, - String timerStateId, - SamzaPipelineOptions pipelineOptions, - KeyValueStore> store) { - - final SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory = - createNonKeyedStateInternalsFactory(pipelineOptions, store); - - return SamzaTimerInternalsFactory.createTimerInternalFactory( - StringUtf8Coder.of(), - timerRegistry, - timerStateId, - nonKeyedStateInternalsFactory, - (WindowingStrategy) WindowingStrategy.globalDefault(), - PCollection.IsBounded.BOUNDED, - pipelineOptions); - } - - private static class TestTimerRegistry implements Scheduler> { - private final List> timers = new ArrayList<>(); - - @Override - public void schedule(KeyedTimerData key, long timestamp) { - timers.add(key); - } - - @Override - public void delete(KeyedTimerData key) { - timers.remove(key); - } - } - - @Test - public void testEventTimeTimers() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - timerInternalsFactory.setInputWatermark(new Instant(5)); - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - assertTrue(readyTimers.isEmpty()); - - timerInternalsFactory.setInputWatermark(new Instant(20)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - assertEquals(timer1, readyTimers.iterator().next().getTimerData()); - - timerInternalsFactory.setInputWatermark(new Instant(150)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - assertEquals(timer2, readyTimers.iterator().next().getTimerData()); - - store.close(); - } - - @Test - public void testRestoreEventBufferSize() throws Exception { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final String key = "testKey"; - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey(key); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - store.close(); - - // restore by creating a new instance - store = createStore(); - - final SamzaTimerInternalsFactory restoredFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - assertEquals(1, restoredFactory.getEventTimeBuffer().size()); - - restoredFactory.setInputWatermark(new Instant(150)); - Collection> readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - - // Timer 1 should be evicted from buffer - assertTrue(restoredFactory.getEventTimeBuffer().isEmpty()); - final TimerInternals restoredTimerInternals = restoredFactory.timerInternalsForKey(key); - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(200), - new Instant(200), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - restoredTimerInternals.setTimer(timer2); - - // Timer 2 should be added to the Event buffer - assertEquals(1, restoredFactory.getEventTimeBuffer().size()); - // Timer 2 should not be ready - readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(0, readyTimers.size()); - - restoredFactory.setInputWatermark(new Instant(250)); - - // Timer 2 should be ready - readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringUtf8Coder.of().encode(key, baos); - byte[] keyBytes = baos.toByteArray(); - assertEquals( - new ArrayList<>(readyTimers), Arrays.asList(new KeyedTimerData<>(keyBytes, key, timer2))); - - store.close(); - } - - @Test - public void testRestore() throws Exception { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final String key = "testKey"; - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey(key); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - store.close(); - - // restore by creating a new instance - store = createStore(); - final SamzaTimerInternalsFactory restoredFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - restoredFactory.setInputWatermark(new Instant(150)); - Collection> readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(2, readyTimers.size()); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringUtf8Coder.of().encode(key, baos); - byte[] keyBytes = baos.toByteArray(); - assertEquals( - new ArrayList<>(readyTimers), - Arrays.asList( - new KeyedTimerData<>(keyBytes, key, timer1), - new KeyedTimerData<>(keyBytes, key, timer2))); - - store.close(); - } - - @Test - public void testProcessingTimeTimers() throws IOException { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - TestTimerRegistry timerRegistry = new TestTimerRegistry(); - - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(timerRegistry, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.PROCESSING_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.PROCESSING_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - final TimerInternals.TimerData timer3 = - TimerInternals.TimerData.of( - "timer3", - "timerFamilyId3", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.PROCESSING_TIME); - timerInternals.setTimer(timer3); - assertEquals(3, timerRegistry.timers.size()); - - store.close(); - - // restore by creating a new instance - store = createStore(); - TestTimerRegistry restoredRegistry = new TestTimerRegistry(); - final SamzaTimerInternalsFactory restoredFactory = - createTimerInternalsFactory(restoredRegistry, "timer", pipelineOptions, store); - - assertEquals(3, restoredRegistry.timers.size()); - - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringUtf8Coder.of().encode("testKey", baos); - final byte[] keyBytes = baos.toByteArray(); - restoredFactory.removeProcessingTimer(new KeyedTimerData<>(keyBytes, "testKey", timer1)); - restoredFactory.removeProcessingTimer(new KeyedTimerData<>(keyBytes, "testKey", timer2)); - restoredFactory.removeProcessingTimer(new KeyedTimerData<>(keyBytes, "testKey", timer3)); - store.close(); - } - - @Test - public void testOverride() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timerId", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - // this timer should override the first timer - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timerId", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - final TimerInternals.TimerData timer3 = - TimerInternals.TimerData.of( - "timerId2", - nameSpace, - new Instant(200), - new Instant(200), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer3); - - // this timer shouldn't override since it has a different id - timerInternalsFactory.setInputWatermark(new Instant(50)); - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(0, readyTimers.size()); - - timerInternalsFactory.setInputWatermark(new Instant(150)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - - timerInternalsFactory.setInputWatermark(new Instant(250)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - - store.close(); - } - - /** - * Test the number of expired event timers for each watermark does not exceed the predefined - * limit. - */ - @Test - public void testMaxExpiredEventTimersProcessAtOnce() { - // If maxExpiredTimersToProcessOnce <= the number of expired timers, then load - // "maxExpiredTimersToProcessOnce" timers. - testMaxExpiredEventTimersProcessAtOnce(10, 10, 5, 5); - testMaxExpiredEventTimersProcessAtOnce(10, 10, 10, 10); - - // If maxExpiredTimersToProcessOnce > the number of expired timers, then load all the ready - // timers. - testMaxExpiredEventTimersProcessAtOnce(10, 10, 20, 10); - } - - private void testMaxExpiredEventTimersProcessAtOnce( - int totalNumberOfTimersInStore, - int totalNumberOfExpiredTimers, - int maxExpiredTimersToProcessOnce, - int expectedExpiredTimersToProcess) { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setMaxReadyTimersToProcessOnce(maxExpiredTimersToProcessOnce); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - TimerInternals.TimerData timer; - for (int i = 0; i < totalNumberOfTimersInStore; i++) { - timer = - TimerInternals.TimerData.of( - "timer" + i, - nameSpace, - new Instant(i), - new Instant(i), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer); - } - - // Set the timestamp of the input watermark to be the value of totalNumberOfExpiredTimers - // so that totalNumberOfExpiredTimers timers are expected be expired with respect to this - // watermark. - final Instant inputWatermark = new Instant(totalNumberOfExpiredTimers); - timerInternalsFactory.setInputWatermark(inputWatermark); - final Collection> readyTimers = - timerInternalsFactory.removeReadyTimers(); - assertEquals(expectedExpiredTimersToProcess, readyTimers.size()); - store.close(); - } - - @Test - public void testBufferSizeNotExceedingPipelineOptionValue() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(2); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 5 timers. - // timers in memory are then timestamped from 0 - 1; - // timers in store are then timestamped from 0 - 4. - for (int i = 0; i < 5; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // only two timers are supposed to be in event time buffer - assertEquals(2, timerInternalsFactory.getEventTimeBuffer().size()); - - store.close(); - } - - @Test - public void testAllTimersAreFiredWithReload() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(2); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 3 timers. - // timers in memory now are timestamped from 0 - 1; - // timers in store now are timestamped from 0 - 2. - for (int i = 0; i < 3; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // total number of event time timers to fire equals to the number of timers in store - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(3)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - // buffer should reload from store and all timers are supposed to be fired. - assertEquals(3, readyTimers.size()); - - store.close(); - } - - /** - * Test the total number of event time timers reloaded into memory is aligned with the number of - * the event time timers written to the store. Moreover, event time timers reloaded into memory is - * maintained in order. - */ - @Test - public void testAllTimersAreFiredInOrder() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(5); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 8 timers. - // timers in memory now are timestamped from 0 - 4; - // timers in store now are timestamped from 0 - 7. - for (int i = 0; i < 8; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // fire the first 2 timers. - // timers in memory now are timestamped from 2 - 4; - // timers in store now are timestamped from 2 - 7. - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(1)); - long lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(2, readyTimers.size()); - - // add another 12 timers. - // timers in memory (reloaded for three times) now are timestamped from 2 - 4; 5 - 9; 10 - 14; - // 15 - 19. - // timers in store now are timestamped from 2 - 19. - // the total number of timers to fire is 18. - for (int i = 8; i < 20; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - timerInternalsFactory.setInputWatermark(new Instant(20)); - lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(18, readyTimers.size()); - - store.close(); - } - - @Test - public void testNewTimersAreInsertedInOrder() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(5); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 10 timers. - // timers in memory now are timestamped from 0 - 4; - // timers in store now are timestamped from 0 - 9. - for (int i = 0; i < 10; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // fire the first 2 timers. - // timers in memory now are timestamped from 2 - 4; - // timers in store now are timestamped from 2 - 9. - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(1)); - long lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(2, readyTimers.size()); - - // add 3 timers but timer 2 has duplicate so drop. - // timers in memory now are timestamped from 0 to 2 prefixed with lateTimer, and 2 to - // 4 prefixed with timer, timestamp is in order; - // timers in store now are timestamped from 0 to 2 prefixed with lateTimer, and 2 to 9 - // prefixed with timer, timestamp is in order; - for (int i = 0; i < 3; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // there are 11 timers in state now. - // watermark 5 comes, so 6 timers will be evicted because their timestamp is less than 5. - // memory will be reloaded once to have 5 to 8 left (reload to have 4 to 8, but 4 is evicted), 5 - // to 9 left in store. - // all of them are in order for firing. - timerInternalsFactory.setInputWatermark(new Instant(5)); - lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(6, readyTimers.size()); - assertEquals(4, timerInternalsFactory.getEventTimeBuffer().size()); - - // watermark 10 comes, so all timers will be evicted in order. - timerInternalsFactory.setInputWatermark(new Instant(10)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(4, readyTimers.size()); - assertEquals(0, timerInternalsFactory.getEventTimeBuffer().size()); - - store.close(); - } - - @Test - public void testBufferRefilledAfterRestoreToNonFullState() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(5); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare (buffer capacity + 1) 6 timers. - // timers in memory now are timestamped from 0 - 4; - // timer in store now is timestamped 6. - for (int i = 0; i < 6; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // total number of event time timers to fire equals to the number of timers in store - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(4)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(5, readyTimers.size()); - // reloaded timer5 - assertEquals(1, timerInternalsFactory.getEventTimeBuffer().size()); - - for (int i = 6; i < 13; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - // timers should go into buffer not state - assertEquals(5, timerInternalsFactory.getEventTimeBuffer().size()); - - // watermark 10 comes,6 timers will be evicted in order and 2 still in buffer. - timerInternalsFactory.setInputWatermark(new Instant(10)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - long lastTimestamp = 0; - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(6, readyTimers.size()); - assertEquals(2, timerInternalsFactory.getEventTimeBuffer().size()); - - store.close(); - } - - @Test - public void testByteArray() { - ByteArray key1 = ByteArray.of("hello world".getBytes(StandardCharsets.UTF_8)); - Serde serde = new ByteArraySerdeFactory().getSerde("", null); - byte[] keyBytes = serde.toBytes(key1); - ByteArray key2 = serde.fromBytes(keyBytes); - assertEquals(key1, key2); - - Map map = new HashMap<>(); - map.put(key1, "found it"); - assertEquals("found it", map.get(key2)); - - map.remove(key1); - assertTrue(!map.containsKey(key2)); - assertTrue(map.isEmpty()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java deleted file mode 100644 index e6029beb93b0..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.concurrent.TimeoutException; -import org.junit.Test; - -public class SdkHarnessDoFnRunnerTest { - - @Test(expected = TimeoutException.class) - public void testRunWithTimeoutOccurred() throws Exception { - SamzaDoFnRunners.SdkHarnessDoFnRunner.runWithTimeout( - 100, - () -> { - try { - Thread.sleep(500); - } catch (InterruptedException ignored) { - } - }); - } - - @Test - public void testRunWithTimeoutDisabled() throws Exception { - SamzaDoFnRunners.SdkHarnessDoFnRunner.runWithTimeout( - -1, - () -> { - try { - Thread.sleep(500); - } catch (InterruptedException ignored) { - } - }); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java deleted file mode 100644 index 9fbc515979b0..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaExecutionEnvironment; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.SamzaRunner; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.StateSpec; -import org.apache.beam.sdk.state.StateSpecs; -import org.apache.beam.sdk.state.ValueState; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.Impulse; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.sdk.values.TypeDescriptors; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; -import org.apache.samza.config.Config; -import org.apache.samza.config.JobConfig; -import org.apache.samza.config.JobCoordinatorConfig; -import org.apache.samza.config.TaskConfig; -import org.apache.samza.config.ZkConfig; -import org.apache.samza.job.yarn.YarnJobFactory; -import org.apache.samza.runtime.LocalApplicationRunner; -import org.apache.samza.runtime.RemoteApplicationRunner; -import org.apache.samza.storage.kv.RocksDbKeyValueStorageEngineFactory; -import org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStorageEngineFactory; -import org.apache.samza.zk.ZkJobCoordinatorFactory; -import org.junit.Test; - -/** Test config generations for {@link org.apache.beam.runners.samza.SamzaRunner}. */ -// TODO(https://github.com/apache/beam/issues/21230): Remove when new version of errorprone is -// released (2.11.0) -@SuppressWarnings("unused") -public class ConfigGeneratorTest { - private static final String APP_RUNNER_CLASS = "app.runner.class"; - private static final String JOB_FACTORY_CLASS = "job.factory.class"; - - @Test - public void testStatefulBeamStoreConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.beamStore.factory")); - assertEquals("byteArraySerde", config.get("stores.beamStore.key.serde")); - assertEquals("stateValueSerde", config.get("stores.beamStore.msg.serde")); - assertNull(config.get("stores.beamStore.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-beamStore-changelog", config2.get("stores.beamStore.changelog")); - } - - @Test - public void testStatelessBeamStoreConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Impulse.create()).apply(Filter.by(Objects::nonNull)); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - InMemoryKeyValueStorageEngineFactory.class.getName(), - config.get("stores.beamStore.factory")); - assertEquals("byteArraySerde", config.get("stores.beamStore.key.serde")); - assertEquals("stateValueSerde", config.get("stores.beamStore.msg.serde")); - assertNull(config.get("stores.beamStore.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - // For stateless jobs, ignore state durable pipeline option. - assertNull(config2.get("stores.beamStore.changelog")); - } - - @Test - public void testSamzaLocalExecutionEnvironmentConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.LOCAL); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertTrue( - Maps.difference(config, ConfigBuilder.localRunConfig()).entriesOnlyOnRight().isEmpty()); - } - - @Test - public void testSamzaYarnExecutionEnvironmentConfig() { - final String yarnPackagePath = "yarn.package.path"; - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.YARN); - options.setConfigOverride( - ImmutableMap.builder() - .put( - yarnPackagePath, - "file://${basedir}/target/${project.artifactId}-${pom.version}-dist.tar.gz") - .build()); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - try { - Config config = configBuilder.build(); - assertEquals(config.get(APP_RUNNER_CLASS), RemoteApplicationRunner.class.getName()); - assertEquals(config.get(JOB_FACTORY_CLASS), YarnJobFactory.class.getName()); - } catch (IllegalArgumentException e) { - throw new AssertionError( - String.format( - "Failed to validate correct configs for %s samza execution environment", - SamzaExecutionEnvironment.YARN), - e); - } - } - - @Test - public void testSamzaStandAloneExecutionEnvironmentConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.STANDALONE); - options.setConfigOverride( - ImmutableMap.builder().put(ZkConfig.ZK_CONNECT, "localhost:2181").build()); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - try { - Config config = configBuilder.build(); - assertEquals(config.get(APP_RUNNER_CLASS), LocalApplicationRunner.class.getName()); - assertEquals( - config.get(JobCoordinatorConfig.JOB_COORDINATOR_FACTORY), - ZkJobCoordinatorFactory.class.getName()); - } catch (IllegalArgumentException e) { - throw new AssertionError( - String.format( - "Failed to validate correct configs for %s samza execution environment", - SamzaExecutionEnvironment.STANDALONE), - e); - } - } - - @Test - public void testUserStoreConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply( - Create.empty(TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings()))) - .apply( - ParDo.of( - new DoFn, Void>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) {} - })); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState.factory")); - assertEquals("byteArraySerde", config.get("stores.testState.key.serde")); - assertEquals("stateValueSerde", config.get("stores.testState.msg.serde")); - assertNull(config.get("stores.testState.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-testState-changelog", config2.get("stores.testState.changelog")); - } - - @Test - public void testUserStoreConfigSameStateIdAcrossParDo() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply( - Create.empty(TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings()))) - .apply( - "First stateful ParDo", - ParDo.of( - new DoFn, KV>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) { - context.output(context.element()); - } - })) - .apply( - "Second stateful ParDo", - ParDo.of( - new DoFn, Void>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) {} - })); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-First_stateful_ParDo.factory")); - assertEquals("byteArraySerde", config.get("stores.testState-First_stateful_ParDo.key.serde")); - assertEquals("stateValueSerde", config.get("stores.testState-First_stateful_ParDo.msg.serde")); - assertNull(config.get("stores.testState-First_stateful_ParDo.changelog")); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-Second_stateful_ParDo.factory")); - assertEquals("byteArraySerde", config.get("stores.testState-Second_stateful_ParDo.key.serde")); - assertEquals("stateValueSerde", config.get("stores.testState-Second_stateful_ParDo.msg.serde")); - assertNull(config.get("stores.testState-Second_stateful_ParDo.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-testState-First_stateful_ParDo-changelog", - config2.get("stores.testState-First_stateful_ParDo.changelog")); - assertEquals( - "TestStoreConfig-1-testState-Second_stateful_ParDo-changelog", - config2.get("stores.testState-Second_stateful_ParDo.changelog")); - } - - @Test - public void testUserStoreConfigSameStateIdAndPTransformName() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply( - Create.empty(TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings()))) - .apply( - "Same stateful ParDo Name", - ParDo.of( - new DoFn, KV>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) { - context.output(context.element()); - } - })) - .apply( - "Same stateful ParDo Name", - ParDo.of( - new DoFn, Void>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) {} - })); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-Same_stateful_ParDo_Name.factory")); - assertEquals( - "byteArraySerde", config.get("stores.testState-Same_stateful_ParDo_Name.key.serde")); - assertEquals( - "stateValueSerde", config.get("stores.testState-Same_stateful_ParDo_Name.msg.serde")); - assertNull(config.get("stores.testState-Same_stateful_ParDo_Name.changelog")); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-Same_stateful_ParDo_Name2.factory")); - assertEquals( - "byteArraySerde", config.get("stores.testState-Same_stateful_ParDo_Name2.key.serde")); - assertEquals( - "stateValueSerde", config.get("stores.testState-Same_stateful_ParDo_Name2.msg.serde")); - assertNull(config.get("stores.testState-Same_stateful_ParDo_Name2.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-testState-Same_stateful_ParDo_Name-changelog", - config2.get("stores.testState-Same_stateful_ParDo_Name.changelog")); - assertEquals( - "TestStoreConfig-1-testState-Same_stateful_ParDo_Name2-changelog", - config2.get("stores.testState-Same_stateful_ParDo_Name2.changelog")); - } - - @Test - public void testCreateBundleConfig() { - // autosizing = 0: disabled - // autosizing = 1: enabled - for (int autosizing = 0; autosizing < 2; autosizing++) { - final SamzaPipelineOptions options = PipelineOptionsFactory.as(SamzaPipelineOptions.class); - final Map config = new HashMap<>(); - - // bundle size = 1 - options.setMaxBundleSize(1); - config.put(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE, "5"); - if (autosizing != 0) { - // Test autosizing enabled, the output should be the same - config.put(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE, "5"); - } - - Map bundleConfig = ConfigBuilder.createBundleConfig(options, config); - - assertEquals("1", bundleConfig.get(TaskConfig.MAX_CONCURRENCY)); - assertNull(bundleConfig.get(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE)); - assertNull(bundleConfig.get(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE)); - assertEquals(1, options.getNumThreadsForProcessElement()); - - // bundle size = 3, NumThreadsForProcessElement = 10 - options.setMaxBundleSize(3); - options.setNumThreadsForProcessElement(10); - bundleConfig = ConfigBuilder.createBundleConfig(options, config); - - assertEquals("3", bundleConfig.get(TaskConfig.MAX_CONCURRENCY)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE)); - assertEquals(10, options.getNumThreadsForProcessElement()); - - // bundle size = 3, NumThreadsForProcessElement = 1 (default), threadPoolSize = 5 - options.setNumThreadsForProcessElement(1); - bundleConfig = ConfigBuilder.createBundleConfig(options, config); - - assertEquals("3", bundleConfig.get(TaskConfig.MAX_CONCURRENCY)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE)); - assertEquals(5, options.getNumThreadsForProcessElement()); - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java deleted file mode 100644 index 9cc0cc10894a..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.samza.Partition; -import org.apache.samza.config.MapConfig; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemStreamPartition; -import org.apache.samza.system.WatermarkMessage; -import org.junit.Assert; -import org.junit.Test; - -/** - * Tests for {@link - * org.apache.beam.runners.samza.translation.SamzaImpulseSystemFactory.SamzaImpulseSystemConsumer}. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) -}) -public class SamzaImpulseSystemTest { - @Test - public void testSamzaImpulseSystemConsumer() throws Exception { - SystemConsumer consumer = - new SamzaImpulseSystemFactory().getConsumer("default-system", new MapConfig(), null); - Map> result = - consumer.poll(Collections.singleton(sspForPartition(0)), 100); - Assert.assertEquals(1, result.size()); - Assert.assertTrue(result.containsKey(sspForPartition(0))); - - List messageEnvelopes = result.get(sspForPartition(0)); - Assert.assertEquals(3, messageEnvelopes.size()); - - Assert.assertTrue(messageEnvelopes.get(0).getMessage() instanceof OpMessage); - OpMessage impulseEvent = (OpMessage) messageEnvelopes.get(0).getMessage(); - Assert.assertEquals(OpMessage.Type.ELEMENT, impulseEvent.getType()); - - Assert.assertTrue(messageEnvelopes.get(1).getMessage() instanceof WatermarkMessage); - - Assert.assertTrue(messageEnvelopes.get(2).isEndOfStream()); - } - - private SystemStreamPartition sspForPartition(int i) { - return new SystemStreamPartition("default-system", "default-stream", new Partition(i)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java deleted file mode 100644 index bb357dd6aced..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.mock; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PValue; -import org.apache.samza.application.descriptors.StreamApplicationDescriptor; -import org.apache.samza.application.descriptors.StreamApplicationDescriptorImpl; -import org.apache.samza.config.Config; -import org.apache.samza.config.MapConfig; -import org.apache.samza.operators.KV; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.operators.functions.MapFunction; -import org.apache.samza.serializers.KVSerde; -import org.apache.samza.serializers.NoOpSerde; -import org.apache.samza.serializers.Serde; -import org.apache.samza.system.descriptors.GenericInputDescriptor; -import org.apache.samza.system.descriptors.GenericSystemDescriptor; -import org.junit.Test; - -@SuppressWarnings({"rawtypes"}) -public class TranslationContextTest { - private final GenericInputDescriptor testInputDescriptor = - new GenericSystemDescriptor("mockSystem", "mockFactoryClassName") - .getInputDescriptor("test-input-1", mock(Serde.class)); - MapFunction keyFn = m -> m.toString(); - MapFunction valueFn = m -> m; - private final String streamName = "testStream"; - KVSerde serde = KVSerde.of(new NoOpSerde<>(), new NoOpSerde<>()); - StreamApplicationDescriptor streamApplicationDescriptor = - new StreamApplicationDescriptorImpl( - appDesc -> { - MessageStream inputStream = appDesc.getInputStream(testInputDescriptor); - inputStream.partitionBy(keyFn, valueFn, serde, streamName); - }, - getConfig()); - Map idMap = new HashMap<>(); - Set nonUniqueStateIds = new HashSet<>(); - TranslationContext translationContext = - new TranslationContext( - streamApplicationDescriptor, idMap, nonUniqueStateIds, mock(SamzaPipelineOptions.class)); - - @Test - public void testRegisterInputMessageStreams() { - final PCollection output = mock(PCollection.class); - List topics = Arrays.asList("stream1", "stream2"); - List inputDescriptors = - topics.stream() - .map(topicName -> createSamzaInputDescriptor(topicName, topicName)) - .collect(Collectors.toList()); - - translationContext.registerInputMessageStreams(output, inputDescriptors); - - assertNotNull(translationContext.getMessageStream(output)); - } - - public GenericInputDescriptor>> createSamzaInputDescriptor( - String systemName, String streamId) { - final Serde>> kvSerde = - KVSerde.of(new NoOpSerde<>(), new NoOpSerde<>()); - return new GenericSystemDescriptor(systemName, "factoryClass") - .getInputDescriptor(streamId, kvSerde); - } - - private static Config getConfig() { - HashMap configMap = new HashMap<>(); - configMap.put("job.name", "testJobName"); - configMap.put("job.id", "testJobId"); - return new MapConfig(configMap); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java deleted file mode 100644 index cef1a463ff96..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.junit.Assert.assertEquals; - -import java.io.Serializable; -import java.util.Objects; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.GroupByKey; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.PipelineTranslation; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.GreedyPipelineFuser; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PDone; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.junit.Test; - -public class DoFnUtilsTest implements Serializable { - private final Pipeline pipeline = Pipeline.create(); - - @Test - public void testExecutableStageWithoutOutput() { - pipeline.apply(Create.of(KV.of(1L, "1"))); - - assertEquals("[Create.Values-]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - @Test - public void testExecutableStageWithCustomizedName() { - pipeline.apply("MyCreateOf", Create.of(KV.of(1L, "1"))); - assertEquals("[MyCreateOf-]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - @Test - public void testExecutableStageWithOutput() { - pipeline - .apply("MyCreateOf", Create.of(KV.of(1L, "1"))) - .apply("MyFilterBy", Filter.by(Objects::nonNull)) - .apply(GroupByKey.create()); - - assertEquals("[MyCreateOf-MyFilterBy]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - @Test - public void testExecutableStageWithPDone() { - pipeline - .apply("MyCreateOf", Create.of("1")) - .apply( - "PDoneTransform", - new PTransform, PDone>() { - @Override - public PDone expand(PCollection input) { - return PDone.in(pipeline); - } - }); - - assertEquals("[MyCreateOf-]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - private static ExecutableStage getOnlyExecutableStage(Pipeline p) { - return Iterables.getOnlyElement( - GreedyPipelineFuser.fuse(PipelineTranslation.toProto(p)).getFusedStages()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java deleted file mode 100644 index c261fc623af3..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; -import org.junit.Assert; -import org.junit.Test; - -/** Unit tests for {@linkplain FutureUtils}. */ -public final class FutureUtilsTest { - private static final List RESULTS = ImmutableList.of("hello", "world"); - - @Test - public void testFlattenFuturesForCollection() { - CompletionStage> resultFuture = - FutureUtils.flattenFutures( - ImmutableList.of( - CompletableFuture.completedFuture("hello"), - CompletableFuture.completedFuture("world"))); - - CompletionStage validationFuture = - resultFuture.thenAccept( - actualResults -> { - Assert.assertEquals( - "Expected flattened results to contain {hello, world}", - RESULTS, - Lists.newArrayList(actualResults)); - }); - - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testFlattenFuturesForFailedFuture() { - CompletionStage> resultFuture = - FutureUtils.flattenFutures( - ImmutableList.of( - CompletableFuture.completedFuture("hello"), - createFailedFuture(new RuntimeException()))); - - CompletionStage validationFuture = - resultFuture.handle( - (results, ex) -> { - Assert.assertTrue( - "Expected exception to be of RuntimeException", ex instanceof RuntimeException); - return null; - }); - - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testWaitForAllFutures() { - CountDownLatch latch = new CountDownLatch(1); - CompletionStage> resultFuture = - FutureUtils.flattenFutures( - ImmutableList.of( - CompletableFuture.supplyAsync( - () -> { - try { - latch.await(); - } catch (InterruptedException e) { - return ""; - } - - return "hello"; - }), - CompletableFuture.supplyAsync( - () -> { - latch.countDown(); - return "world"; - }))); - - CompletionStage validationFuture = - resultFuture.thenAccept( - actualResults -> { - Assert.assertEquals( - "Expected flattened results to contain {hello, world}", - RESULTS, - Lists.newArrayList(actualResults)); - }); - - validationFuture.toCompletableFuture().join(); - } - - private static CompletionStage createFailedFuture(Throwable t) { - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(t); - return future; - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java deleted file mode 100644 index fed43430a440..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.HashMap; -import java.util.Map; -import org.apache.samza.metrics.MetricsReporter; -import org.apache.samza.metrics.ReadableMetricsRegistry; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** An in-memory {@link MetricsReporter} for testing. */ -public class InMemoryMetricsReporter implements MetricsReporter { - private Map registries; - - public InMemoryMetricsReporter() { - registries = new HashMap<>(); - } - - @Override - public void start() {} - - @Override - public void register(String source, ReadableMetricsRegistry registry) { - registries.put(source, registry); - } - - @Override - public void stop() {} - - public @Nullable ReadableMetricsRegistry getMetricsRegistry(@NonNull String source) { - return registries.get(source); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java deleted file mode 100644 index 0a4f532808b1..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.junit.Assert.assertEquals; - -import com.google.gson.JsonParser; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaExecutionEnvironment; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.SamzaRunner; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.runners.samza.translation.ConfigContext; -import org.apache.beam.runners.samza.translation.PViewToIdMapper; -import org.apache.beam.runners.samza.translation.SamzaPipelineTranslator; -import org.apache.beam.runners.samza.translation.SamzaTransformOverrides; -import org.apache.beam.runners.samza.translation.StateIdParser; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.Impulse; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.Window; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.sdk.values.TimestampedValue; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.Test; - -/** Tests for {@link org.apache.beam.runners.samza.util.PipelineJsonRenderer}. */ -public class PipelineJsonRendererTest { - - @Test - public void testEmptyPipeline() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setRunner(SamzaRunner.class); - - Pipeline p = Pipeline.create(options); - final Map idMap = PViewToIdMapper.buildIdMap(p); - final Set nonUniqueStateIds = StateIdParser.scan(p); - final ConfigContext ctx = new ConfigContext(idMap, nonUniqueStateIds, options); - - String jsonDag = - "{ \"RootNode\": [" - + " { \"fullName\":\"OuterMostNode\"," - + " \"ChildNodes\":[ ]}],\"graphLinks\": [],\"transformIOInfo\": []" - + "}"; - - assertEquals( - JsonParser.parseString(jsonDag), - JsonParser.parseString( - PipelineJsonRenderer.toJsonString(p, ctx).replaceAll(System.lineSeparator(), ""))); - } - - @Test - public void testCompositePipeline() throws IOException { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setRunner(SamzaRunner.class); - options.setJobName("TestEnvConfig"); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.LOCAL); - - Pipeline p = Pipeline.create(options); - - p.apply( - Create.timestamped( - TimestampedValue.of(KV.of(1, 1), new Instant(1)), - TimestampedValue.of(KV.of(2, 2), new Instant(2)))) - .apply(Window.into(FixedWindows.of(Duration.millis(10)))) - .apply(Sum.integersPerKey()); - - p.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(p); - final Set nonUniqueStateIds = StateIdParser.scan(p); - final ConfigContext ctx = new ConfigContext(idMap, nonUniqueStateIds, options); - - String jsonDagFileName = "src/test/resources/ExpectedDag.json"; - String jsonDag = - new String(Files.readAllBytes(Paths.get(jsonDagFileName)), StandardCharsets.UTF_8); - String renderedDag = PipelineJsonRenderer.toJsonString(p, ctx); - - assertEquals( - JsonParser.parseString(jsonDag), - JsonParser.parseString(renderedDag.replaceAll(System.lineSeparator(), ""))); - } - - @Test - public void testBeamTransformIOConfigGen() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.LOCAL); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Impulse.create()).apply(Filter.by(Objects::nonNull)); - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Map> transformInputOutput = - PipelineJsonRenderer.buildTransformIOMap(pipeline, configCtx); - - assertEquals(2, transformInputOutput.size()); - assertEquals("", transformInputOutput.get("Impulse").getKey()); // no input to impulse - assertEquals( - "Impulse.out", - transformInputOutput.get("Impulse").getValue()); // PValue for to Impulse.output - - // Input to Filter is PValue Output from Impulse - assertEquals( - "Impulse.out", - transformInputOutput.get("Filter/ParDo(Anonymous)/ParMultiDo(Anonymous)").getKey()); - // output PValue of filter - assertEquals( - "Filter/ParDo(Anonymous)/ParMultiDo(Anonymous).output", - transformInputOutput.get("Filter/ParDo(Anonymous)/ParMultiDo(Anonymous)").getValue()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java deleted file mode 100644 index 14ab5b1af5fb..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.junit.Assert; -import org.junit.Test; - -public class PortableConfigUtilsTest { - - @Test - public void testNonPortableMode() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - Map config = new HashMap<>(); - config.put(PortableConfigUtils.BEAM_PORTABLE_MODE, "false"); - doReturn(config).when(mockOptions).getConfigOverride(); - Assert.assertFalse( - "Expected false for portable mode ", PortableConfigUtils.isPortable(mockOptions)); - } - - @Test - public void testNonPortableModeNullConfig() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - doReturn(null).when(mockOptions).getConfigOverride(); - Assert.assertFalse( - "Expected false for portable mode ", PortableConfigUtils.isPortable(mockOptions)); - } - - @Test - public void testPortableMode() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - Map config = new HashMap<>(); - config.put(PortableConfigUtils.BEAM_PORTABLE_MODE, "true"); - doReturn(config).when(mockOptions).getConfigOverride(); - Assert.assertTrue( - "Expected true for portable runner", PortableConfigUtils.isPortable(mockOptions)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java deleted file mode 100644 index cf765e3db221..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.mockito.Mockito.mock; - -import java.util.Set; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.Count; -import org.apache.beam.sdk.transforms.MapElements; -import org.apache.beam.sdk.transforms.Max; -import org.apache.beam.sdk.transforms.Min; -import org.apache.beam.sdk.transforms.SerializableFunction; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; -import org.junit.Assert; -import org.junit.Test; - -/** Test class for {@link HashIdGenerator}. */ -public class TestHashIdGenerator { - - @Test - public void testGetId() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - final Set ids = - ImmutableSet.of( - idGenerator.getId(Count.perKey().getName()), - idGenerator.getId(MapElements.into(null).getName()), - idGenerator.getId(Count.globally().getName()), - idGenerator.getId(Combine.perKey(mock(SerializableFunction.class)).getName()), - idGenerator.getId(Min.perKey().getName()), - idGenerator.getId(Max.globally().getName())); - Assert.assertEquals(6, ids.size()); - } - - @Test - public void testGetShortId() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - String id = idGenerator.getId("abcd"); - Assert.assertEquals("abcd", id); - } - - @Test - public void testSameNames() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - String id1 = idGenerator.getId(Count.perKey().getName()); - String id2 = idGenerator.getId(Count.perKey().getName()); - Assert.assertNotEquals(id1, id2); - } - - @Test - public void testSameShortNames() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - String id = idGenerator.getId("abcd"); - Assert.assertEquals("abcd", id); - String id2 = idGenerator.getId("abcd"); - Assert.assertNotEquals("abcd", id2); - } - - @Test - public void testLongHash() { - final HashIdGenerator idGenerator = new HashIdGenerator(10); - String id1 = idGenerator.getId(Count.perKey().getName()); - String id2 = idGenerator.getId(Count.perKey().getName()); - String id3 = idGenerator.getId(Count.perKey().getName()); - String id4 = idGenerator.getId(Count.perKey().getName()); - Assert.assertNotEquals(id1, id2); - Assert.assertNotEquals(id3, id2); - Assert.assertNotEquals(id3, id4); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java deleted file mode 100644 index 9c614175e5de..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.VarLongCoder; -import org.apache.beam.sdk.coders.VoidCoder; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.IntervalWindow; -import org.apache.beam.sdk.transforms.windowing.TimestampCombiner; -import org.apache.beam.sdk.util.construction.Environments; -import org.apache.beam.sdk.util.construction.SdkComponents; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.joda.time.Duration; -import org.junit.Test; - -/** Unit tests for {@link WindowUtils}. */ -public class WindowUtilsTest { - - @Test - public void testGetWindowStrategy() throws IOException { - SdkComponents components = SdkComponents.create(); - String environmentId = - components.registerEnvironment(Environments.createDockerEnvironment("java")); - WindowingStrategy expected = - WindowingStrategy.of(FixedWindows.of(Duration.standardMinutes(1))) - .withMode(WindowingStrategy.AccumulationMode.DISCARDING_FIRED_PANES) - .withTimestampCombiner(TimestampCombiner.END_OF_WINDOW) - .withAllowedLateness(Duration.ZERO) - .withEnvironmentId(environmentId); - components.registerWindowingStrategy(expected); - String collectionId = - components.registerPCollection( - PCollection.createPrimitiveOutputInternal( - Pipeline.create(), expected, PCollection.IsBounded.BOUNDED, VoidCoder.of()) - .setName("name")); - - WindowingStrategy actual = - WindowUtils.getWindowStrategy(collectionId, components.toComponents()); - - assertEquals(expected, actual); - } - - @Test - public void testInstantiateWindowedCoder() throws IOException { - Coder> expectedValueCoder = - KvCoder.of(VarLongCoder.of(), StringUtf8Coder.of()); - SdkComponents components = SdkComponents.create(); - components.registerEnvironment(Environments.createDockerEnvironment("java")); - String collectionId = - components.registerPCollection( - PCollection.createPrimitiveOutputInternal( - Pipeline.create(), - WindowingStrategy.globalDefault(), - PCollection.IsBounded.BOUNDED, - expectedValueCoder) - .setName("name")); - - assertEquals( - expectedValueCoder, - WindowUtils.instantiateWindowedCoder(collectionId, components.toComponents()) - .getValueCoder()); - } -} diff --git a/runners/samza/src/test/resources/ExpectedDag.json b/runners/samza/src/test/resources/ExpectedDag.json deleted file mode 100644 index c61b80134d8a..000000000000 --- a/runners/samza/src/test/resources/ExpectedDag.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "RootNode": [ - { - "fullName": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues", - "enclosingNode": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values", - "enclosingNode": "Create.TimestampedValues", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)", - "enclosingNode": "Create.TimestampedValues/Create.Values", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)" - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map" - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow/Window.Assign", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)" - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/GroupByKey", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle" - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)" - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map" - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements" - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/ParDo(ConvertTimestamps)", - "enclosingNode": "Create.TimestampedValues", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)", - "enclosingNode": "Create.TimestampedValues/ParDo(ConvertTimestamps)" - } - ] - } - ] - }, - { - "fullName": "Window.Into()", - "enclosingNode": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Window.Into()/Window.Assign", - "enclosingNode": "Window.Into()" - } - ] - }, - { - "fullName": "Combine.perKey(SumInteger)", - "enclosingNode": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Combine.perKey(SumInteger)/GroupByKey", - "enclosingNode": "Combine.perKey(SumInteger)" - }, - { - "fullName": "Combine.perKey(SumInteger)/Combine.GroupedValues", - "enclosingNode": "Combine.perKey(SumInteger)", - "ChildNodes": [ - { - "fullName": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)", - "enclosingNode": "Combine.perKey(SumInteger)/Combine.GroupedValues", - "ChildNodes": [ - { - "fullName": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "enclosingNode": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)" - } - ] - } - ] - } - ] - } - ] - } - ], - "graphLinks": [ - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow/Window.Assign" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow/Window.Assign", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/GroupByKey" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/GroupByKey", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess", - "to": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)" - }, - { - "from": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)", - "to": "Window.Into()/Window.Assign" - }, - { - "from": "Window.Into()/Window.Assign", - "to": "Combine.perKey(SumInteger)/GroupByKey" - }, - { - "from": "Combine.perKey(SumInteger)/GroupByKey", - "to": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)/ParMultiDo(Anonymous)" - } - ], - "transformIOInfo": [ - { - "transformName": "Window.Into()/Window.Assign", - "inputs": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps).output", - "outputs": "Window.Into()/Window.Assign.out" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse.out", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper).output" - }, - { - "transformName": "Combine.perKey(SumInteger)", - "inputs": "Window.Into()/Window.Assign.out", - "outputs": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper).output", - "outputs": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse", - "inputs": "", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse.out" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows).output" - } - ] -} \ No newline at end of file diff --git a/runners/samza/src/test/resources/log4j-test.properties b/runners/samza/src/test/resources/log4j-test.properties deleted file mode 100644 index 8a95279413c2..000000000000 --- a/runners/samza/src/test/resources/log4j-test.properties +++ /dev/null @@ -1,26 +0,0 @@ -################################################################################ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -################################################################################ - -# Set root logger level to ERROR. -# set manually to INFO for debugging purposes. -log4j.rootLogger=INFO,testlogger - -# ConsoleAppender. -log4j.appender.testlogger=org.apache.log4j.ConsoleAppender -log4j.appender.testlogger.layout=org.apache.log4j.PatternLayout -log4j.appender.testlogger.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n diff --git a/sdks/go/examples/large_wordcount/large_wordcount.go b/sdks/go/examples/large_wordcount/large_wordcount.go index eb9cf3010e75..63e6b3b88efb 100644 --- a/sdks/go/examples/large_wordcount/large_wordcount.go +++ b/sdks/go/examples/large_wordcount/large_wordcount.go @@ -74,7 +74,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dot" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/prism" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal" ) diff --git a/sdks/go/pkg/beam/runners/samza/samza.go b/sdks/go/pkg/beam/runners/samza/samza.go deleted file mode 100644 index 01a7c5233af2..000000000000 --- a/sdks/go/pkg/beam/runners/samza/samza.go +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one or more -// contributor license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright ownership. -// The ASF licenses this file to You under the Apache License, Version 2.0 -// (the "License"); you may not use this file except in compliance with -// the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package samza contains the Samza runner. -package samza - -import ( - "context" - - "github.com/apache/beam/sdks/v2/go/pkg/beam" - "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal" -) - -func init() { - beam.RegisterRunner("samza", Execute) - beam.RegisterRunner("SamzaRunner", Execute) -} - -// Execute runs the given pipeline on Samza. Convenience wrapper over the -// universal runner. -func Execute(ctx context.Context, p *beam.Pipeline) (beam.PipelineResult, error) { - return universal.Execute(ctx, p) -} diff --git a/sdks/go/pkg/beam/x/beamx/run.go b/sdks/go/pkg/beam/x/beamx/run.go index 0be42561b658..ff3583917b7a 100644 --- a/sdks/go/pkg/beam/x/beamx/run.go +++ b/sdks/go/pkg/beam/x/beamx/run.go @@ -33,7 +33,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dot" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/prism" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal" ) diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index 9302f25f8fce..ddaaa136b432 100644 --- a/sdks/go/test/build.gradle +++ b/sdks/go/test/build.gradle @@ -117,35 +117,6 @@ task flinkValidatesRunner { } } -// ValidatesRunner tests for Samza. Runs tests in the integration directory -// with Samza to validate that the runner behaves as expected. -task samzaValidatesRunner { - dependsOn ":sdks:go:test:goBuild" - dependsOn ":sdks:go:container:docker" - dependsOn ":sdks:java:container:${project.ext.currentJavaVersion}:docker" - dependsOn ":runners:samza:job-server:shadowJar" - dependsOn ":sdks:java:testing:expansion-service:buildTestExpansionServiceJar" - - doLast { - def pipelineOptions = [ // Pipeline options piped directly to Go SDK flags. - "--expansion_jar=test:${project(":sdks:java:testing:expansion-service").buildTestExpansionServiceJar.archivePath}", - ] - def options = [ - "--runner samza", - "--samza_job_server_jar ${project(":runners:samza:job-server").shadowJar.archivePath}", - "--pipeline_opts \"${pipelineOptions.join(' ')}\"", - "--prebuild_go_docker_tag ${project(":sdks:go:container").containerImageTags().first()}", - ] - exec { - if (fork_java_home) { - environment "JAVA_HOME_JOB_SERVER", fork_java_home - } - executable "sh" - args "-c", "./run_validatesrunner_tests.sh ${options.join(' ')}" - } - } -} - // ValidatesRunner tests for Spark. Runs tests in the integration directory // with Spark to validate that the runner behaves as expected. task sparkValidatesRunner { diff --git a/sdks/go/test/integration/expansions_test.go b/sdks/go/test/integration/expansions_test.go index 3afa2470157c..6e47f5d303b0 100644 --- a/sdks/go/test/integration/expansions_test.go +++ b/sdks/go/test/integration/expansions_test.go @@ -22,7 +22,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/test/integration/internal/jars" "github.com/google/go-cmp/cmp" diff --git a/sdks/go/test/integration/integration.go b/sdks/go/test/integration/integration.go index 9d04f08de4fb..b23547bf4fa1 100644 --- a/sdks/go/test/integration/integration.go +++ b/sdks/go/test/integration/integration.go @@ -206,53 +206,6 @@ var flinkFilters = []string{ "TestParDoBundleFinalizer.*", } -var samzaFilters = []string{ - // TODO(https://github.com/apache/beam/issues/20987): Samza tests invalid encoding. - "TestReshuffle", - "TestReshuffleKV", - // The Samza runner does not support the TestStream primitive - "TestTestStream.*", - // The trigger and pane tests uses TestStream - "TestTrigger.*", - "TestPanes", - // TODO(https://github.com/apache/beam/issues/21244): Samza doesn't yet support post job metrics, used by WordCount - "TestWordCount.*", - // TODO(BEAM-13215): GCP IOs currently do not work in non-Dataflow portable runners. - "TestBigQueryIO.*", - "TestBigtableIO.*", - "TestSpannerIO.*", - // The Samza runner does not support self-checkpointing - "TestCheckpointing", - // The samza runner does not support pipeline drain for SDF. - "TestDrain", - // FhirIO currently only supports Dataflow runner - "TestFhirIO.*", - // OOMs currently only lead to heap dumps on Dataflow runner - "TestOomParDo", - // The samza runner does not support user state. - "TestValueState", - "TestValueStateWindowed", - "TestValueStateClear", - "TestBagState", - "TestBagStateClear", - "TestCombiningState", - "TestMapState", - "TestMapStateClear", - "TestSetState", - "TestSetStateClear", - // TODO(https://github.com/apache/beam/issues/26126): Java runner issue (AcitveBundle has no regsitered handler) - "TestDebeziumIO_BasicRead", - - "TestOrderedListState", - - // Samza does not support state. - "TestTimers.*", - "TestBagStateBlindWrite", - - // no support for BundleFinalizer - "TestParDoBundleFinalizer.*", -} - var sparkFilters = []string{ // TODO(BEAM-11498): XLang tests broken with Spark runner. "TestXLang.*", @@ -375,8 +328,6 @@ func CheckFilters(t *testing.T) { filters = portableFilters case "flink", "FlinkRunner": filters = flinkFilters - case "samza", "SamzaRunner": - filters = samzaFilters case "spark", "SparkRunner": filters = sparkFilters case "dataflow", "DataflowRunner": diff --git a/sdks/go/test/integration/io/mongodbio/mongodbio_test.go b/sdks/go/test/integration/io/mongodbio/mongodbio_test.go index b8885e7c728d..481729c9eb98 100644 --- a/sdks/go/test/integration/io/mongodbio/mongodbio_test.go +++ b/sdks/go/test/integration/io/mongodbio/mongodbio_test.go @@ -26,7 +26,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/io/mongodbio" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" diff --git a/sdks/go/test/integration/io/xlang/debezium/debezium_test.go b/sdks/go/test/integration/io/xlang/debezium/debezium_test.go index a4850d4a3a33..8ccb64cae209 100644 --- a/sdks/go/test/integration/io/xlang/debezium/debezium_test.go +++ b/sdks/go/test/integration/io/xlang/debezium/debezium_test.go @@ -25,7 +25,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/io/xlang/debeziumio" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go index 0eddc3e788d2..93d9f4c07ffc 100644 --- a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go +++ b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go @@ -26,7 +26,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/io/xlang/kafka/kafka_test.go b/sdks/go/test/integration/io/xlang/kafka/kafka_test.go index e1cdc2e935db..2a682ea5cd2c 100644 --- a/sdks/go/test/integration/io/xlang/kafka/kafka_test.go +++ b/sdks/go/test/integration/io/xlang/kafka/kafka_test.go @@ -24,7 +24,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/primitives/primitives_test.go b/sdks/go/test/integration/primitives/primitives_test.go index ef8a265b8bfa..ceaac4532f3c 100644 --- a/sdks/go/test/integration/primitives/primitives_test.go +++ b/sdks/go/test/integration/primitives/primitives_test.go @@ -20,7 +20,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" ) diff --git a/sdks/go/test/integration/synthetic/synthetic_test.go b/sdks/go/test/integration/synthetic/synthetic_test.go index 3161012975bd..f55fc7d7a302 100644 --- a/sdks/go/test/integration/synthetic/synthetic_test.go +++ b/sdks/go/test/integration/synthetic/synthetic_test.go @@ -22,7 +22,7 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/io/synthetic" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" + _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" diff --git a/sdks/go/test/integration/wordcount/wordcount_test.go b/sdks/go/test/integration/wordcount/wordcount_test.go index 09c6683cd14e..3c18a799f752 100644 --- a/sdks/go/test/integration/wordcount/wordcount_test.go +++ b/sdks/go/test/integration/wordcount/wordcount_test.go @@ -23,7 +23,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/core/metrics" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/xlang/xlang_test.go b/sdks/go/test/integration/xlang/xlang_test.go index f1473f199057..091967a9d03c 100644 --- a/sdks/go/test/integration/xlang/xlang_test.go +++ b/sdks/go/test/integration/xlang/xlang_test.go @@ -27,7 +27,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" diff --git a/sdks/go/test/regression/lperror_test.go b/sdks/go/test/regression/lperror_test.go index 7e99e73c9f80..0dd251980b3d 100644 --- a/sdks/go/test/regression/lperror_test.go +++ b/sdks/go/test/regression/lperror_test.go @@ -26,7 +26,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" ) diff --git a/sdks/go/test/regression/pardo_test.go b/sdks/go/test/regression/pardo_test.go index fc6d240fbf5a..e193739de452 100644 --- a/sdks/go/test/regression/pardo_test.go +++ b/sdks/go/test/regression/pardo_test.go @@ -23,7 +23,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" ) diff --git a/sdks/go/test/run_validatesrunner_tests.sh b/sdks/go/test/run_validatesrunner_tests.sh index daae322dec85..9ea89fd22cfc 100755 --- a/sdks/go/test/run_validatesrunner_tests.sh +++ b/sdks/go/test/run_validatesrunner_tests.sh @@ -36,7 +36,7 @@ # --timeout -> Timeout for the go test command, on a per-package level. # --simultaneous -> Number of simultaneous packages to test. # Controls the -p flag for the go test command. -# Not used for Flink, Spark, or Samza runners. Defaults to 3 otherwise. +# Not used for Flink or Spark runners. Defaults to 3 otherwise. # --endpoint -> An endpoint for an existing job server outside the script. # If present, job server jar flags are ignored. # --test_expansion_jar -> Filepath to jar for an expansion service, for @@ -170,11 +170,6 @@ case $key in shift # past argument shift # past value ;; - --samza_job_server_jar) - SAMZA_JOB_SERVER_JAR="$2" - shift # past argument - shift # past value - ;; --spark_job_server_jar) SPARK_JOB_SERVER_JAR="$2" shift # past argument @@ -272,7 +267,7 @@ else fi # Set up environment based on runner. -if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "samza" || "$RUNNER" == "portable" || "$RUNNER" == "prism" ]]; then +if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "portable" || "$RUNNER" == "prism" ]]; then if [[ -z "$ENDPOINT" ]]; then JOB_PORT=$(python3 -c "$SOCKET_SCRIPT") ENDPOINT="localhost:$JOB_PORT" @@ -289,12 +284,6 @@ if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "samza" || "$ --job-port $JOB_PORT \ --expansion-port 0 \ --artifact-port 0 & - elif [[ "$RUNNER" == "samza" ]]; then - "$JAVA_CMD" \ - -jar $SAMZA_JOB_SERVER_JAR \ - --job-port $JOB_PORT \ - --expansion-port 0 \ - --artifact-port 0 & elif [[ "$RUNNER" == "spark" ]]; then "$JAVA_CMD" \ -jar $SPARK_JOB_SERVER_JAR \ @@ -354,7 +343,7 @@ if [[ "$RUNNER" != "direct" ]]; then fi # Disable parallelism on runners that don't support it. -if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "samza" ]]; then +if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" ]]; then SIMULTANEOUS=1 fi diff --git a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml index afd80d1f4426..e734e98e7d40 100644 --- a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml @@ -261,17 +261,6 @@ - - samza-runner - - - org.apache.beam - beam-runners-samza - ${beam.version} - runtime - - - twister2-runner diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml index 320aacd8d4bb..d93a6b09284f 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml @@ -253,16 +253,6 @@ - - samza-runner - - - org.apache.beam - beam-runners-samza - runtime - - - twister2-runner diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py index 0197733e9115..8bae82f0aaaf 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py @@ -836,7 +836,6 @@ def test_pardo_et_timer_with_no_firing(self): 'FnApiRunnerTestWithMultiWorkers', 'FnApiRunnerTestWithBundleRepeat', 'FnApiRunnerTestWithBundleRepeatAndMultiWorkers', - 'SamzaRunnerTest', 'SparkRunnerTest'}: raise unittest.SkipTest("https://github.com/apache/beam/issues/35168") @@ -853,7 +852,6 @@ def test_pardo_et_timer_with_no_reset(self): 'FnApiRunnerTestWithMultiWorkers', 'FnApiRunnerTestWithBundleRepeat', 'FnApiRunnerTestWithBundleRepeatAndMultiWorkers', - 'SamzaRunnerTest', 'SparkRunnerTest'}: raise unittest.SkipTest("https://github.com/apache/beam/issues/35168") @@ -869,7 +867,6 @@ def test_pardo_et_timer_with_no_reset_and_no_clear(self): 'FnApiRunnerTestWithMultiWorkers', 'FnApiRunnerTestWithBundleRepeat', 'FnApiRunnerTestWithBundleRepeatAndMultiWorkers', - 'SamzaRunnerTest', 'SparkRunnerTest'}: raise unittest.SkipTest("https://github.com/apache/beam/issues/35168") # The timer will fire at T + 10. After the timer is set, it is never diff --git a/sdks/python/apache_beam/runners/portability/portable_runner_test.py b/sdks/python/apache_beam/runners/portability/portable_runner_test.py index 2fd63b822e96..0f44afb2f123 100644 --- a/sdks/python/apache_beam/runners/portability/portable_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/portable_runner_test.py @@ -230,7 +230,6 @@ def test_pardo_et_timer_with_no_firing(self): 'PortableRunnerTestWithExternalEnv', 'PortableRunnerTestWithLocalDocker', 'PortableRunnerOptimizedWithoutFusion', - 'SamzaRunnerTest', 'SparkRunnerTest' }: raise unittest.SkipTest("https://github.com/apache/beam/issues/35168") @@ -244,7 +243,6 @@ def test_pardo_et_timer_with_no_reset(self): 'PortableRunnerTestWithExternalEnv', 'PortableRunnerTestWithLocalDocker', 'PortableRunnerOptimizedWithoutFusion', - 'SamzaRunnerTest', 'SparkRunnerTest' }: raise unittest.SkipTest("https://github.com/apache/beam/issues/35168") diff --git a/sdks/python/apache_beam/runners/portability/samza_runner_test.py b/sdks/python/apache_beam/runners/portability/samza_runner_test.py deleted file mode 100644 index cc8d947f054e..000000000000 --- a/sdks/python/apache_beam/runners/portability/samza_runner_test.py +++ /dev/null @@ -1,200 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# pytype: skip-file - -# Run as -# -# pytest samza_runner_test.py[::TestClass::test_case] \ -# --test-pipeline-options="--environment_type=LOOPBACK" -import argparse -import logging -import shlex -import unittest -from shutil import rmtree -from tempfile import mkdtemp - -import pytest - -from apache_beam.options.pipeline_options import PortableOptions -from apache_beam.runners.portability import job_server -from apache_beam.runners.portability import portable_runner -from apache_beam.runners.portability import portable_runner_test -from apache_beam.utils import subprocess_server - -_LOGGER = logging.getLogger(__name__) - - -class SamzaRunnerTest(portable_runner_test.PortableRunnerTest): - _use_grpc = True - _use_subprocesses = True - - expansion_port = None - samza_job_server_jar = None - - @pytest.fixture(autouse=True) - def parse_options(self, request): - if not request.config.option.test_pipeline_options: - raise unittest.SkipTest( - 'Skipping because --test-pipeline-options is not specified.') - test_pipeline_options = request.config.option.test_pipeline_options - parser = argparse.ArgumentParser(add_help=True) - parser.add_argument( - '--samza_job_server_jar', - help='Job server jar to submit jobs.', - action='store') - parser.add_argument( - '--environment_type', - default='LOOPBACK', - choices=['DOCKER', 'PROCESS', 'LOOPBACK'], - help='Set the environment type for running user code. DOCKER runs ' - 'user code in a container. PROCESS runs user code in ' - 'automatically started processes. LOOPBACK runs user code on ' - 'the same process that originally submitted the job.') - parser.add_argument( - '--environment_option', - '--environment_options', - dest='environment_options', - action='append', - default=None, - help=( - 'Environment configuration for running the user code. ' - 'Recognized options depend on --environment_type.\n ' - 'For DOCKER: docker_container_image (optional)\n ' - 'For PROCESS: process_command (required), process_variables ' - '(optional, comma-separated)\n ' - 'For EXTERNAL: external_service_address (required)')) - known_args, unknown_args = parser.parse_known_args( - shlex.split(test_pipeline_options)) - if unknown_args: - _LOGGER.warning('Discarding unrecognized arguments %s' % unknown_args) - self.set_samza_job_server_jar( - known_args.samza_job_server_jar or - job_server.JavaJarJobServer.path_to_beam_jar( - ':runners:samza:job-server:shadowJar')) - self.environment_type = known_args.environment_type - self.environment_options = known_args.environment_options\ - - @classmethod - def _subprocess_command(cls, job_port, expansion_port): - # will be cleaned up at the end of this method, and recreated and used by - # the job server - tmp_dir = mkdtemp(prefix='samzatest') - - cls.expansion_port = expansion_port - - try: - return [ - subprocess_server.JavaHelper.get_java(), - '-jar', - cls.samza_job_server_jar, - '--artifacts-dir', - tmp_dir, - '--job-port', - str(job_port), - '--artifact-port', - '0', - '--expansion-port', - str(expansion_port), - ] - finally: - rmtree(tmp_dir) - - @classmethod - def set_samza_job_server_jar(cls, samza_job_server_jar): - cls.samza_job_server_jar = samza_job_server_jar - - @classmethod - def get_runner(cls): - return portable_runner.PortableRunner() - - @classmethod - def get_expansion_service(cls): - # TODO Move expansion address resides into PipelineOptions - return 'localhost:%s' % cls.expansion_port - - def create_options(self): - options = super().create_options() - options.view_as(PortableOptions).environment_type = self.environment_type - options.view_as( - PortableOptions).environment_options = self.environment_options - - return options - - def test_metrics(self): - # Skip until Samza portable runner supports distribution metrics. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21043") - - def test_flattened_side_input(self): - # Blocked on support for transcoding - # https://github.com/apache/beam/issues/20984 - super().test_flattened_side_input(with_transcoding=False) - - def test_flatten_and_gbk(self): - # Blocked on support for transcoding - # https://github.com/apache/beam/issues/20984 - # Also blocked on support of flatten and groupby sharing the same input - # https://github.com/apache/beam/issues/34647 - raise unittest.SkipTest("https://github.com/apache/beam/issues/34647") - - def test_pack_combiners(self): - # Stages produced by translations.pack_combiners are fused - # by translations.greedily_fuse, which prevent the stages - # from being detecting using counters by the test. - self._test_pack_combiners(assert_using_counter_names=False) - - def test_pardo_timers(self): - # Skip until Samza portable runner supports clearing timer. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21059") - - def test_register_finalizations(self): - # Skip until Samza runner supports bundle finalization. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21044") - - def test_callbacks_with_exception(self): - # Skip until Samza runner supports bundle finalization. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21044") - - def test_sdf_with_dofn_as_watermark_estimator(self): - # Skip until Samza runner supports SDF and self-checkpoint. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21045") - - def test_sdf_with_sdf_initiated_checkpointing(self): - # Skip until Samza runner supports SDF. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21045") - - def test_sdf_with_watermark_tracking(self): - # Skip until Samza runner supports SDF. - raise unittest.SkipTest("https://github.com/apache/beam/issues/21045") - - def test_custom_merging_window(self): - # Skip until Samza runner supports merging window fns - raise unittest.SkipTest("https://github.com/apache/beam/issues/21049") - - def test_custom_window_type(self): - raise unittest.SkipTest("https://github.com/apache/beam/issues/21049") - - def test_reshuffle_after_custom_window(self): - raise unittest.SkipTest("https://github.com/apache/beam/issues/34831") - - def test_sliding_windows(self): - raise unittest.SkipTest("https://github.com/apache/beam/issues/35429") - - -if __name__ == '__main__': - # Run the tests. - logging.getLogger().setLevel(logging.INFO) - unittest.main() diff --git a/sdks/python/test-suites/gradle.properties b/sdks/python/test-suites/gradle.properties index 6cadc5e57b44..b5cb4cdae43a 100644 --- a/sdks/python/test-suites/gradle.properties +++ b/sdks/python/test-suites/gradle.properties @@ -41,9 +41,6 @@ flink_validates_runner_precommit_py_versions=3.14 flink_validates_runner_postcommit_py_versions=3.10,3.14 flink_examples_postcommit_py_versions=3.10,3.14 -# samza runner test-suites -samza_validates_runner_postcommit_py_versions=3.10,3.14 - # spark runner test-suites spark_examples_postcommit_py_versions=3.10,3.14 diff --git a/sdks/python/test-suites/portable/build.gradle b/sdks/python/test-suites/portable/build.gradle index 41cd88acfb6a..6e28f114e02b 100644 --- a/sdks/python/test-suites/portable/build.gradle +++ b/sdks/python/test-suites/portable/build.gradle @@ -25,12 +25,6 @@ tasks.register("flinkValidatesRunner") { } } -tasks.register("samzaValidatesRunner") { - getVersionsAsList('samza_validates_runner_postcommit_py_versions').each { - dependsOn.add(":sdks:python:test-suites:portable:py${getVersionSuffix(it)}:samzaValidatesRunner") - } -} - tasks.register("prismValidatesRunner") { getVersionsAsList('prism_validates_runner_postcommit_py_versions').each { dependsOn.add(":sdks:python:test-suites:portable:py${getVersionSuffix(it)}:prismValidatesRunner") diff --git a/sdks/python/test-suites/portable/common.gradle b/sdks/python/test-suites/portable/common.gradle index 8c5bd6341065..17bf9989f28b 100644 --- a/sdks/python/test-suites/portable/common.gradle +++ b/sdks/python/test-suites/portable/common.gradle @@ -146,33 +146,6 @@ tasks.register("portableLocalRunnerTestWithRequirementsFile") { } } -def createSamzaRunnerTestTask(String workerType) { - def taskName = "samzaCompatibilityMatrix${workerType}" - def jobServerJar = "${rootDir}/runners/samza/job-server/build/libs/beam-runners-samza-job-server-${version}.jar" - def options = "--samza_job_server_jar=${jobServerJar} --environment_type=${workerType}" - if (workerType == 'PROCESS') { - options += " --environment_options=process_command=${buildDir.absolutePath}/sdk_worker.sh" - } - def task = toxTask(taskName, 'samza-runner-test', options) - task.configure { - dependsOn ":runners:samza:job-server:shadowJar" - if (workerType == 'DOCKER') { - dependsOn pythonContainerTask - } else if (workerType == 'PROCESS') { - dependsOn createProcessWorker - } - } - return task -} - -createSamzaRunnerTestTask('DOCKER') -createSamzaRunnerTestTask('PROCESS') -createSamzaRunnerTestTask('LOOPBACK') - -task samzaValidatesRunner() { - dependsOn 'samzaCompatibilityMatrixLOOPBACK' -} - def createSparkRunnerTestTask(String workerType) { def taskName = "sparkCompatibilityMatrix${workerType}" // `project(':runners:spark:3:job-server').shadowJar.archivePath` is not resolvable until runtime, so hard-code it here. diff --git a/sdks/python/tox.ini b/sdks/python/tox.ini index 0f6183ae7715..ffc39a086efe 100644 --- a/sdks/python/tox.ini +++ b/sdks/python/tox.ini @@ -325,12 +325,6 @@ extras = test commands = bash {toxinidir}/scripts/pytest_validates_runner.sh {envname} {toxinidir}/apache_beam/runners/portability/flink_runner_test.py {posargs} -[testenv:samza-runner-test] -passenv = JAVA_HOME -extras = test -commands = - bash {toxinidir}/scripts/pytest_validates_runner.sh {envname} {toxinidir}/apache_beam/runners/portability/samza_runner_test.py {posargs} - [testenv:spark-runner-test] extras = test commands = diff --git a/settings.gradle.kts b/settings.gradle.kts index c001a1add446..4080206bb542 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,8 +150,6 @@ include(":runners:prism:java") include(":runners:spark:3") include(":runners:spark:3:job-server") include(":runners:spark:3:job-server:container") -include(":runners:samza") -include(":runners:samza:job-server") include(":sdks:go") include(":sdks:go:container") include(":sdks:go:examples") diff --git a/website/www/site/content/en/blog/capability-matrix.md b/website/www/site/content/en/blog/capability-matrix.md index a3e65734eb2b..6ca5c44f3133 100644 --- a/website/www/site/content/en/blog/capability-matrix.md +++ b/website/www/site/content/en/blog/capability-matrix.md @@ -28,7 +28,7 @@ With initial code drops complete ([Dataflow SDK and Runner](https://github.com/a -While we’d love to have a world where all runners support the full suite of semantics included in the Beam Model (formerly referred to as the [Dataflow Model](https://www.vldb.org/pvldb/vol8/p1792-Akidau.pdf)), practically speaking, there will always be certain features that some runners can’t provide. For example, a Hadoop-based runner would be inherently batch-based and may be unable to (easily) implement support for unbounded collections. However, that doesn’t prevent it from being extremely useful for a large set of uses. In other cases, the implementations provided by one runner may have slightly different semantics that those provided by another (e.g. even though the current suite of runners all support exactly-once delivery guarantees, an [Apache Samza](https://samza.apache.org/) runner, which would be a welcome addition, would currently only support at-least-once). +While we’d love to have a world where all runners support the full suite of semantics included in the Beam Model (formerly referred to as the [Dataflow Model](https://www.vldb.org/pvldb/vol8/p1792-Akidau.pdf)), practically speaking, there will always be certain features that some runners can’t provide. For example, a Hadoop-based runner would be inherently batch-based and may be unable to (easily) implement support for unbounded collections. However, that doesn’t prevent it from being extremely useful for a large set of uses. In other cases, the implementations provided by one runner may have slightly different semantics that those provided by another (e.g. even though the current suite of runners all support exactly-once delivery guarantees, an [Apache Samza](https://samza.apache.org/) runner, supported in Beam 2.73.0 and before, only supported at-least-once). To help clarify things, we’ve been working on enumerating the key features of the Beam model in a [capability matrix](/documentation/runners/capability-matrix/) for all existing runners, categorized around the four key questions addressed by the model: What / Where / When / How (if you’re not familiar with those questions, you might want to read through [Streaming 102](https://oreilly.com/ideas/the-world-beyond-batch-streaming-102) for an overview). This table will be maintained over time as the model evolves, our understanding grows, and runners are created or features added. diff --git a/website/www/site/content/en/contribute/postcommits-policies-details.md b/website/www/site/content/en/contribute/postcommits-policies-details.md index bb0f75b0ce25..55ca7b0aedd7 100644 --- a/website/www/site/content/en/contribute/postcommits-policies-details.md +++ b/website/www/site/content/en/contribute/postcommits-policies-details.md @@ -91,7 +91,7 @@ implement a new unit test that covers a problematic code branch. ## Inform the community if Beam breaks downstream projects {#inform_community} There are multiple external projects depending on Beam which contain tests that are -outside of Beam repository. For example, Dataflow, Samza runner, and IBM Streams. +outside of Beam repository. For example, Dataflow, Scio, and IBM Streams. When an external project encounters an issue caused by (a PR) in Beam and, in consequence, requests for a change in the Beam repository, diff --git a/website/www/site/content/en/documentation/runners/samza.md b/website/www/site/content/en/documentation/runners/samza.md index c36d06fee861..9dbbcffb49d4 100644 --- a/website/www/site/content/en/documentation/runners/samza.md +++ b/website/www/site/content/en/documentation/runners/samza.md @@ -19,7 +19,7 @@ limitations under the License. # Using the Apache Samza Runner -**Note** Samza runner is deprecated and the support is planned to be removed in Beam 3.0 ([Issue](https://github.com/apache/beam/issues/35448)). +**Note** Samza runner is no longer supported since Beam 2.74.0 ([Issue](https://github.com/apache/beam/issues/35448)). The Apache Samza Runner can be used to execute Beam pipelines using [Apache Samza](https://samza.apache.org/). The Samza Runner executes Beam pipeline in a Samza application and can run locally. The application can further be built into a .tgz file, and deployed to a YARN cluster or Samza standalone cluster with Zookeeper. @@ -44,7 +44,7 @@ The Samza Runner is built on Samza version greater than 1.0. org.apache.beam beam-runners-samza - {{< param release_latest >}} + 2.72.0 runtime diff --git a/website/www/site/content/en/get-started/beam-overview.md b/website/www/site/content/en/get-started/beam-overview.md index 5a8fcd3b917c..c50ebdc39b54 100644 --- a/website/www/site/content/en/get-started/beam-overview.md +++ b/website/www/site/content/en/get-started/beam-overview.md @@ -50,12 +50,15 @@ Beam currently supports the following runners: - [Direct Runner](/documentation/runners/direct) - [Apache Flink Runner](/documentation/runners/flink) Apache Flink logo - [Apache Nemo Runner](/documentation/runners/nemo) -- [Apache Samza Runner](/documentation/runners/samza) Apache Samza logo - [Apache Spark Runner](/documentation/runners/spark) Apache Spark logo - [Google Cloud Dataflow Runner](/documentation/runners/dataflow) Google Cloud Dataflow logo - [Hazelcast Jet Runner](/documentation/runners/jet) Hazelcast Jet logo - [Twister2 Runner](/documentation/runners/twister2) Twister2 logo +Runners supported in older Beam versions + +- [Apache Samza Runner](/documentation/runners/samza) Apache Samza logo until Beam 2.74.0. + **Note:** You can always execute your pipeline locally for testing and debugging purposes. ## Get Started diff --git a/website/www/site/content/en/get-started/quickstart-java.md b/website/www/site/content/en/get-started/quickstart-java.md index 3152351facf5..d911918b9d2b 100644 --- a/website/www/site/content/en/get-started/quickstart-java.md +++ b/website/www/site/content/en/get-started/quickstart-java.md @@ -185,7 +185,6 @@ To run the WordCount pipeline: * [FlinkRunner](/documentation/runners/flink) * [SparkRunner](/documentation/runners/spark) * [DataflowRunner](/documentation/runners/dataflow) - * [SamzaRunner](/documentation/runners/samza) * [NemoRunner](/documentation/runners/nemo) * [JetRunner](/documentation/runners/jet) @@ -221,10 +220,6 @@ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ --inputFile=gs://apache-beam-samples/shakespeare/* --output=gs:///counts" \ -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Dexec.args="--inputFile=sample.txt --output=/tmp/counts --runner=SamzaRunner" -Psamza-runner -{{< /runner >}} {{< runner nemo >}} mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WordCount \ --runner=NemoRunner --inputFile=`pwd`/sample.txt --output=counts @@ -262,10 +257,6 @@ mvn compile exec:java -D exec.mainClass=org.apache.beam.examples.WordCount ` --inputFile=gs://apache-beam-samples/shakespeare/* --output=gs:///counts" ` -P dataflow-runner {{< /runner >}} -{{< runner samza >}} -mvn compile exec:java -D exec.mainClass=org.apache.beam.examples.WordCount ` - -D exec.args="--inputFile=sample.txt --output=/tmp/counts --runner=SamzaRunner" -P samza-runner -{{< /runner >}} {{< runner nemo >}} mvn package -P nemo-runner -DskipTests java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WordCount ` @@ -299,9 +290,6 @@ gradle clean execute -DmainClass=org.apache.beam.examples.WordCount \ --args="--project= --inputFile=gs://apache-beam-samples/shakespeare/* \ --output=gs:///counts --runner=DataflowRunner" -Pdataflow-runner {{< /runner >}} -{{< runner samza>}} -TODO: document Samza on Gradle: https://github.com/apache/beam/issues/21500 -{{< /runner >}} {{< runner nemo>}} TODO: document Nemo on Gradle: https://github.com/apache/beam/issues/21503 {{< /runner >}} @@ -329,9 +317,6 @@ TODO: document Spark on Gradle: https://github.com/apache/beam/issues/21502 --args="--project= --inputFile=gs://apache-beam-samples/shakespeare/* \ --output=gs:///counts --runner=DataflowRunner" -Pdataflow-runner {{< /runner >}} -{{< runner samza>}} -TODO: document Samza on Gradle: https://github.com/apache/beam/issues/21500 -{{< /runner >}} {{< runner nemo>}} TODO: document Nemo on Gradle: https://github.com/apache/beam/issues/21503 {{< /runner >}} @@ -360,9 +345,6 @@ ls counts* {{< /runner >}} {{< runner dataflow >}} gsutil ls gs:///counts* - {{< /runner >}} - {{< runner samza >}} -ls /tmp/counts* {{< /runner >}} {{< runner nemo >}} ls counts* @@ -387,9 +369,6 @@ more counts* {{< /runner >}} {{< runner dataflow >}} gsutil cat gs:///counts* - {{< /runner >}} - {{< runner samza >}} -more /tmp/counts* {{< /runner >}} {{< runner nemo >}} more counts* diff --git a/website/www/site/content/en/get-started/wordcount-example.md b/website/www/site/content/en/get-started/wordcount-example.md index e91226c247ca..157019230a65 100644 --- a/website/www/site/content/en/get-started/wordcount-example.md +++ b/website/www/site/content/en/get-started/wordcount-example.md @@ -376,11 +376,6 @@ $ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Dexec.args="--inputFile=pom.xml --output=counts --runner=SamzaRunner" -Psamza-runner -{{< /runner >}} - {{< runner nemo >}} $ mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WordCount \ --runner=NemoRunner --inputFile=`pwd`/pom.xml --output=counts @@ -428,10 +423,6 @@ python -m apache_beam.examples.wordcount --input gs://dataflow-samples/shakespea --temp_location gs://YOUR_GCS_BUCKET/tmp/ {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} @@ -476,10 +467,6 @@ $ wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ --worker_harness_container_image=apache/beam_go_sdk:latest {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Go SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Go SDK. {{< /runner >}} @@ -716,11 +703,6 @@ $ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.DebuggingWordC -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.DebuggingWordCount \ - -Dexec.args="--runner=SamzaRunner --output=counts" -Psamza-runner -{{< /runner >}} - {{< runner nemo >}} $ mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.DebuggingWordCount \ --runner=NemoRunner --inputFile=`pwd`/pom.xml --output=counts @@ -762,10 +744,6 @@ python -m apache_beam.examples.wordcount_debugging --input gs://dataflow-samples --temp_location gs://YOUR_GCS_BUCKET/tmp/ {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} @@ -810,10 +788,6 @@ $ debugging_wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ --worker_harness_container_image=apache-docker-beam-snapshots-docker.bintray.io/beam/go:20180515 {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Go SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Go SDK. {{< /runner >}} @@ -1048,11 +1022,6 @@ $ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WindowedWordCo -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WindowedWordCount \ - -Dexec.args="--runner=SamzaRunner --inputFile=pom.xml --output=counts" -Psamza-runner -{{< /runner >}} - {{< runner nemo >}} $ mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WindowedWordCount \ --runner=NemoRunner --inputFile=`pwd`/pom.xml --output=counts @@ -1098,10 +1067,6 @@ python -m apache_beam.examples.windowed_wordcount --input YOUR_INPUT_FILE \ --temp_location gs://YOUR_GCS_BUCKET/tmp/ {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} @@ -1145,10 +1110,6 @@ $ windowed_wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ --worker_harness_container_image=apache-docker-beam-snapshots-docker.bintray.io/beam/go:20180515 {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Go SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Go SDK. {{< /runner >}} @@ -1410,10 +1371,6 @@ python -m apache_beam.examples.streaming_wordcount \ --streaming {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} diff --git a/website/www/site/content/en/roadmap/_index.md b/website/www/site/content/en/roadmap/_index.md index 698b41dd798f..e764898d3f84 100644 --- a/website/www/site/content/en/roadmap/_index.md +++ b/website/www/site/content/en/roadmap/_index.md @@ -36,7 +36,7 @@ Beam 3 is the planned first major version upgrade. See https://s.apache.org/beam Portability is the primary Beam vision: running pipelines authored with _any SDK_ on _any runner_. This is a cross-cutting effort across Java, Python, and Go, and every Beam runner. Portability is currently supported on the -[DataFlow](/documentation/runners/dataflow), [Flink](/documentation/runners/flink/), [Jet](/documentation/runners/jet), [Nemo](/documentation/runners/nemo), [Prism](/documentation/runners/prism/), [Samza](/documentation/runners/samza), [Spark](/documentation/runners/spark/), and [Twister2](/documentation/runners/Twister2) +[DataFlow](/documentation/runners/dataflow), [Flink](/documentation/runners/flink/), [Jet](/documentation/runners/jet), [Nemo](/documentation/runners/nemo), [Prism](/documentation/runners/prism/), [Spark](/documentation/runners/spark/), and [Twister2](/documentation/runners/Twister2) runners. See the details on the [Portability Roadmap](/roadmap/portability/) diff --git a/website/www/site/content/en/roadmap/go-sdk.md b/website/www/site/content/en/roadmap/go-sdk.md index 4e2cc4ddba11..eb16b7936f73 100644 --- a/website/www/site/content/en/roadmap/go-sdk.md +++ b/website/www/site/content/en/roadmap/go-sdk.md @@ -20,7 +20,7 @@ limitations under the License. The Go SDK is [fully released as of v2.33.0](/blog/go-sdk-release/). The Go SDK the first SDK purely on the [Beam Portability Framework](/roadmap/portability/) -and can execute pipelines on portable runners, like Flink, Spark, Samza, and Google Cloud Dataflow. +and can execute pipelines on portable runners, like Flink, Spark, and Google Cloud Dataflow. Current roadmap: * continue building up unbounded pipeline facing features, as described on the [Beam Dev Wiki](https://cwiki.apache.org/confluence/display/BEAM/Supporting+Streaming+in+the+Go+SDK). diff --git a/website/www/site/content/en/roadmap/samza-runner.md b/website/www/site/content/en/roadmap/samza-runner.md index c27430601b09..801629222c68 100644 --- a/website/www/site/content/en/roadmap/samza-runner.md +++ b/website/www/site/content/en/roadmap/samza-runner.md @@ -17,7 +17,7 @@ limitations under the License. # Samza Runner Roadmap -**Note** Samza runner is deprecated and the support is planned to be removed in Beam 3.0 ([Issue](https://github.com/apache/beam/issues/35448)). +**Note** Samza runner is no longer supported since Beam 2.74.0 ([Issue](https://github.com/apache/beam/issues/35448)). For references, here are available resources: diff --git a/website/www/site/data/capability_matrix.yaml b/website/www/site/data/capability_matrix.yaml index c1da306b9cb7..3a753262eeae 100644 --- a/website/www/site/data/capability_matrix.yaml +++ b/website/www/site/data/capability_matrix.yaml @@ -22,8 +22,6 @@ capability-matrix: name: Apache Spark (RDD/DStream based) - class: spark-dataset name: Apache Spark Structured Streaming (Dataset based) - - class: samza - name: Apache Samza - class: nemo name: Apache Nemo - class: jet @@ -68,10 +66,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: ParDo applies per-element transformations as Spark FlatMapFunction. - - class: samza - l1: "Yes" - l2: fully supported - l3: Supported with per-element transformation. - class: nemo l1: "Yes" l2: fully supported @@ -115,10 +109,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "Using Spark's groupByKey." - - class: samza - l1: "Yes" - l2: fully supported - l3: "Uses Samza's partitionBy for key grouping and Beam's logic for window aggregation and triggering." - class: nemo l1: "Yes" l2: fully supported @@ -162,10 +152,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: Some corner cases like flatten on empty collections are not yet supported. - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -209,10 +195,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "Using Spark's Aggregator and agg function" - - class: samza - l1: "Yes" - l2: fully supported - l3: Use combiner for efficient pre-aggregation. - class: nemo l1: "Yes" l2: fully supported @@ -256,10 +238,6 @@ capability-matrix: l1: "Partially" l2: supported via inlining only in batch mode l3: "" - - class: samza - l1: "Partially" - l2: supported via inlining - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -303,10 +281,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "Using Spark's broadcast variables." - - class: samza - l1: "Yes" - l2: fully supported - l3: Uses Samza's broadcast operator to distribute the side inputs. - class: nemo l1: "Yes" l2: fully supported @@ -350,10 +324,6 @@ capability-matrix: l1: "Partially" l2: bounded source only l3: "Using Spark's DatasourceV2 API in microbatch mode (Continuous streaming mode is tagged experimental in spark and does not support aggregation)." - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -397,10 +367,6 @@ capability-matrix: l1: "Partially" l2: All metric types are supported in batch mode. l3: Only attempted values are supported. No committed values for metrics. - - class: samza - l1: "Partially" - l2: Counter and Gauge are supported. - l3: Only attempted values are supported. No committed values for metrics. - class: nemo l1: "No" l2: not implemented diff --git a/website/www/site/layouts/partials/section-menu/en/runners.html b/website/www/site/layouts/partials/section-menu/en/runners.html index 58576bb5f963..337debf3ecec 100644 --- a/website/www/site/layouts/partials/section-menu/en/runners.html +++ b/website/www/site/layouts/partials/section-menu/en/runners.html @@ -16,7 +16,6 @@

  • Prism Runner
  • Apache Flink
  • Apache Nemo
  • -
  • Apache Samza
  • Apache Spark
  • Google Cloud Dataflow
  • Hazelcast Jet
  • From f8e2b8a3bb95e4b333eaa8db1065ebee24d50e7d Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 29 Apr 2026 11:29:33 -0400 Subject: [PATCH 020/490] Revert "Add DiskProvisionedIops/ThroughputMibps options and update Go client libraries (#37377)" (#38321) This reverts commit 7910b1b3793f9f114308f868ad9f67df6a1b86b5. --- CHANGES.md | 1 - .../beam/gradle/BeamModulePlugin.groovy | 2 +- .../dataflow/DataflowPipelineTranslator.java | 7 - .../DataflowPipelineWorkerPoolOptions.java | 14 - .../options/DataflowPipelineOptionsTest.java | 9 - sdks/go.mod | 28 +- sdks/go.sum | 901 +++++++++++++++++- sdks/go/container/boot.go | 14 +- sdks/go/container/boot_test.go | 14 +- sdks/go/pkg/beam/io/fhirio/common.go | 7 +- sdks/go/pkg/beam/runners/dataflow/dataflow.go | 174 ++-- .../beam/runners/dataflow/dataflow_test.go | 22 - .../beam/runners/dataflow/dataflowlib/job.go | 71 +- .../runners/dataflow/dataflowlib/job_test.go | 36 - sdks/go/test/integration/expansions.go | 8 +- .../apache_beam/options/pipeline_options.py | 18 - .../options/pipeline_options_test.py | 6 - .../runners/dataflow/internal/apiclient.py | 5 - .../dataflow/internal/apiclient_test.py | 17 - .../clients/dataflow/dataflow_v1b3_client.py | 1 + .../dataflow/dataflow_v1b3_messages.py | 101 +- 21 files changed, 1063 insertions(+), 393 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 28aa22f66811..e8e11e830d14 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,7 +69,6 @@ ## New Features / Improvements -* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Python/Go) ([#37377](https://github.com/apache/beam/issues/37377)). * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to encode finished bitset. SentinelBitSetCoder and BitSetCoder are state diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index fe9f1f750862..005a8b587804 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -740,7 +740,7 @@ class BeamModulePlugin implements Plugin { google_api_common : "com.google.api:api-common", // google_cloud_platform_libraries_bom sets version google_api_services_bigquery : "com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0", // [bomupgrader] sets version google_api_services_cloudresourcemanager : "com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0", // [bomupgrader] sets version - google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260405-$google_clients_version", + google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260118-$google_clients_version", google_api_services_healthcare : "com.google.apis:google-api-services-healthcare:v1-rev20240130-$google_clients_version", google_api_services_pubsub : "com.google.apis:google-api-services-pubsub:v1-rev20220904-$google_clients_version", google_api_services_storage : "com.google.apis:google-api-services-storage:v1-rev20260204-2.0.0", // [bomupgrader] sets version diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index 4016f31a5475..c57b5e3b1a0e 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -489,13 +489,6 @@ public Job translate(List packages) { if (options.getDiskSizeGb() > 0) { workerPool.setDiskSizeGb(options.getDiskSizeGb()); } - if (options.getDiskProvisionedIops() != null && options.getDiskProvisionedIops() > 0) { - workerPool.setDiskProvisionedIops(options.getDiskProvisionedIops()); - } - if (options.getDiskProvisionedThroughputMibps() != null - && options.getDiskProvisionedThroughputMibps() > 0) { - workerPool.setDiskProvisionedThroughputMibps(options.getDiskProvisionedThroughputMibps()); - } AutoscalingSettings settings = new AutoscalingSettings(); if (options.getAutoscalingAlgorithm() != null) { settings.setAlgorithm(options.getAutoscalingAlgorithm().getAlgorithm()); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java index 3fcf69f4cacf..fd4af6d5e043 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java @@ -193,20 +193,6 @@ public String getAlgorithm() { void setWorkerDiskType(String value); - /** Provisioned IOPS for the worker disk. */ - @Description("Provisioned IOPS for the worker disk.") - @Nullable - Long getDiskProvisionedIops(); - - void setDiskProvisionedIops(Long value); - - /** Provisioned throughput in MiB/s for the worker disk. */ - @Description("Provisioned throughput in MiB/s for the worker disk.") - @Nullable - Long getDiskProvisionedThroughputMibps(); - - void setDiskProvisionedThroughputMibps(Long value); - /** * Specifies whether worker pools should be started with public IP addresses. * diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java index faa19eefddc5..b381396f2bcc 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java @@ -322,13 +322,4 @@ public void destroy() { TimeoutException.class, () -> DefaultGcpRegionFactory.getRegionFromGcloudCli(1L)); } } - - @Test - public void testDiskProvisionedIopsAndThroughput() { - DataflowPipelineOptions options = PipelineOptionsFactory.as(DataflowPipelineOptions.class); - options.setDiskProvisionedIops(1000L); - options.setDiskProvisionedThroughputMibps(100L); - assertEquals(Long.valueOf(1000), options.getDiskProvisionedIops()); - assertEquals(Long.valueOf(100), options.getDiskProvisionedThroughputMibps()); - } } diff --git a/sdks/go.mod b/sdks/go.mod index a73c8f3d44f2..37701285734e 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -25,12 +25,12 @@ go 1.26.0 toolchain go1.26.2 require ( - cloud.google.com/go/bigquery v1.74.0 - cloud.google.com/go/bigtable v1.42.0 - cloud.google.com/go/datastore v1.22.0 + cloud.google.com/go/bigquery v1.72.0 + cloud.google.com/go/bigtable v1.41.0 + cloud.google.com/go/datastore v1.21.0 cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.1 - cloud.google.com/go/spanner v1.88.0 + cloud.google.com/go/spanner v1.87.0 cloud.google.com/go/storage v1.59.2 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.7 @@ -56,12 +56,12 @@ require ( github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 golang.org/x/net v0.52.0 - golang.org/x/oauth2 v0.36.0 + golang.org/x/oauth2 v0.35.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 - google.golang.org/api v0.276.0 - google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 + google.golang.org/api v0.257.0 + google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 @@ -77,13 +77,13 @@ require ( require ( cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/pubsub/v2 v2.0.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect - github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect + github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect @@ -124,8 +124,8 @@ require ( go.einride.tech/aip v0.73.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect @@ -141,7 +141,7 @@ require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.8.0 // indirect + cloud.google.com/go/longrunning v0.7.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect @@ -175,8 +175,8 @@ require ( github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.21.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 074c3122592a..59d42c98adaf 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -5,6 +5,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -18,6 +19,7 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -32,52 +34,421 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= -cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.74.0 h1:Q6bAMv+eyvufOpIrfrYxhM46qq1D3ZQTdgUDQqKS+n8= -cloud.google.com/go/bigquery v1.74.0/go.mod h1:iViO7Cx3A/cRKcHNRsHB3yqGAMInFBswrE9Pxazsc90= -cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= -cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/bigquery v1.72.0 h1:D/yLju+3Ens2IXx7ou1DJ62juBm+/coBInn4VVOg5Cw= +cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= +cloud.google.com/go/bigtable v1.41.0 h1:99KOWShm/MUyuIbXBeVscdWJFV7GdgiYwFUrB5Iu4BI= +cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.22.0 h1:FOyx2Ag6ibD2wFkz9S8EiNrmBugia8pQOfpyJxi2yqA= -cloud.google.com/go/datastore v1.22.0/go.mod h1:aopSX+Whx0lHspWWBj+AjWt68/zjYsPfDe3LjWtqZg8= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastore v1.21.0 h1:dUrYq47ysCA4nM7u8kRT0WnbfXc6TzX49cP3TCwIiA0= +cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= -cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= -cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= -cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= +cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI= cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -85,26 +456,196 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0= cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= -cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= -cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= +cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw= cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= -cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= -cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= @@ -114,6 +655,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= @@ -161,8 +703,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= @@ -171,21 +713,29 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apache/arrow/go/arrow v0.0.0-20200730104253-651201b0f516/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= @@ -281,14 +831,17 @@ github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqx github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc= @@ -301,10 +854,15 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -324,6 +882,7 @@ github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GK github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -344,6 +903,7 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -357,6 +917,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= @@ -364,6 +927,9 @@ github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9O github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -382,6 +948,7 @@ github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -395,6 +962,7 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -404,6 +972,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -417,6 +987,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -433,6 +1004,8 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -468,6 +1041,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -481,6 +1055,7 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -497,6 +1072,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -524,6 +1100,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -545,15 +1122,28 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= -github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -561,6 +1151,8 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= @@ -568,6 +1160,7 @@ github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO0 github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -634,7 +1227,9 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -644,13 +1239,16 @@ github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -672,6 +1270,9 @@ github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolM github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -680,6 +1281,10 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= @@ -740,15 +1345,20 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -759,10 +1369,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/proullon/ramsql v0.1.4 h1:yTFRTn46gFH/kPbzCx+mGjuFlyTBUeDr3h2ldwxddl0= github.com/proullon/ramsql v0.1.4/go.mod h1:CFGqeQHQpdRfWqYmWD3yXqPTEaHkF4zgXy1C6qDWc9E= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -773,6 +1388,7 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -788,6 +1404,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -812,6 +1431,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= @@ -846,6 +1466,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -873,10 +1494,10 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -894,6 +1515,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHS go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= @@ -930,9 +1553,11 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -955,6 +1580,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= @@ -966,6 +1592,10 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -990,8 +1620,11 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= @@ -1029,6 +1662,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= @@ -1036,15 +1670,27 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= @@ -1072,8 +1718,19 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1085,7 +1742,10 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= @@ -1133,12 +1793,14 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1148,10 +1810,12 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1164,11 +1828,23 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1182,6 +1858,9 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= @@ -1199,6 +1878,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1209,6 +1890,9 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1266,17 +1950,22 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= @@ -1286,16 +1975,22 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1340,8 +2035,30 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= -google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= +google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1387,10 +2104,13 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= @@ -1434,9 +2154,71 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= @@ -1468,8 +2250,21 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -1487,6 +2282,9 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1525,6 +2323,41 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/sdks/go/container/boot.go b/sdks/go/container/boot.go index ab2da3169319..b75201520f39 100644 --- a/sdks/go/container/boot.go +++ b/sdks/go/container/boot.go @@ -83,13 +83,13 @@ func configureGoogleCloudProfilerEnvVars(ctx context.Context, logger *tools.Logg } // Fallback to job_name from metadata - if profilerServiceName == "" { - if jobName, jobNameExists := metadata["job_name"]; jobNameExists { - profilerServiceName = jobName - } else { - return errors.New("required job_name missing from metadata, profiling will not be enabled without it") - } - } + if profilerServiceName == "" { + if jobName, jobNameExists := metadata["job_name"]; jobNameExists { + profilerServiceName = jobName + } else { + return errors.New("required job_name missing from metadata, profiling will not be enabled without it") + } + } jobID, idExists := metadata["job_id"] if !idExists { diff --git a/sdks/go/container/boot_test.go b/sdks/go/container/boot_test.go index bb94aca36be3..244f91fe42e7 100644 --- a/sdks/go/container/boot_test.go +++ b/sdks/go/container/boot_test.go @@ -267,19 +267,19 @@ func TestConfigureGoogleCloudProfilerEnvVars(t *testing.T) { expectingError: true, }, { - name: "Missing profiler name and job_name", - options: `{ + name: "Missing profiler name and job_name", + options: `{ "beam:option:go_options:v1": { "options": { "dataflow_service_options": "enable_google_cloud_profiler" } } }`, - metadata: map[string]string{ - "job_id": "job-789", - }, - expectingError: true, - }, + metadata: map[string]string{ + "job_id": "job-789", + }, + expectingError: true, + }, } for _, tt := range tests { diff --git a/sdks/go/pkg/beam/io/fhirio/common.go b/sdks/go/pkg/beam/io/fhirio/common.go index 504e65270d58..75781f7ba0cc 100644 --- a/sdks/go/pkg/beam/io/fhirio/common.go +++ b/sdks/go/pkg/beam/io/fhirio/common.go @@ -127,12 +127,11 @@ func (c *fhirStoreClientImpl) search(storePath, resourceType string, queries map queryParams = append(queryParams, googleapi.QueryParameter(pageTokenParameterKey, pageToken)) } - // Pass nil as the body because search parameters are passed via queryParams, - // and the new API expects an io.Reader instead of a specific struct. + searchRequest := &healthcare.SearchResourcesRequest{} if resourceType == "" { - return c.fhirService().Search(storePath, nil).Do(queryParams...) + return c.fhirService().Search(storePath, searchRequest).Do(queryParams...) } - return c.fhirService().SearchType(storePath, resourceType, nil).Do(queryParams...) + return c.fhirService().SearchType(storePath, resourceType, searchRequest).Do(queryParams...) } func (c *fhirStoreClientImpl) deidentify(srcStorePath, dstStorePath string, deidConfig *healthcare.DeidentifyConfig) (operationResults, error) { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow.go b/sdks/go/pkg/beam/runners/dataflow/dataflow.go index ecbfe53939ec..101441dbcb56 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow.go @@ -52,34 +52,32 @@ import ( // TODO(herohde) 5/16/2017: the Dataflow flags should match the other SDKs. var ( - endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") - stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") - workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") - image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") - labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") - serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") - numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") - workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") - maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") - diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") - diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") - diskProvisionedIops = flag.Int64("disk_provisioned_iops", 0, "Provisioned IOPS for the worker disk (optional).") - diskProvisionedThroughputMibps = flag.Int64("disk_provisioned_throughput_mibps", 0, "Provisioned throughput in MiB/s for the worker disk (optional).") - autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") - zone = flag.String("zone", "", "GCP zone (optional)") - kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") - network = flag.String("network", "", "GCP network (optional)") - subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") - noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") - usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") - tempLocation = flag.String("temp_location", "", "Temp location (optional)") - workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") - machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") - minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") - workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") - workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") - dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") - flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") + endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") + stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") + workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") + image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") + labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") + serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") + numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") + workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") + maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") + diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") + diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") + autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") + zone = flag.String("zone", "", "GCP zone (optional)") + kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") + network = flag.String("network", "", "GCP network (optional)") + subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") + noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") + usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") + tempLocation = flag.String("temp_location", "", "Temp location (optional)") + workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") + machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") + minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") + workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") + workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") + dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") + flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") // TODO(https://github.com/apache/beam/issues/21604) Turn this on once TO_STRING is implemented // enableHotKeyLogging = flag.Bool("enable_hot_key_logging", false, "Specifies that when a hot key is detected in the pipeline, the literal, human-readable key is printed in the user's Cloud Logging project (optional).") @@ -107,35 +105,33 @@ func init() { // should be added to this map. // Don't filter temp_location since we need this included in PipelineOptions to correctly upload heap dumps. var flagFilter = map[string]bool{ - "dataflow_endpoint": true, - "staging_location": true, - "worker_harness_container_image": true, - "sdk_container_image": true, - "labels": true, - "service_account_email": true, - "num_workers": true, - "max_num_workers": true, - "disk_size_gb": true, - "disk_type": true, - "disk_provisioned_iops": true, - "disk_provisioned_throughput_mibps": true, - "autoscaling_algorithm": true, - "zone": true, - "network": true, - "subnetwork": true, - "no_use_public_ips": true, - "template_location": true, - "worker_machine_type": true, - "machine_type": true, - "min_cpu_platform": true, - "dataflow_worker_jar": true, - "worker_region": true, - "worker_zone": true, - "teardown_policy": true, - "cpu_profiling": true, - "session_recording": true, - "update": true, - "transform_name_mapping": true, + "dataflow_endpoint": true, + "staging_location": true, + "worker_harness_container_image": true, + "sdk_container_image": true, + "labels": true, + "service_account_email": true, + "num_workers": true, + "max_num_workers": true, + "disk_size_gb": true, + "disk_type": true, + "autoscaling_algorithm": true, + "zone": true, + "network": true, + "subnetwork": true, + "no_use_public_ips": true, + "template_location": true, + "worker_machine_type": true, + "machine_type": true, + "min_cpu_platform": true, + "dataflow_worker_jar": true, + "worker_region": true, + "worker_zone": true, + "teardown_policy": true, + "cpu_profiling": true, + "session_recording": true, + "update": true, + "transform_name_mapping": true, // Job Options flags "endpoint": true, @@ -383,39 +379,37 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions beam.PipelineOptions.LoadOptionsFromFlags(flagFilter) opts := &dataflowlib.JobOptions{ - Name: jobopts.GetJobName(), - Streaming: streaming, - Experiments: experiments, - DataflowServiceOptions: dfServiceOptions, - Options: beam.PipelineOptions.Export(), - Project: project, - Region: region, - Zone: *zone, - KmsKey: *kmsKey, - Network: *network, - Subnetwork: *subnetwork, - NoUsePublicIPs: *noUsePublicIPs, - NumWorkers: *numWorkers, - MaxNumWorkers: *maxNumWorkers, - WorkerHarnessThreads: *workerHarnessThreads, - DiskSizeGb: *diskSizeGb, - DiskType: *diskType, - DiskProvisionedIops: *diskProvisionedIops, - DiskProvisionedThroughputMibps: *diskProvisionedThroughputMibps, - Algorithm: *autoscalingAlgorithm, - FlexRSGoal: *flexRSGoal, - MachineType: *firstNonEmpty(workerMachineType, machineType), - Labels: jobLabels, - ServiceAccountEmail: *serviceAccountEmail, - TempLocation: *tempLocation, - TemplateLocation: *templateLocation, - Worker: *jobopts.WorkerBinary, - WorkerRegion: *workerRegion, - WorkerZone: *workerZone, - TeardownPolicy: *teardownPolicy, - ContainerImage: getContainerImage(ctx), - Update: *update, - TransformNameMapping: updateTransformMapping, + Name: jobopts.GetJobName(), + Streaming: streaming, + Experiments: experiments, + DataflowServiceOptions: dfServiceOptions, + Options: beam.PipelineOptions.Export(), + Project: project, + Region: region, + Zone: *zone, + KmsKey: *kmsKey, + Network: *network, + Subnetwork: *subnetwork, + NoUsePublicIPs: *noUsePublicIPs, + NumWorkers: *numWorkers, + MaxNumWorkers: *maxNumWorkers, + WorkerHarnessThreads: *workerHarnessThreads, + DiskSizeGb: *diskSizeGb, + DiskType: *diskType, + Algorithm: *autoscalingAlgorithm, + FlexRSGoal: *flexRSGoal, + MachineType: *firstNonEmpty(workerMachineType, machineType), + Labels: jobLabels, + ServiceAccountEmail: *serviceAccountEmail, + TempLocation: *tempLocation, + TemplateLocation: *templateLocation, + Worker: *jobopts.WorkerBinary, + WorkerRegion: *workerRegion, + WorkerZone: *workerZone, + TeardownPolicy: *teardownPolicy, + ContainerImage: getContainerImage(ctx), + Update: *update, + TransformNameMapping: updateTransformMapping, } if opts.TempLocation == "" { opts.TempLocation = gcsx.Join(*stagingLocation, "tmp") diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go index 23dcd034120a..fa418ebbe6a0 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go @@ -516,26 +516,6 @@ func getFieldFromOpt(fieldName string, opts *dataflowlib.JobOptions) string { return reflect.ValueOf(opts).Elem().FieldByName(fieldName).String() } -func TestGetJobOptions_DiskProvisionedOptions(t *testing.T) { - resetGlobals() - *stagingLocation = "gs://testStagingLocation" - *gcpopts.Project = "testProject" - *gcpopts.Region = "testRegion" - *diskProvisionedIops = 1000 - *diskProvisionedThroughputMibps = 100 - - opts, err := getJobOptions(context.Background(), false) - if err != nil { - t.Fatalf("getJobOptions() returned error %q, want %q", err, "nil") - } - if got, want := opts.DiskProvisionedIops, int64(1000); got != want { - t.Errorf("opts.DiskProvisionedIops = %d, want %d", got, want) - } - if got, want := opts.DiskProvisionedThroughputMibps, int64(100); got != want { - t.Errorf("opts.DiskProvisionedThroughputMibps = %d, want %d", got, want) - } -} - func resetGlobals() { *autoscalingAlgorithm = "" *dataflowServiceOptions = "" @@ -557,6 +537,4 @@ func resetGlobals() { *workerHarnessImage = "" *workerMachineType = "" *machineType = "" - *diskProvisionedIops = 0 - *diskProvisionedThroughputMibps = 0 } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go index 26412031cb0d..db83992fbebc 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go @@ -47,28 +47,26 @@ type JobOptions struct { // Pipeline options Options runtime.RawOptions - Streaming bool - Project string - Region string - Zone string - KmsKey string - Network string - Subnetwork string - NoUsePublicIPs bool - NumWorkers int64 - DiskSizeGb int64 - DiskType string - DiskProvisionedIops int64 - DiskProvisionedThroughputMibps int64 - MachineType string - Labels map[string]string - ServiceAccountEmail string - WorkerRegion string - WorkerZone string - ContainerImage string - ArtifactURLs []string // Additional packages for workers. - FlexRSGoal string - EnableHotKeyLogging bool + Streaming bool + Project string + Region string + Zone string + KmsKey string + Network string + Subnetwork string + NoUsePublicIPs bool + NumWorkers int64 + DiskSizeGb int64 + DiskType string + MachineType string + Labels map[string]string + ServiceAccountEmail string + WorkerRegion string + WorkerZone string + ContainerImage string + ArtifactURLs []string // Additional packages for workers. + FlexRSGoal string + EnableHotKeyLogging bool // Streaming update settings Update bool @@ -156,9 +154,6 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker return nil, err } - if opts.Options.Options == nil { - opts.Options.Options = make(map[string]string) - } opts.Options.Options["experiments"] = strings.Join(opts.Experiments, ",") job := &df.Job{ ProjectId: opts.Project, @@ -194,20 +189,18 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker AutoscalingSettings: &df.AutoscalingSettings{ MaxNumWorkers: opts.MaxNumWorkers, }, - DiskSizeGb: opts.DiskSizeGb, - DiskType: opts.DiskType, - DiskProvisionedIops: opts.DiskProvisionedIops, - DiskProvisionedThroughputMibps: opts.DiskProvisionedThroughputMibps, - IpConfiguration: ipConfiguration, - Kind: "harness", - Packages: packages, - WorkerHarnessContainerImage: opts.ContainerImage, - SdkHarnessContainerImages: dfImages, - NumWorkers: 1, - MachineType: opts.MachineType, - Network: opts.Network, - Subnetwork: opts.Subnetwork, - Zone: opts.Zone, + DiskSizeGb: opts.DiskSizeGb, + DiskType: opts.DiskType, + IpConfiguration: ipConfiguration, + Kind: "harness", + Packages: packages, + WorkerHarnessContainerImage: opts.ContainerImage, + SdkHarnessContainerImages: dfImages, + NumWorkers: 1, + MachineType: opts.MachineType, + Network: opts.Network, + Subnetwork: opts.Subnetwork, + Zone: opts.Zone, }}, WorkerRegion: opts.WorkerRegion, WorkerZone: opts.WorkerZone, diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go index 4b908900db75..fb44ff9c0133 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go @@ -280,39 +280,3 @@ func Test_containerImages(t *testing.T) { } } - -func TestTranslate(t *testing.T) { - ctx := context.Background() - p := &pipepb.Pipeline{} - opts := &JobOptions{ - Project: "test-project", - Name: "test-job", - DiskProvisionedIops: 4000, - DiskProvisionedThroughputMibps: 200, - } - workerURL := "gs://any-location/temp" - modelURL := "gs://any-location/temp" - - job, err := Translate(ctx, p, opts, workerURL, modelURL) - if err != nil { - t.Fatalf("Translate(...) error = %v, want nil", err) - } - - if job.Environment == nil { - t.Fatal("Translate(...) job has nil environment") - } - - if len(job.Environment.WorkerPools) == 0 { - t.Fatal("Translate(...) job has no worker pools") - } - - workerPool := job.Environment.WorkerPools[0] - - if workerPool.DiskProvisionedIops != 4000 { - t.Errorf("DiskProvisionedIops = %v, want %v", workerPool.DiskProvisionedIops, 4000) - } - - if workerPool.DiskProvisionedThroughputMibps != 200 { - t.Errorf("DiskProvisionedThroughputMibps = %v, want %v", workerPool.DiskProvisionedThroughputMibps, 200) - } -} diff --git a/sdks/go/test/integration/expansions.go b/sdks/go/test/integration/expansions.go index 74700e839269..633f88d02930 100644 --- a/sdks/go/test/integration/expansions.go +++ b/sdks/go/test/integration/expansions.go @@ -103,9 +103,9 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { if err != nil { return "", fmt.Errorf("cannot run jar for expansion service labeled \"%s\": %w", label, err) } - + addr := "localhost:" + portStr - + // Use different wait strategies for test mode vs production if es.testMode { // In test mode, use simple wait time for compatibility with mock processes @@ -114,7 +114,7 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { // In production, wait for the jar to start with improved retry logic maxRetries := 30 retryDelay := time.Second - + for i := 0; i < maxRetries; i++ { time.Sleep(retryDelay) // Try to connect to the expansion service to verify it's ready @@ -128,7 +128,7 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { } } } - + es.procs = append(es.procs, proc) es.addrs[label] = addr return addr, nil diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index 91dd77d6cf02..d60d75283eab 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -1405,24 +1405,6 @@ def _add_argparse_args(cls, parser): dest='disk_type', default=None, help=('Specifies what type of persistent disk should be used.')) - parser.add_argument( - '--disk_provisioned_iops', - type=int, - default=None, - dest='disk_provisioned_iops', - help=( - 'The provisioned IOPS of the disk. If not set, the Dataflow service' - ' will choose a reasonable default.'), - ) - parser.add_argument( - '--disk_provisioned_throughput_mibps', - type=int, - default=None, - dest='disk_provisioned_throughput_mibps', - help=( - 'The provisioned throughput of the disk in MiB/s. If not set, the' - ' Dataflow service will choose a reasonable default.'), - ) parser.add_argument( '--worker_region', default=None, diff --git a/sdks/python/apache_beam/options/pipeline_options_test.py b/sdks/python/apache_beam/options/pipeline_options_test.py index 901f56b99cb6..215c44156ea6 100644 --- a/sdks/python/apache_beam/options/pipeline_options_test.py +++ b/sdks/python/apache_beam/options/pipeline_options_test.py @@ -444,18 +444,12 @@ def test_worker_options(self): 'abc', '--disk_type', 'def', - '--disk_provisioned_iops', - '4000', - '--disk_provisioned_throughput_mibps', - '200', '--element_processing_timeout_minutes', '10', ]) worker_options = options.view_as(WorkerOptions) self.assertEqual(worker_options.machine_type, 'abc') self.assertEqual(worker_options.disk_type, 'def') - self.assertEqual(worker_options.disk_provisioned_iops, 4000) - self.assertEqual(worker_options.disk_provisioned_throughput_mibps, 200) self.assertEqual(worker_options.element_processing_timeout_minutes, 10) options = PipelineOptions( diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 871f227c7c73..29cb36071488 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -207,11 +207,6 @@ def __init__( pool.diskSizeGb = self.worker_options.disk_size_gb if self.worker_options.disk_type: pool.diskType = self.worker_options.disk_type - if self.worker_options.disk_provisioned_iops is not None: - pool.diskProvisionedIops = self.worker_options.disk_provisioned_iops - if self.worker_options.disk_provisioned_throughput_mibps is not None: - pool.diskProvisionedThroughputMibps = ( - self.worker_options.disk_provisioned_throughput_mibps) if self.worker_options.zone: pool.zone = self.worker_options.zone if self.worker_options.network: diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index 3870bb4a2a4a..66b1c8e1e5bb 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -609,23 +609,6 @@ def test_number_of_worker_harness_threads(self): FAKE_PIPELINE_URL) self.assertEqual(env.proto.workerPools[0].numThreadsPerWorker, 2) - def test_disk_provisioning_options(self): - pipeline_options = PipelineOptions([ - '--temp_location', - 'gs://any-location/temp', - '--disk_provisioned_iops', - '4000', - '--disk_provisioned_throughput_mibps', - '200' - ]) - env = apiclient.Environment([], - pipeline_options, - '2.0.0', - FAKE_PIPELINE_URL) - self.assertEqual(env.proto.workerPools[0].diskProvisionedIops, 4000) - self.assertEqual( - env.proto.workerPools[0].diskProvisionedThroughputMibps, 200) - @mock.patch( 'apache_beam.runners.dataflow.internal.apiclient.' 'beam_version.__version__', diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py index 66c57dd46837..179e51eb95e8 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py @@ -1,5 +1,6 @@ """Generated client library for dataflow version v1b3.""" # NOTE: This file is autogenerated and should not be edited by hand. + from apitools.base.py import base_api from . import dataflow_v1b3_messages as messages diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py index f38e1e69d385..0c096e73c1ac 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py @@ -2634,8 +2634,8 @@ class FlexTemplateRuntimeEnvironment(_messages.Message): ipConfiguration: Configuration for VM IPs. kmsKeyName: Name for the Cloud KMS key for the job. Key format is: projects//locations//keyRings//cryptoKeys/ - launcherMachineType: The machine type to use for launching the job. If not - set, Dataflow will select a default machine type. + launcherMachineType: The machine type to use for launching the job. The + default is n1-standard-1. machineType: The machine type to use for the job. Defaults to the value from the template if not specified. maxWorkers: The maximum number of Google Compute Engine instances to be @@ -3209,7 +3209,6 @@ class Job(_messages.Message): attempts to create a job with the same name as an active job that already exists, the attempt returns the existing job. The name must match the regular expression `[a-z]([-a-z0-9]{0,1022}[a-z0-9])?` - pausable: Output only. Indicates whether the job can be paused. pipelineDescription: Preliminary field: The format of this data may change at any time. A description of the user pipeline and stages through which it is executed. Created by Cloud Dataflow service. Only retrieved with @@ -3499,23 +3498,22 @@ class AdditionalProperty(_messages.Message): labels = _messages.MessageField('LabelsValue', 10) location = _messages.StringField(11) name = _messages.StringField(12) - pausable = _messages.BooleanField(13) - pipelineDescription = _messages.MessageField('PipelineDescription', 14) - projectId = _messages.StringField(15) - replaceJobId = _messages.StringField(16) - replacedByJobId = _messages.StringField(17) - requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 18) - runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 19) - satisfiesPzi = _messages.BooleanField(20) - satisfiesPzs = _messages.BooleanField(21) - serviceResources = _messages.MessageField('ServiceResources', 22) - stageStates = _messages.MessageField('ExecutionStageState', 23, repeated=True) - startTime = _messages.StringField(24) - steps = _messages.MessageField('Step', 25, repeated=True) - stepsLocation = _messages.StringField(26) - tempFiles = _messages.StringField(27, repeated=True) - transformNameMapping = _messages.MessageField('TransformNameMappingValue', 28) - type = _messages.EnumField('TypeValueValuesEnum', 29) + pipelineDescription = _messages.MessageField('PipelineDescription', 13) + projectId = _messages.StringField(14) + replaceJobId = _messages.StringField(15) + replacedByJobId = _messages.StringField(16) + requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 17) + runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 18) + satisfiesPzi = _messages.BooleanField(19) + satisfiesPzs = _messages.BooleanField(20) + serviceResources = _messages.MessageField('ServiceResources', 21) + stageStates = _messages.MessageField('ExecutionStageState', 22, repeated=True) + startTime = _messages.StringField(23) + steps = _messages.MessageField('Step', 24, repeated=True) + stepsLocation = _messages.StringField(25) + tempFiles = _messages.StringField(26, repeated=True) + transformNameMapping = _messages.MessageField('TransformNameMappingValue', 27) + type = _messages.EnumField('TypeValueValuesEnum', 28) class JobExecutionDetails(_messages.Message): @@ -5344,14 +5342,8 @@ class RuntimeUpdatableParams(_messages.Message): during job creation. Fields: - acceptableBacklogDuration: Optional. Deprecated: Use `latency_tier` - instead. The backlog threshold duration in seconds for autoscaling. - Value must be non-negative. - autoscalingTier: Optional. Deprecated: Use `latency_tier` instead. The - backlog threshold tier for autoscaling. Value must be one of "low- - latency", "medium-latency", or "high-latency". - latencyTier: Optional. The backlog threshold tier for autoscaling. Value - must be one of "low-latency", "medium-latency", or "high-latency". + acceptableBacklogDuration: Optional. The backlog threshold duration in + seconds for autoscaling. Value must be non-negative. maxNumWorkers: The maximum number of workers to cap autoscaling at. This field is currently only supported for Streaming Engine jobs. minNumWorkers: The minimum number of workers to scale down to. This field @@ -5365,11 +5357,9 @@ class RuntimeUpdatableParams(_messages.Message): """ acceptableBacklogDuration = _messages.StringField(1) - autoscalingTier = _messages.StringField(2) - latencyTier = _messages.StringField(3) - maxNumWorkers = _messages.IntegerField(4, variant=_messages.Variant.INT32) - minNumWorkers = _messages.IntegerField(5, variant=_messages.Variant.INT32) - workerUtilizationHint = _messages.FloatField(6) + maxNumWorkers = _messages.IntegerField(2, variant=_messages.Variant.INT32) + minNumWorkers = _messages.IntegerField(3, variant=_messages.Variant.INT32) + workerUtilizationHint = _messages.FloatField(4) class SDKInfo(_messages.Message): @@ -7785,9 +7775,6 @@ class WorkerPool(_messages.Message): defaultPackageSet: The default package set to install. This allows the service to select a default set of packages which are useful to worker harnesses written in a particular language. - diskProvisionedIops: Optional. IOPS provisioned for the root disk for VMs. - diskProvisionedThroughputMibps: Optional. Throughput provisioned for the - root disk for VMs. diskSizeGb: Size of root disk for VMs, in GB. If zero or unspecified, the service will attempt to choose a reasonable default. diskSourceImage: Fully qualified source image for disks. @@ -7951,27 +7938,25 @@ class AdditionalProperty(_messages.Message): autoscalingSettings = _messages.MessageField('AutoscalingSettings', 1) dataDisks = _messages.MessageField('Disk', 2, repeated=True) defaultPackageSet = _messages.EnumField('DefaultPackageSetValueValuesEnum', 3) - diskProvisionedIops = _messages.IntegerField(4) - diskProvisionedThroughputMibps = _messages.IntegerField(5) - diskSizeGb = _messages.IntegerField(6, variant=_messages.Variant.INT32) - diskSourceImage = _messages.StringField(7) - diskType = _messages.StringField(8) - ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 9) - kind = _messages.StringField(10) - machineType = _messages.StringField(11) - metadata = _messages.MessageField('MetadataValue', 12) - network = _messages.StringField(13) - numThreadsPerWorker = _messages.IntegerField(14, variant=_messages.Variant.INT32) - numWorkers = _messages.IntegerField(15, variant=_messages.Variant.INT32) - onHostMaintenance = _messages.StringField(16) - packages = _messages.MessageField('Package', 17, repeated=True) - poolArgs = _messages.MessageField('PoolArgsValue', 18) - sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 19, repeated=True) - subnetwork = _messages.StringField(20) - taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 21) - teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 22) - workerHarnessContainerImage = _messages.StringField(23) - zone = _messages.StringField(24) + diskSizeGb = _messages.IntegerField(4, variant=_messages.Variant.INT32) + diskSourceImage = _messages.StringField(5) + diskType = _messages.StringField(6) + ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 7) + kind = _messages.StringField(8) + machineType = _messages.StringField(9) + metadata = _messages.MessageField('MetadataValue', 10) + network = _messages.StringField(11) + numThreadsPerWorker = _messages.IntegerField(12, variant=_messages.Variant.INT32) + numWorkers = _messages.IntegerField(13, variant=_messages.Variant.INT32) + onHostMaintenance = _messages.StringField(14) + packages = _messages.MessageField('Package', 15, repeated=True) + poolArgs = _messages.MessageField('PoolArgsValue', 16) + sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 17, repeated=True) + subnetwork = _messages.StringField(18) + taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 19) + teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 20) + workerHarnessContainerImage = _messages.StringField(21) + zone = _messages.StringField(22) class WorkerSettings(_messages.Message): @@ -8069,4 +8054,4 @@ class WriteInstruction(_messages.Message): encoding.AddCustomJsonFieldMapping( DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_gcsPath', 'dynamicTemplate.gcsPath') encoding.AddCustomJsonFieldMapping( - DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_stagingLocation', 'dynamicTemplate.stagingLocation') \ No newline at end of file + DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_stagingLocation', 'dynamicTemplate.stagingLocation') From aa424cb69d5bf15bdd5ad57fcc8a99fe84f58313 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:39:59 -0400 Subject: [PATCH 021/490] Bump pytest from 8.4.2 to 9.0.3 in /sdks/python/container/ml/py312 (#38178) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/ml/py312/base_image_requirements.txt | 2 +- sdks/python/container/ml/py312/gpu_image_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index c72558f8acd9..3e2cef4e0243 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -173,7 +173,7 @@ pymongo==4.16.0 PyMySQL==1.1.2 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index 632ba689e3ee..c12dc4b97f81 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -232,7 +232,7 @@ pymongo==4.16.0 PyMySQL==1.1.2 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 From 5a142ce6a2edc2e5c3302f95bd63915c775deca1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:51:15 -0400 Subject: [PATCH 022/490] Bump requests from 2.32.5 to 2.33.0 in /sdks/python/container/ml/py312 (#37968) Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.33.0) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/ml/py312/base_image_requirements.txt | 2 +- sdks/python/container/ml/py312/gpu_image_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index 3e2cef4e0243..5b3b627c28c0 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -183,7 +183,7 @@ pytz==2026.1.post1 PyYAML==6.0.3 referencing==0.37.0 regex==2026.3.32 -requests==2.33.1 +requests==2.33.0 requests-mock==1.12.1 rich==14.3.3 rpds-py==0.30.0 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index c12dc4b97f81..2aa44b8ebb7f 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -246,7 +246,7 @@ pyzmq==27.1.0 ray==2.54.0 referencing==0.37.0 regex==2026.2.28 -requests==2.32.5 +requests==2.33.0 requests-mock==1.12.1 rich==14.3.3 rich-toolkit==0.19.7 From 6b0710d0019efb634a0d9a86cefa6cb798ca58ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:51:46 -0400 Subject: [PATCH 023/490] Bump requests (#37962) Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.33.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.33.0) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../kfp/components/preprocessing/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt index 8d282bff5224..dc8e9f96796e 100644 --- a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt @@ -14,7 +14,7 @@ # limitations under the License. apache_beam[gcp]==2.40.0 -requests==2.32.4 +requests==2.33.0 torch==2.8.0 torchvision==0.13.0 numpy==1.22.4 From 59417558d2731166be9cd4305253e9122fee173a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Wed, 29 Apr 2026 18:07:59 +0200 Subject: [PATCH 024/490] Add outstanding parameters to processElement and onTimer signatures (#37933) * add outstanding parameters to processElement and onTimer signatures --- .../beam/runners/core/SimpleDoFnRunner.java | 51 ++++++++++++ .../org/apache/beam/sdk/transforms/DoFn.java | 43 +++++++++- .../reflect/ByteBuddyDoFnInvokerFactory.java | 33 ++++++++ .../sdk/transforms/reflect/DoFnInvoker.java | 44 +++++++++++ .../sdk/transforms/reflect/DoFnSignature.java | 78 +++++++++++++++++++ .../transforms/reflect/DoFnSignatures.java | 19 +++++ .../SplittableParDoNaiveBounded.java | 15 ++++ .../reflect/DoFnSignaturesTest.java | 76 ++++++++++++++++++ .../beam/fn/harness/FnApiDoFnRunner.java | 21 +++++ 9 files changed, 376 insertions(+), 4 deletions(-) diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java index d5f09522a3b0..d553a7be2d44 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java @@ -576,6 +576,22 @@ public Instant timestamp(DoFn doFn) { return timestamp(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return currentRecordId(); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return currentRecordOffset(); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access fire timestamp outside of @OnTimer method."); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return elem.causedByDrain(); @@ -857,6 +873,23 @@ public Instant timestamp(DoFn doFn) { return timestamp(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record id outside of @ProcessElement method."); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record offset outside of @ProcessElement method."); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + return fireTimestamp(); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return causedByDrain; @@ -1166,6 +1199,24 @@ public CausedByDrain causedByDrain(DoFn doFn) { throw new UnsupportedOperationException("CausedByDrain parameters are not supported."); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record id outside of @ProcessElement method."); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record offset outside of @ProcessElement method."); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access fire timestamp outside of @OnTimer method."); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException("Timer parameters are not supported."); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java index 0d892ab12d33..864b903b25f9 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java @@ -595,8 +595,10 @@ public interface MultiOutputReceiver { * *

    The method annotated with {@code @OnTimer} may have parameters according to the same logic * as {@link ProcessElement}, but limited to the {@link BoundedWindow}, {@link State} subclasses, - * and {@link Timer}. State and timer parameters must be annotated with their {@link StateId} and - * {@link TimerId} respectively. + * {@link Timer}, {@link FireTimestamp}, {@link Timestamp}, {@link Key}, {@link TimeDomain}, + * {@link PipelineOptions}, {@link OutputReceiver}, {@link MultiOutputReceiver}, and {@link + * org.apache.beam.sdk.values.CausedByDrain CausedByDrain}. State and timer parameters must be + * annotated with their {@link StateId} and {@link TimerId} respectively. */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -613,8 +615,11 @@ public interface MultiOutputReceiver { * *

    The method annotated with {@code @OnTimerFamily} may have parameters according to the same * logic as {@link ProcessElement}, but limited to the {@link BoundedWindow}, {@link State} - * subclasses, and {@link org.apache.beam.sdk.state.TimerMap}. State and timer parameters must be - * annotated with their {@link StateId} and {@link TimerId} respectively. + * subclasses, {@link org.apache.beam.sdk.state.TimerMap}, {@link FireTimestamp}, {@link + * Timestamp}, {@link Key}, {@link TimeDomain}, {@link PipelineOptions}, {@link OutputReceiver}, + * {@link MultiOutputReceiver}, and {@link org.apache.beam.sdk.values.CausedByDrain + * CausedByDrain}. State and timer parameters must be annotated with their {@link StateId} and + * {@link TimerId} respectively. */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -705,6 +710,12 @@ public interface MultiOutputReceiver { *

  • If one of its arguments is tagged with the {@link Element} annotation, then it will be * passed the current element being processed. The argument type must match the input type * of this DoFn exactly, or both types must have equivalent schemas registered. + *
  • If one of its arguments is tagged with the {@link CurrentRecordId} annotation, then it + * will be passed the record id of the current element being processed; the argument must be + * of type {@link String}. + *
  • If one of its arguments is tagged with the {@link CurrentRecordOffset} annotation, then + * it will be passed the record offset of the current element being processed; the argument + * must be of type {@link Long}. *
  • If one of its arguments is tagged with the {@link Timestamp} annotation, then it will be * passed the timestamp of the current element being processed; the argument must be of type * {@link Instant}. @@ -795,6 +806,12 @@ public interface MultiOutputReceiver { *
  • If one of its arguments is tagged with the {@link Timestamp} annotation, then it will be * passed the timestamp of the current element being processed; the argument must be of type * {@link Instant}. + *
  • If one of its arguments is tagged with the {@link CurrentRecordId} annotation, then it + * will be passed the record ID of the current element being processed; the argument must be + * of type {@link String}. + *
  • If one of its arguments is tagged with the {@link CurrentRecordOffset} annotation, then + * it will be passed the record offset of the current element being processed; the argument + * must be of type {@link Long}. *
  • If one of its arguments is of the type {@link WatermarkEstimator}, then it will be passed * the watermark estimator. *
  • If one of its arguments is of the type {@link ManualWatermarkEstimator}, then it will be @@ -838,6 +855,18 @@ public interface MultiOutputReceiver { @Target(ElementType.PARAMETER) public @interface Element {} + /** Parameter annotation for the input element record id for {@link ProcessElement}. */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CurrentRecordId {} + + /** Parameter annotation for the input element record offset for {@link ProcessElement}. */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CurrentRecordOffset {} + /** * Parameter annotation for the restriction for {@link GetSize}, {@link SplitRestriction}, {@link * GetInitialWatermarkEstimatorState}, {@link NewWatermarkEstimator}, and {@link NewTracker} @@ -860,6 +889,12 @@ public interface MultiOutputReceiver { @Target(ElementType.PARAMETER) public @interface Timestamp {} + /** Parameter annotation for the firing timestamp for {@link OnTimer}. */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface FireTimestamp {} + /** Parameter annotation for the SideInput for a {@link ProcessElement} method. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java index 54d630d92fe4..9adbe3a12cf4 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java @@ -77,8 +77,11 @@ import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.BundleFinalizerParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.Cases; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CausedByDrainParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordIdParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordOffsetParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.ElementParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FinishBundleContextParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FireTimestampParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.OnTimerContextParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.OutputReceiverParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.PaneInfoParameter; @@ -128,6 +131,9 @@ class ByteBuddyDoFnInvokerFactory implements DoFnInvokerFactory { public static final String SCHEMA_ELEMENT_PARAMETER_METHOD = "schemaElement"; public static final String TIMESTAMP_PARAMETER_METHOD = "timestamp"; public static final String CAUSED_BY_DRAIN_PARAMETER_METHOD = "causedByDrain"; + public static final String CURRENT_RECORD_ID_PARAMETER_METHOD = "currentRecordId"; + public static final String CURRENT_RECORD_OFFSET_PARAMETER_METHOD = "currentRecordOffset"; + public static final String FIRE_TIMESTAMP_PARAMETER_METHOD = "fireTimestamp"; public static final String BUNDLE_FINALIZER_PARAMETER_METHOD = "bundleFinalizer"; public static final String OUTPUT_ROW_RECEIVER_METHOD = "outputRowReceiver"; public static final String TIME_DOMAIN_PARAMETER_METHOD = "timeDomain"; @@ -1265,6 +1271,33 @@ public StackManipulation dispatch(DoFnSignature.Parameter.TimerIdParameter p) { TIMER_ID_PARAMETER_METHOD, DoFn.class))); } + @Override + public StackManipulation dispatch(CurrentRecordIdParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + CURRENT_RECORD_ID_PARAMETER_METHOD, DoFn.class))); + } + + @Override + public StackManipulation dispatch(CurrentRecordOffsetParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + CURRENT_RECORD_OFFSET_PARAMETER_METHOD, DoFn.class))); + } + + @Override + public StackManipulation dispatch(FireTimestampParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + FIRE_TIMESTAMP_PARAMETER_METHOD, DoFn.class))); + } + @Override public StackManipulation dispatch(DoFnSignature.Parameter.KeyParameter p) { return new StackManipulation.Compound( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java index a615761292aa..1f122f1bf661 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java @@ -218,6 +218,17 @@ interface ArgumentProvider { /** Provide a reference to the input element timestamp. */ Instant timestamp(DoFn doFn); + /** Provide a reference to the record id of the current element. */ + @Nullable + String currentRecordId(DoFn doFn); + + /** Provide a reference to the record offset of the current element. */ + @Nullable + Long currentRecordOffset(DoFn doFn); + + /** Provide a reference to the firing timestamp of the current timer. */ + Instant fireTimestamp(DoFn doFn); + /** Provide a reference to the caused by drain. */ CausedByDrain causedByDrain(DoFn doFn); @@ -329,6 +340,24 @@ public Instant timestamp(DoFn doFn) { String.format("Timestamp unsupported in %s", getErrorContext())); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("RecordId unsupported in %s", getErrorContext())); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("RecordOffset unsupported in %s", getErrorContext())); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("FireTimestamp unsupported in %s", getErrorContext())); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { throw new UnsupportedOperationException( @@ -524,6 +553,21 @@ public Instant timestamp(DoFn doFn) { return delegate.timestamp(doFn); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return delegate.currentRecordId(doFn); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return delegate.currentRecordOffset(doFn); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + return delegate.fireTimestamp(doFn); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return delegate.causedByDrain(doFn); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java index 83c2a67655bd..33fccc2e1cde 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java @@ -345,6 +345,12 @@ public ResultT match(Cases cases) { return cases.dispatch((BundleFinalizerParameter) this); } else if (this instanceof CausedByDrainParameter) { return cases.dispatch((CausedByDrainParameter) this); + } else if (this instanceof CurrentRecordIdParameter) { + return cases.dispatch((CurrentRecordIdParameter) this); + } else if (this instanceof CurrentRecordOffsetParameter) { + return cases.dispatch((CurrentRecordOffsetParameter) this); + } else if (this instanceof FireTimestampParameter) { + return cases.dispatch((FireTimestampParameter) this); } else if (this instanceof KeyParameter) { return cases.dispatch((KeyParameter) this); } else { @@ -375,6 +381,12 @@ public interface Cases { ResultT dispatch(TaggedOutputReceiverParameter p); + ResultT dispatch(CurrentRecordIdParameter p); + + ResultT dispatch(CurrentRecordOffsetParameter p); + + ResultT dispatch(FireTimestampParameter p); + ResultT dispatch(OnTimerContextParameter p); ResultT dispatch(WindowParameter p); @@ -462,6 +474,21 @@ public ResultT dispatch(TimeDomainParameter p) { return dispatchDefault(p); } + @Override + public ResultT dispatch(CurrentRecordIdParameter p) { + return dispatchDefault(p); + } + + @Override + public ResultT dispatch(CurrentRecordOffsetParameter p) { + return dispatchDefault(p); + } + + @Override + public ResultT dispatch(FireTimestampParameter p) { + return dispatchDefault(p); + } + @Override public ResultT dispatch(OnTimerContextParameter p) { return dispatchDefault(p); @@ -566,6 +593,12 @@ public ResultT dispatch(KeyParameter p) { new AutoValue_DoFnSignature_Parameter_CausedByDrainParameter(); private static final OnWindowExpirationContextParameter ON_WINDOW_EXPIRATION_CONTEXT_PARAMETER = new AutoValue_DoFnSignature_Parameter_OnWindowExpirationContextParameter(); + private static final CurrentRecordIdParameter CURRENT_RECORD_ID_PARAMETER = + new AutoValue_DoFnSignature_Parameter_CurrentRecordIdParameter(); + private static final CurrentRecordOffsetParameter CURRENT_RECORD_OFFSET_PARAMETER = + new AutoValue_DoFnSignature_Parameter_CurrentRecordOffsetParameter(); + private static final FireTimestampParameter FIRE_TIMESTAMP_PARAMETER = + new AutoValue_DoFnSignature_Parameter_FireTimestampParameter(); /** Returns a {@link ProcessContextParameter}. */ public static ProcessContextParameter processContext() { @@ -592,6 +625,21 @@ public static CausedByDrainParameter causedByDrainParameter() { return CAUSED_BY_DRAIN_PARAMETER; } + /** Returns a {@link CurrentRecordIdParameter}. */ + public static CurrentRecordIdParameter currentRecordIdParameter() { + return CURRENT_RECORD_ID_PARAMETER; + } + + /** Returns a {@link CurrentRecordOffsetParameter}. */ + public static CurrentRecordOffsetParameter currentRecordOffsetParameter() { + return CURRENT_RECORD_OFFSET_PARAMETER; + } + + /** Returns a {@link FireTimestampParameter}. */ + public static FireTimestampParameter fireTimestampParameter() { + return FIRE_TIMESTAMP_PARAMETER; + } + public static ElementParameter elementParameter(TypeDescriptor elementT) { return new AutoValue_DoFnSignature_Parameter_ElementParameter(elementT); } @@ -754,6 +802,36 @@ public abstract static class CausedByDrainParameter extends Parameter { CausedByDrainParameter() {} } + /** + * Descriptor for a {@link Parameter} of type {@link DoFn.RecordId}. + * + *

    All such descriptors are equal. + */ + @AutoValue + public abstract static class CurrentRecordIdParameter extends Parameter { + CurrentRecordIdParameter() {} + } + + /** + * Descriptor for a {@link Parameter} of type {@link DoFn.RecordOffset}. + * + *

    All such descriptors are equal. + */ + @AutoValue + public abstract static class CurrentRecordOffsetParameter extends Parameter { + CurrentRecordOffsetParameter() {} + } + + /** + * Descriptor for a {@link Parameter} of type {@link DoFn.FireTimestamp}. + * + *

    All such descriptors are equal. + */ + @AutoValue + public abstract static class FireTimestampParameter extends Parameter { + FireTimestampParameter() {} + } + /** * Descriptor for a {@link Parameter} of type {@link DoFn.Element}. * diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java index 3dcf7ff1f9d0..ec624696fc7c 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java @@ -140,6 +140,8 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.SideInputParameter.class, Parameter.TimerFamilyParameter.class, + Parameter.CurrentRecordIdParameter.class, + Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, Parameter.BundleFinalizerParameter.class); @@ -157,6 +159,8 @@ private DoFnSignatures() {} Parameter.RestrictionTrackerParameter.class, Parameter.WatermarkEstimatorParameter.class, Parameter.SideInputParameter.class, + Parameter.CurrentRecordIdParameter.class, + Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, Parameter.BundleFinalizerParameter.class); @@ -188,6 +192,7 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.TimerFamilyParameter.class, Parameter.TimerIdParameter.class, + Parameter.FireTimestampParameter.class, Parameter.CausedByDrainParameter.class, Parameter.KeyParameter.class); @@ -205,6 +210,7 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.TimerFamilyParameter.class, Parameter.TimerIdParameter.class, + Parameter.FireTimestampParameter.class, Parameter.CausedByDrainParameter.class, Parameter.KeyParameter.class); @@ -1348,6 +1354,19 @@ private static Parameter analyzeExtraParameter( rawType.equals(Instant.class), "@Timestamp argument must have type org.joda.time.Instant."); return Parameter.timestampParameter(); + } else if (hasAnnotation(DoFn.CurrentRecordId.class, param.getAnnotations())) { + methodErrors.checkArgument( + rawType.equals(String.class), "@CurrentRecordId argument must have type String."); + return Parameter.currentRecordIdParameter(); + } else if (hasAnnotation(DoFn.CurrentRecordOffset.class, param.getAnnotations())) { + methodErrors.checkArgument( + rawType.equals(Long.class), "@CurrentRecordOffset argument must have type Long."); + return Parameter.currentRecordOffsetParameter(); + } else if (hasAnnotation(DoFn.FireTimestamp.class, param.getAnnotations())) { + methodErrors.checkArgument( + rawType.equals(Instant.class), + "@FireTimestamp argument must have type org.joda.time.Instant."); + return Parameter.fireTimestampParameter(); } else if (hasAnnotation(DoFn.Key.class, param.getAnnotations())) { methodErrors.checkArgument( KV.class.equals(inputT.getRawType()), diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java index 5adf3d32a1a9..44212f97ad28 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java @@ -688,6 +688,21 @@ public Long currentRecordOffset() { return outerContext.currentRecordOffset(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return outerContext.currentRecordId(); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return outerContext.currentRecordOffset(); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new IllegalStateException(); + } + @Override public Object watermarkEstimatorState() { throw new UnsupportedOperationException( diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java index 3369e18519ba..5a5353482c95 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java @@ -57,8 +57,11 @@ import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.BundleFinalizerParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CausedByDrainParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordIdParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordOffsetParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.ElementParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FinishBundleContextParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FireTimestampParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.OutputReceiverParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.PaneInfoParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.PipelineOptionsParameter; @@ -101,6 +104,37 @@ @SuppressWarnings("unused") public class DoFnSignaturesTest { + @Test + public void testRecordIdOnTimerError() throws Exception { + final String timerId = "some-timer-id"; + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Illegal parameter type"); + thrown.expectMessage("CurrentRecordIdParameter"); + DoFnSignatures.getSignature( + new DoFn() { + @TimerId(timerId) + private final TimerSpec myfield1 = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void process(ProcessContext c) {} + + @OnTimer(timerId) + public void onTimer(@DoFn.CurrentRecordId String id) {} + }.getClass()); + } + + @Test + public void testFireTimestampOnProcessElementError() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Illegal parameter type"); + thrown.expectMessage("FireTimestampParameter"); + DoFnSignatures.getSignature( + new DoFn() { + @ProcessElement + public void process(@FireTimestamp Instant ts) {} + }.getClass()); + } + @Rule public ExpectedException thrown = ExpectedException.none(); @Test @@ -153,6 +187,24 @@ public void process( sig.processElement().extraParameters().get(9), instanceOf(CausedByDrainParameter.class)); } + @Test + public void testProcessElementRecordIdAndOffset() throws Exception { + DoFnSignature sig = + DoFnSignatures.getSignature( + new DoFn() { + @ProcessElement + public void process( + @DoFn.CurrentRecordId String id, @DoFn.CurrentRecordOffset Long offset) {} + }.getClass()); + + assertThat(sig.processElement().extraParameters().size(), equalTo(2)); + assertThat( + sig.processElement().extraParameters().get(0), instanceOf(CurrentRecordIdParameter.class)); + assertThat( + sig.processElement().extraParameters().get(1), + instanceOf(CurrentRecordOffsetParameter.class)); + } + @Test public void testBasicDoFnMultiOutputReceiver() throws Exception { DoFnSignature sig = @@ -647,6 +699,30 @@ public void onTimer( instanceOf(WindowParameter.class)); } + @Test + public void testFireTimestampOnTimer() throws Exception { + final String timerId = "some-timer-id"; + final String timerDeclarationId = TimerDeclaration.PREFIX + timerId; + + DoFnSignature sig = + DoFnSignatures.getSignature( + new DoFn() { + @TimerId(timerId) + private final TimerSpec myfield1 = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void process(ProcessContext c) {} + + @OnTimer(timerId) + public void onTimer(@FireTimestamp Instant fireTimestamp) {} + }.getClass()); + + assertThat(sig.onTimerMethods().get(timerDeclarationId).extraParameters().size(), equalTo(1)); + assertThat( + sig.onTimerMethods().get(timerDeclarationId).extraParameters().get(0), + instanceOf(FireTimestampParameter.class)); + } + @Test public void testPipelineOptionsParameter() throws Exception { DoFnSignature sig = diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java index 4d7819ce9b42..0aaf71c016d4 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java @@ -2070,6 +2070,22 @@ public Instant timestamp(DoFn doFn) { return timestamp(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return currentRecordId(); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return currentRecordOffset(); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access fire timestamp outside of @OnTimer method."); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException( @@ -2748,6 +2764,11 @@ public TimeDomain timeDomain(DoFn doFn) { return currentTimeDomain; } + @Override + public Instant fireTimestamp(DoFn doFn) { + return currentTimer.getFireTimestamp(); + } + @Override public K key() { return (K) currentTimer.getUserKey(); From d952d3b3f637f5d1639ccf05e09dddddf26c8c27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:38:01 -0400 Subject: [PATCH 025/490] Bump torch from 2.7.1 to 2.8.0 in /sdks/python/container/ml/py312 (#38226) Bumps [torch](https://github.com/pytorch/pytorch) from 2.7.1 to 2.8.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.7.1...v2.8.0) --- updated-dependencies: - dependency-name: torch dependency-version: 2.8.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/ml/py312/base_image_requirements.txt | 2 +- sdks/python/container/ml/py312/gpu_image_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index 5b3b627c28c0..761753b6801a 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -209,7 +209,7 @@ termcolor==3.3.0 testcontainers==4.14.2 threadpoolctl==3.6.0 tokenizers==0.21.4 -torch==2.8.0+cpu +torch==2.8.0 tqdm==4.67.3 transformers==4.55.4 typing-inspection==0.4.2 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index 2aa44b8ebb7f..94d4c23ec3a4 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -284,7 +284,7 @@ testcontainers==4.14.1 threadpoolctl==3.6.0 tiktoken==0.12.0 tokenizers==0.21.4 -torch==2.7.1 +torch==2.8.0 torchaudio==2.7.1 torchvision==0.22.1 tqdm==4.67.3 From 9e2d17a8d1c8a1865ea573671b38cc7acfc918fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:38:35 -0400 Subject: [PATCH 026/490] Bump keras from 3.12.1 to 3.13.2 in /sdks/python/container/ml/py310 (#38198) Bumps [keras](https://github.com/keras-team/keras) from 3.12.1 to 3.13.2. - [Release notes](https://github.com/keras-team/keras/releases) - [Commits](https://github.com/keras-team/keras/compare/v3.12.1...v3.13.2) --- updated-dependencies: - dependency-name: keras dependency-version: 3.13.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/ml/py310/base_image_requirements.txt | 2 +- sdks/python/container/ml/py310/gpu_image_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/python/container/ml/py310/base_image_requirements.txt b/sdks/python/container/ml/py310/base_image_requirements.txt index 7ad86d13b506..b60f788ea396 100644 --- a/sdks/python/container/ml/py310/base_image_requirements.txt +++ b/sdks/python/container/ml/py310/base_image_requirements.txt @@ -125,7 +125,7 @@ Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.12.1 +keras==3.13.2 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 diff --git a/sdks/python/container/ml/py310/gpu_image_requirements.txt b/sdks/python/container/ml/py310/gpu_image_requirements.txt index dbad9196b561..ac7f9add86af 100644 --- a/sdks/python/container/ml/py310/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py310/gpu_image_requirements.txt @@ -145,7 +145,7 @@ joblib==1.5.3 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.12.1 +keras==3.13.2 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 From 515b5c1ab6d0d92d36caf5b9f1986146f50e2ad7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:39:05 -0400 Subject: [PATCH 027/490] Bump xgrammar from 0.1.21 to 0.1.32 in /sdks/python/container/ml/py312 (#37781) Bumps [xgrammar](https://github.com/mlc-ai/xgrammar) from 0.1.21 to 0.1.32. - [Release notes](https://github.com/mlc-ai/xgrammar/releases) - [Commits](https://github.com/mlc-ai/xgrammar/compare/v0.1.21...v0.1.32) --- updated-dependencies: - dependency-name: xgrammar dependency-version: 0.1.32 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/ml/py312/gpu_image_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index 94d4c23ec3a4..2b947b22e23a 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -306,7 +306,7 @@ Werkzeug==3.1.6 wheel==0.46.3 wrapt==2.1.2 xformers==0.0.31 -xgrammar==0.1.21 +xgrammar==0.1.32 yarl==1.23.0 zipp==3.23.0 zstandard==0.25.0 From 41f116d7a15008539a07a6c68159f52d184eecab Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 29 Apr 2026 18:08:17 -0400 Subject: [PATCH 028/490] update gemini review to not run on draft prs (#38333) --- .gemini/config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gemini/config.yaml b/.gemini/config.yaml index 70e33f3d77cd..b590c9d230dc 100644 --- a/.gemini/config.yaml +++ b/.gemini/config.yaml @@ -48,6 +48,10 @@ code_review: # Type boolean, default: true. code_review: true + # Enables agent functionality on draft pull requests. + # Type: boolean, default: true. + include_drafts: false + # List of glob patterns to ignore (files and directories). # Type: array of string, default: []. ignore_patterns: [] From d028f351953d0b950cef609abe394c764718fcac Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Thu, 30 Apr 2026 14:12:01 +0200 Subject: [PATCH 029/490] [runners-spark] Prep shared base for Spark 4 (#38324) --- .../beam/gradle/BeamModulePlugin.groovy | 3 +++ .../spark/job-server/spark_job_server.gradle | 3 +++ runners/spark/spark_runner.gradle | 22 ++++++++++++++----- .../beam/runners/spark/io/SourceRDD.java | 10 ++++----- .../spark/io/SparkUnboundedSource.java | 2 +- .../SparkGroupAlsoByWindowViaWindowSet.java | 6 ++--- ...parkStructuredStreamingPipelineResult.java | 2 +- .../translation/batch/DoFnRunnerFactory.java | 2 +- ...rkStreamingPortablePipelineTranslator.java | 3 ++- .../streaming/ParDoStateUpdateFn.java | 4 ++-- .../StreamingTransformTranslator.java | 2 +- 11 files changed, 39 insertions(+), 20 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 005a8b587804..a99e0de47934 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -649,6 +649,7 @@ class BeamModulePlugin implements Plugin { def solace_version = "10.21.0" def spark2_version = "2.4.8" def spark3_version = "3.5.0" + def spark4_version = "4.0.2" def spotbugs_version = "4.8.3" def testcontainers_version = "1.21.4" // [bomupgrader] determined by: org.apache.arrow:arrow-memory-core, consistent with: google_cloud_platform_libraries_bom @@ -658,6 +659,7 @@ class BeamModulePlugin implements Plugin { // Export Spark versions, so they are defined in a single place only project.ext.spark3_version = spark3_version + project.ext.spark4_version = spark4_version // version for BigQueryMetastore catalog (used by sdks:java:io:iceberg:bqms) // TODO: remove this and download the jar normally when the catalog gets // open-sourced (https://github.com/apache/iceberg/pull/11039) @@ -820,6 +822,7 @@ class BeamModulePlugin implements Plugin { jackson_datatype_jsr310 : "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version", jackson_module_scala_2_11 : "com.fasterxml.jackson.module:jackson-module-scala_2.11:$jackson_version", jackson_module_scala_2_12 : "com.fasterxml.jackson.module:jackson-module-scala_2.12:$jackson_version", + jackson_module_scala_2_13 : "com.fasterxml.jackson.module:jackson-module-scala_2.13:$jackson_version", jamm : 'com.github.jbellis:jamm:0.4.0', jaxb_api : "jakarta.xml.bind:jakarta.xml.bind-api:$jaxb_api_version", jaxb_impl : "com.sun.xml.bind:jaxb-impl:$jaxb_api_version", diff --git a/runners/spark/job-server/spark_job_server.gradle b/runners/spark/job-server/spark_job_server.gradle index 7e2deaf6e395..a35bbea377d5 100644 --- a/runners/spark/job-server/spark_job_server.gradle +++ b/runners/spark/job-server/spark_job_server.gradle @@ -28,7 +28,10 @@ apply plugin: 'application' // we need to set mainClassName before applying shadow plugin mainClassName = "org.apache.beam.runners.spark.SparkJobServerDriver" +def sparkVersion = project.findProperty('spark_version') ?: '' + applyJavaNature( + requireJavaVersion: (sparkVersion.startsWith("4") ? org.gradle.api.JavaVersion.VERSION_17 : null), automaticModuleName: 'org.apache.beam.runners.spark.jobserver', archivesBaseName: project.hasProperty('archives_base_name') ? archives_base_name : archivesBaseName, validateShadowJar: false, diff --git a/runners/spark/spark_runner.gradle b/runners/spark/spark_runner.gradle index 091cfb053f2e..0e77821e533e 100644 --- a/runners/spark/spark_runner.gradle +++ b/runners/spark/spark_runner.gradle @@ -19,8 +19,20 @@ import groovy.json.JsonOutput apply plugin: 'org.apache.beam.module' + +// Numeric version comparison (lexicographic string compare was fragile — e.g. "3.10.0" < "3.5.0"). +def isSparkAtLeast = { String minVersion -> + def parts = spark_version.tokenize('.-').findAll { it.isInteger() }*.toInteger() + def minParts = minVersion.tokenize('.-').findAll { it.isInteger() }*.toInteger() + for (int i = 0; i < Math.min(parts.size(), minParts.size()); i++) { + if (parts[i] != minParts[i]) return parts[i] > minParts[i] + } + return parts.size() >= minParts.size() +} + applyJavaNature( enableStrictDependencies: true, + requireJavaVersion: (isSparkAtLeast("4.0.0") ? org.gradle.api.JavaVersion.VERSION_17 : null), automaticModuleName: 'org.apache.beam.runners.spark', archivesBaseName: (project.hasProperty('archives_base_name') ? archives_base_name : archivesBaseName), exportJavadoc: (project.hasProperty('exportJavadoc') ? exportJavadoc : true), @@ -240,7 +252,7 @@ dependencies { spark.components.each { component -> provided "$component:$spark_version" } - if ("$spark_version" >= "3.5.0") { + if (isSparkAtLeast("3.5.0")) { implementation "org.apache.spark:spark-common-utils_$spark_scala_version:$spark_version" implementation "org.apache.spark:spark-sql-api_$spark_scala_version:$spark_version" } @@ -270,7 +282,7 @@ dependencies { testImplementation library.java.mockito_core testImplementation "org.assertj:assertj-core:3.11.1" testImplementation "org.apache.zookeeper:zookeeper:3.4.11" - if ("$spark_version" >= "3.5.0") { + if (isSparkAtLeast("3.5.0")) { testImplementation "org.apache.spark:spark-common-utils_$spark_scala_version:$spark_version" testImplementation "org.apache.spark:spark-sql-api_$spark_scala_version:$spark_version" } @@ -284,7 +296,7 @@ dependencies { "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-common:$kv.value" // Force paranamer 2.8 to avoid issues when using Scala 2.12 "hadoopVersion$kv.key" "com.thoughtworks.paranamer:paranamer:2.8" - if ("$spark_version" >= "3.5.0") { + if (isSparkAtLeast("3.5.0")) { // Add log4j 2.x dependencies as Spark 3.5+ uses slf4j with log4j 2.x backend "hadoopVersion$kv.key" library.java.log4j2_api "hadoopVersion$kv.key" library.java.log4j2_core @@ -310,7 +322,7 @@ configurations.validatesRunner { // Exclude to make sure log4j binding is used exclude group: "org.slf4j", module: "slf4j-simple" - if ("$spark_version" >= "3.5.0") { + if (isSparkAtLeast("3.5.0")) { // Exclude log4j 1.x dependencies to prevent conflict with log4j 2.x used by spark 3.5+ exclude group: "log4j", module: "log4j" } @@ -321,7 +333,7 @@ hadoopVersions.each { kv -> resolutionStrategy { force "org.apache.hadoop:hadoop-common:$kv.value" } - if ("$spark_version" >= "3.5.0") { + if (isSparkAtLeast("3.5.0")) { // Exclude log4j 1.x dependencies to prevent conflict with log4j 2.x used by spark 3.5+ exclude group: "log4j", module: "log4j" } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SourceRDD.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SourceRDD.java index e65dccd23f24..56a2219933b6 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SourceRDD.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SourceRDD.java @@ -50,7 +50,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.Option; -import scala.collection.JavaConversions; +import scala.collection.JavaConverters; /** Classes implementing Beam {@link Source} {@link RDD}s. */ @SuppressWarnings({ @@ -75,7 +75,7 @@ public static class Bounded extends RDD> { // to satisfy Scala API. private static final scala.collection.immutable.Seq> NIL = - JavaConversions.asScalaBuffer(Collections.>emptyList()).toList(); + JavaConverters.asScalaBuffer(Collections.>emptyList()).toList(); public Bounded( SparkContext sc, @@ -148,7 +148,7 @@ public scala.collection.Iterator> compute( final Iterator> readerIterator = new ReaderToIteratorAdapter<>(metricsContainer, reader); - return new InterruptibleIterator<>(context, JavaConversions.asScalaIterator(readerIterator)); + return new InterruptibleIterator<>(context, JavaConverters.asScalaIterator(readerIterator)); } /** @@ -299,7 +299,7 @@ public static class Unbounded> NIL = - JavaConversions.asScalaBuffer(Collections.>emptyList()).toList(); + JavaConverters.asScalaBuffer(Collections.>emptyList()).toList(); public Unbounded( SparkContext sc, @@ -344,7 +344,7 @@ public scala.collection.Iterator, CheckpointMarkT>> compu (CheckpointableSourcePartition) split; scala.Tuple2, CheckpointMarkT> tuple2 = new scala.Tuple2<>(partition.getSource(), partition.checkpointMark); - return JavaConversions.asScalaIterator(Collections.singleton(tuple2).iterator()); + return JavaConverters.asScalaIterator(Collections.singleton(tuple2).iterator()); } } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SparkUnboundedSource.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SparkUnboundedSource.java index bea1557a7103..3f1fb103e47c 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SparkUnboundedSource.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/io/SparkUnboundedSource.java @@ -186,7 +186,7 @@ public Duration slideDuration() { @Override public scala.collection.immutable.List> dependencies() { - return scala.collection.JavaConversions.asScalaBuffer( + return scala.collection.JavaConverters.asScalaBuffer( Collections.>singletonList(parent)) .toList(); } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java index 2c54f90badbe..1c7a4c2a2416 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java @@ -73,7 +73,7 @@ import scala.Tuple2; import scala.Tuple3; import scala.collection.Iterator; -import scala.collection.JavaConversions; +import scala.collection.JavaConverters; import scala.collection.Seq; import scala.runtime.AbstractFunction1; @@ -238,7 +238,7 @@ private Collection filterTimersEligibleForProcessing( // new input for key. try { final Iterable> elements = - FluentIterable.from(JavaConversions.asJavaIterable(encodedElements)) + FluentIterable.from(JavaConverters.asJavaIterable(encodedElements)) .transform(bytes -> CoderHelpers.fromByteArray(bytes, wvCoder)); LOG.trace("{}: input elements: {}", logPrefix, elements); @@ -410,7 +410,7 @@ private Collection filterTimersEligibleForProcessing( droppedDueToClosedWindow.inc(-droppedDueToClosedWindow.getCumulative()); } - return scala.collection.JavaConversions.asScalaIterator( + return JavaConverters.asScalaIterator( new UpdateStateByKeyOutputIterator(input, reduceFn, droppedDueToLateness)); } } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java index b490ff875c31..806d838d9bff 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java @@ -18,7 +18,7 @@ package org.apache.beam.runners.spark.structuredstreaming; import static org.apache.beam.runners.core.metrics.MetricsContainerStepMap.asAttemptedOnlyMetricResults; -import static org.sparkproject.guava.base.Objects.firstNonNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects.firstNonNull; import java.io.IOException; import java.util.concurrent.ExecutionException; diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerFactory.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerFactory.java index 15ec818dba74..99ce3dc69889 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerFactory.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerFactory.java @@ -17,6 +17,7 @@ */ package org.apache.beam.runners.spark.structuredstreaming.translation.batch; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -49,7 +50,6 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.joda.time.Instant; -import scala.Serializable; /** * Factory to create a {@link DoFnRunner}. The factory supports fusing multiple {@link DoFnRunner diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java index 4850f886241b..9975c81b56a4 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java @@ -330,7 +330,8 @@ private static void translateFlatten( } } // Unify streams into a single stream. - unifiedStreams = context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams)); + unifiedStreams = + context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams).toList()); } context.pushDataset( diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java index 909624c23239..ed9299db4ee7 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -62,7 +63,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sparkproject.guava.collect.Iterators; import scala.Option; import scala.Tuple2; import scala.runtime.AbstractFunction3; @@ -236,7 +236,7 @@ public TimerInternals timerInternals() { final byte[] byteValue = serializedValue.get(); @Nullable WindowedValue windowedValue; @Nullable WindowedValue> keyedWindowedValue; - Iterator>> iterator = Iterators.emptyIterator(); + Iterator>> iterator = Collections.emptyIterator(); if (byteValue.length > 0) { windowedValue = CoderHelpers.fromByteArray(byteValue, this.wvCoder); keyedWindowedValue = windowedValue.withValue(KV.of(key, windowedValue.getValue())); diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java index 48697a3dbafc..4a96edceba31 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java @@ -306,7 +306,7 @@ public void evaluate(Flatten.PCollections transform, EvaluationContext contex } // start by unifying streams into a single stream. JavaDStream> unifiedStreams = - context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams)); + context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams).toList()); context.putDataset(transform, new UnboundedDataset<>(unifiedStreams, streamingSources)); } From 79e49addd312d2c080f541f06e09b020e50bdce8 Mon Sep 17 00:00:00 2001 From: reuvenlax Date: Thu, 30 Apr 2026 05:14:40 -0700 Subject: [PATCH 030/490] fix conversion failures - block was at the wrong nesting level (#38336) --- .../beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java | 6 +++--- .../gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java index 371c77c5f9c1..ba72bb8682fd 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java @@ -1639,9 +1639,9 @@ public static ByteString mergeNewFields( null, null, collectedExceptions); - if (!collectedExceptions.isEmpty()) { - return null; - } + } + if (!collectedExceptions.isEmpty()) { + return null; } } else if (schemaInformation.getType() == TableFieldSchema.Type.TIMESTAMP && schemaInformation.getTimestampPrecision() == PICOSECOND_PRECISION) { diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java index ef390f5a8601..b9ab25ec5810 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java @@ -36,6 +36,7 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition; import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.testing.BigqueryClient; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.state.StateSpec; import org.apache.beam.sdk.state.StateSpecs; import org.apache.beam.sdk.state.ValueState; @@ -187,6 +188,7 @@ public void testDataTriggeredSchemaUpgradeAtLeastOnce() throws Exception { private void runTest(Write.Method method) throws Exception { Pipeline p = Pipeline.create(TestPipeline.testingPipelineOptions()); p.getOptions().as(BigQueryOptions.class).setSchemaUpgradeBufferingShards(1); + p.getOptions().as(StreamingOptions.class).setStreaming(true); TableSchema baseSchema = new TableSchema() From fbbcd97e6eeed9bff66b58c8ddc01ec39ce56a1a Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:40:56 -0400 Subject: [PATCH 031/490] Update managed-io.md for release 2.73.0-RC2. (#38276) Co-authored-by: jrmccluskey --- .../content/en/documentation/io/managed-io.md | 866 +++++++++--------- 1 file changed, 433 insertions(+), 433 deletions(-) diff --git a/website/www/site/content/en/documentation/io/managed-io.md b/website/www/site/content/en/documentation/io/managed-io.md index 0e365eeb23e3..46f20c5e11ab 100644 --- a/website/www/site/content/en/documentation/io/managed-io.md +++ b/website/www/site/content/en/documentation/io/managed-io.md @@ -58,31 +58,6 @@ and Beam SQL is invoked via the Managed API under the hood. Read Configuration Write Configuration - - ICEBERG - - table (str)
    - catalog_name (str)
    - catalog_properties (map[str, str])
    - config_properties (map[str, str])
    - drop (list[str])
    - filter (str)
    - keep (list[str])
    - - - table (str)
    - catalog_name (str)
    - catalog_properties (map[str, str])
    - config_properties (map[str, str])
    - direct_write_byte_limit (int32)
    - drop (list[str])
    - keep (list[str])
    - only (str)
    - partition_fields (list[str])
    - table_properties (map[str, str])
    - triggering_frequency_seconds (int32)
    - - ICEBERG_CDC @@ -134,10 +109,37 @@ and Beam SQL is invoked via the Managed API under the hood. - POSTGRES + ICEBERG + + table (str)
    + catalog_name (str)
    + catalog_properties (map[str, str])
    + config_properties (map[str, str])
    + drop (list[str])
    + filter (str)
    + keep (list[str])
    + + + table (str)
    + catalog_name (str)
    + catalog_properties (map[str, str])
    + config_properties (map[str, str])
    + direct_write_byte_limit (int32)
    + drop (list[str])
    + keep (list[str])
    + only (str)
    + partition_fields (list[str])
    + table_properties (map[str, str])
    + triggering_frequency_seconds (int32)
    + + + + MYSQL jdbc_url (str)
    + connection_init_sql (list[str])
    connection_properties (str)
    + disable_auto_commit (boolean)
    fetch_size (int32)
    location (str)
    num_partitions (int32)
    @@ -151,6 +153,7 @@ and Beam SQL is invoked via the Managed API under the hood. jdbc_url (str)
    autosharding (boolean)
    batch_size (int64)
    + connection_init_sql (list[str])
    connection_properties (str)
    location (str)
    password (str)
    @@ -159,29 +162,10 @@ and Beam SQL is invoked via the Managed API under the hood. - BIGQUERY - - kms_key (str)
    - query (str)
    - row_restriction (str)
    - fields (list[str])
    - table (str)
    - - - table (str)
    - drop (list[str])
    - keep (list[str])
    - kms_key (str)
    - only (str)
    - triggering_frequency_seconds (int64)
    - - - - SQLSERVER + POSTGRES jdbc_url (str)
    connection_properties (str)
    - disable_auto_commit (boolean)
    fetch_size (int32)
    location (str)
    num_partitions (int32)
    @@ -203,10 +187,9 @@ and Beam SQL is invoked via the Managed API under the hood. - MYSQL + SQLSERVER jdbc_url (str)
    - connection_init_sql (list[str])
    connection_properties (str)
    disable_auto_commit (boolean)
    fetch_size (int32)
    @@ -222,7 +205,6 @@ and Beam SQL is invoked via the Managed API under the hood. jdbc_url (str)
    autosharding (boolean)
    batch_size (int64)
    - connection_init_sql (list[str])
    connection_properties (str)
    location (str)
    password (str)
    @@ -230,12 +212,30 @@ and Beam SQL is invoked via the Managed API under the hood. write_statement (str)
    + + BIGQUERY + + kms_key (str)
    + query (str)
    + row_restriction (str)
    + fields (list[str])
    + table (str)
    + + + table (str)
    + drop (list[str])
    + keep (list[str])
    + kms_key (str)
    + only (str)
    + triggering_frequency_seconds (int64)
    + + ## Configuration Details -### `ICEBERG` Read +### `ICEBERG_CDC` Read

    @@ -310,6 +310,28 @@ and Beam SQL is invoked via the Managed API under the hood. SQL-like predicate to filter data at scan time. Example: "id > 5 AND status = 'ACTIVE'". Uses Apache Calcite syntax: https://calcite.apache.org/docs/reference.html + + + + + + + + + + -
    + from_snapshot + + int64 + + Starts reading from this snapshot ID (inclusive). +
    + from_timestamp + + int64 + + Starts reading from the first snapshot (inclusive) that was created after this timestamp (in milliseconds). +
    keep @@ -321,155 +343,154 @@ and Beam SQL is invoked via the Managed API under the hood. A subset of column names to read exclusively. If null or empty, all columns will be read.
    -
    - -### `ICEBERG` Write - -
    - - - - + + + +
    ConfigurationTypeDescription + poll_interval_seconds + + int32 + + The interval at which to poll for new snapshots. Defaults to 60 seconds. +
    - table + starting_strategy str - A fully-qualified table identifier. You may also provide a template to write to multiple dynamic destinations, for example: `dataset.my_{col1}_{col2.nested}_table`. + The source's starting strategy. Valid options are: "earliest" or "latest". Can be overriden by setting a starting snapshot or timestamp. Defaults to earliest for batch, and latest for streaming.
    - catalog_name + streaming - str + boolean - Name of the catalog containing the table. + Enables streaming reads, where source continuously polls for snapshots forever.
    - catalog_properties + to_snapshot - map[str, str] + int64 - Properties used to set up the Iceberg catalog. + Reads up to this snapshot ID (inclusive).
    - config_properties + to_timestamp - map[str, str] + int64 - Properties passed to the Hadoop Configuration. + Reads up to the latest snapshot (inclusive) created before this timestamp (in milliseconds).
    +
    + +### `KAFKA` Write + +
    + + + + + +
    ConfigurationTypeDescription
    - direct_write_byte_limit + bootstrap_servers - int32 + str - For a streaming pipeline, sets the limit for lifting bundles into the direct write path. + A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. | Format: host1:port1,host2:port2,...
    - drop + format - list[str] + str - A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. + The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO
    - keep + topic - list[str] + str - A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'. + n/a
    - only + file_descriptor_path str - The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'. + The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization.
    - partition_fields + message_name - list[str] + str - Fields used to create a partition spec that is applied when tables are created. For a field 'foo', the available partition transforms are: - -- `foo` -- `truncate(foo, N)` -- `bucket(foo, N)` -- `hour(foo)` -- `day(foo)` -- `month(foo)` -- `year(foo)` -- `void(foo)` - -For more information on partition transforms, please visit https://iceberg.apache.org/spec/#partition-transforms. + The name of the Protocol Buffer message to be used for schema extraction and data conversion.
    - table_properties + producer_config_updates map[str, str] - Iceberg table properties to be set on the table when it is created. -For more information on table properties, please visit https://iceberg.apache.org/docs/latest/configuration/#table-properties. + A list of key-value pairs that act as configuration parameters for Kafka producers. Most of these configurations will not be needed, but if you need to customize your Kafka producer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html
    - triggering_frequency_seconds + schema - int32 + str - For a streaming pipeline, sets the frequency at which snapshots are produced. + n/a
    -### `ICEBERG_CDC` Read +### `KAFKA` Read
    @@ -480,162 +501,251 @@ For more information on table properties, please visit https://iceberg.apache.or + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - table + bootstrap_servers str - Identifier of the Iceberg table. + A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. This list should be in the form `host1:port1,host2:port2,...`
    - catalog_name + topic str - Name of the catalog containing the table. + n/a
    - catalog_properties + allow_duplicates - map[str, str] + boolean - Properties used to set up the Iceberg catalog. + If the Kafka read allows duplicates.
    - config_properties + confluent_schema_registry_subject + + str + + n/a +
    + confluent_schema_registry_url + + str + + n/a +
    + consumer_config_updates map[str, str] - Properties passed to the Hadoop Configuration. + A list of key-value pairs that act as configuration parameters for Kafka consumers. Most of these configurations will not be needed, but if you need to customize your Kafka consumer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html
    - drop + file_descriptor_path - list[str] + str - A subset of column names to exclude from reading. If null or empty, all columns will be read. + The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization.
    - filter + format str - SQL-like predicate to filter data at scan time. Example: "id > 5 AND status = 'ACTIVE'". Uses Apache Calcite syntax: https://calcite.apache.org/docs/reference.html + The encoding format for the data stored in Kafka. Valid options are: RAW,STRING,AVRO,JSON,PROTO
    - from_snapshot + message_name - int64 + str - Starts reading from this snapshot ID (inclusive). + The name of the Protocol Buffer message to be used for schema extraction and data conversion.
    - from_timestamp + offset_deduplication - int64 + boolean + + If the redistribute is using offset deduplication mode. +
    + redistribute_by_record_key + + boolean + + If the redistribute keys by the Kafka record key. +
    + redistribute_num_keys + + int32 + + The number of keys for redistributing Kafka inputs. +
    + redistributed + + boolean + + If the Kafka read should be redistributed. +
    + schema + + str + + The schema in which the data is encoded in the Kafka topic. For AVRO data, this is a schema defined with AVRO schema syntax (https://avro.apache.org/docs/1.10.2/spec.html#schemas). For JSON data, this is a schema defined with JSON-schema syntax (https://json-schema.org/). If a URL to Confluent Schema Registry is provided, then this field is ignored, and the schema is fetched from Confluent Schema Registry. +
    +
    + +### `ICEBERG` Read + +
    + + + + + + + + +
    ConfigurationTypeDescription
    + table + + str - Starts reading from the first snapshot (inclusive) that was created after this timestamp (in milliseconds). + Identifier of the Iceberg table.
    - keep + catalog_name - list[str] + str - A subset of column names to read exclusively. If null or empty, all columns will be read. + Name of the catalog containing the table.
    - poll_interval_seconds + catalog_properties - int32 + map[str, str] - The interval at which to poll for new snapshots. Defaults to 60 seconds. + Properties used to set up the Iceberg catalog.
    - starting_strategy + config_properties - str + map[str, str] - The source's starting strategy. Valid options are: "earliest" or "latest". Can be overriden by setting a starting snapshot or timestamp. Defaults to earliest for batch, and latest for streaming. + Properties passed to the Hadoop Configuration.
    - streaming + drop - boolean + list[str] - Enables streaming reads, where source continuously polls for snapshots forever. + A subset of column names to exclude from reading. If null or empty, all columns will be read.
    - to_snapshot + filter - int64 + str - Reads up to this snapshot ID (inclusive). + SQL-like predicate to filter data at scan time. Example: "id > 5 AND status = 'ACTIVE'". Uses Apache Calcite syntax: https://calcite.apache.org/docs/reference.html
    - to_timestamp + keep - int64 + list[str] - Reads up to the latest snapshot (inclusive) created before this timestamp (in milliseconds). + A subset of column names to read exclusively. If null or empty, all columns will be read.
    -### `KAFKA` Write +### `ICEBERG` Write
    @@ -646,251 +756,285 @@ For more information on table properties, please visit https://iceberg.apache.or + + + + + -
    - bootstrap_servers + table str - A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. | Format: host1:port1,host2:port2,... + A fully-qualified table identifier. You may also provide a template to write to multiple dynamic destinations, for example: `dataset.my_{col1}_{col2.nested}_table`.
    - format + catalog_name str - The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO + Name of the catalog containing the table.
    - topic + catalog_properties - str + map[str, str] - n/a + Properties used to set up the Iceberg catalog.
    - file_descriptor_path + config_properties - str + map[str, str] - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + Properties passed to the Hadoop Configuration.
    - message_name + direct_write_byte_limit - str + int32 - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + For a streaming pipeline, sets the limit for lifting bundles into the direct write path.
    - producer_config_updates + drop - map[str, str] + list[str] - A list of key-value pairs that act as configuration parameters for Kafka producers. Most of these configurations will not be needed, but if you need to customize your Kafka producer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html + A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'.
    - schema + keep + + list[str] + + A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'. +
    + only str - n/a + The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'.
    -
    + + + partition_fields + + + list[str] + + + Fields used to create a partition spec that is applied when tables are created. For a field 'foo', the available partition transforms are: -### `KAFKA` Read +- `foo` +- `truncate(foo, N)` +- `bucket(foo, N)` +- `hour(foo)` +- `day(foo)` +- `month(foo)` +- `year(foo)` +- `void(foo)` -
    - - - - - +For more information on partition transforms, please visit https://iceberg.apache.org/spec/#partition-transforms. + +
    ConfigurationTypeDescription
    - bootstrap_servers + table_properties - str + map[str, str] - A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. This list should be in the form `host1:port1,host2:port2,...` + Iceberg table properties to be set on the table when it is created. +For more information on table properties, please visit https://iceberg.apache.org/docs/latest/configuration/#table-properties.
    - topic + triggering_frequency_seconds - str + int32 - n/a + For a streaming pipeline, sets the frequency at which snapshots are produced.
    +
    + +### `MYSQL` Read + +
    + + + + + +
    ConfigurationTypeDescription
    - allow_duplicates + jdbc_url - boolean + str - If the Kafka read allows duplicates. + Connection URL for the JDBC source.
    - confluent_schema_registry_subject + connection_init_sql - str + list[str] - n/a + Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this.
    - confluent_schema_registry_url + connection_properties str - n/a + Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;".
    - consumer_config_updates + disable_auto_commit - map[str, str] + boolean - A list of key-value pairs that act as configuration parameters for Kafka consumers. Most of these configurations will not be needed, but if you need to customize your Kafka consumer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html + Whether to disable auto commit on read. Defaults to true if not provided. The need for this config varies depending on the database platform. Informix requires this to be set to false while Postgres requires this to be set to true.
    - file_descriptor_path + fetch_size - str + int32 - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors.
    - format + location str - The encoding format for the data stored in Kafka. Valid options are: RAW,STRING,AVRO,JSON,PROTO + Name of the table to read from.
    - message_name + num_partitions - str + int32 - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + The number of partitions
    - offset_deduplication + output_parallelization boolean - If the redistribute is using offset deduplication mode. + Whether to reshuffle the resulting PCollection so results are distributed to all workers.
    - redistribute_by_record_key + partition_column - boolean + str - If the redistribute keys by the Kafka record key. + Name of a column of numeric type that will be used for partitioning.
    - redistribute_num_keys + password - int32 + str - The number of keys for redistributing Kafka inputs. + Password for the JDBC source.
    - redistributed + read_query - boolean + str - If the Kafka read should be redistributed. + SQL query used to query the JDBC source.
    - schema + username str - The schema in which the data is encoded in the Kafka topic. For AVRO data, this is a schema defined with AVRO schema syntax (https://avro.apache.org/docs/1.10.2/spec.html#schemas). For JSON data, this is a schema defined with JSON-schema syntax (https://json-schema.org/). If a URL to Confluent Schema Registry is provided, then this field is ignored, and the schema is fetched from Confluent Schema Registry. + Username for the JDBC source.
    -### `POSTGRES` Write +### `MYSQL` Write
    @@ -932,6 +1076,17 @@ For more information on table properties, please visit https://iceberg.apache.or n/a + + + + +
    + connection_init_sql + + list[str] + + Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this. +
    connection_properties @@ -990,7 +1145,7 @@ For more information on table properties, please visit https://iceberg.apache.or
    -### `POSTGRES` Read +### `POSTGRES` Write
    @@ -1007,73 +1162,51 @@ For more information on table properties, please visit https://iceberg.apache.or str - - - - - - - - - - @@ -1089,30 +1222,30 @@ For more information on table properties, please visit https://iceberg.apache.or
    - Connection URL for the JDBC source. -
    - connection_properties - - str - - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". -
    - fetch_size - - int32 - - This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors. + Connection URL for the JDBC sink.
    - location + autosharding - str + boolean - Name of the table to read from. + If true, enables using a dynamically determined number of shards to write.
    - num_partitions + batch_size - int32 + int64 - The number of partitions + n/a
    - output_parallelization + connection_properties - boolean + str - Whether to reshuffle the resulting PCollection so results are distributed to all workers. + Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;".
    - partition_column + location str - Name of a column of numeric type that will be used for partitioning. + Name of the table to write to.
    - read_query + username str - SQL query used to query the JDBC source. + Username for the JDBC source.
    - username + write_statement str - Username for the JDBC source. + SQL query used to insert records into the JDBC sink.
    -### `BIGQUERY` Read +### `POSTGRES` Read
    @@ -1123,135 +1256,112 @@ For more information on table properties, please visit https://iceberg.apache.or - - - - - -
    - kms_key - - str - - Use this Cloud KMS key to encrypt your data -
    - query + jdbc_url str - The SQL query to be executed to read from the BigQuery table. + Connection URL for the JDBC source.
    - row_restriction + connection_properties str - Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query. + Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;".
    - fields + fetch_size - list[str] + int32 - Read only the specified fields (columns) from a BigQuery table. Fields may not be returned in the order specified. If no value is specified, then all fields are returned. Example: "col1, col2, col3" + This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors.
    - table + location str - The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE} + Name of the table to read from.
    -
    - -### `BIGQUERY` Write - -
    - - - - - -
    ConfigurationTypeDescription
    - table + num_partitions - str + int32 - The bigquery table to write to. Format: [${PROJECT}:]${DATASET}.${TABLE} + The number of partitions
    - drop + output_parallelization - list[str] + boolean - A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. + Whether to reshuffle the resulting PCollection so results are distributed to all workers.
    - keep + partition_column - list[str] + str - A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'. + Name of a column of numeric type that will be used for partitioning.
    - kms_key + password str - Use this Cloud KMS key to encrypt your data + Password for the JDBC source.
    - only + read_query str - The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'. + SQL query used to query the JDBC source.
    - triggering_frequency_seconds + username - int64 + str - Determines how often to 'commit' progress into BigQuery. Default is every 5 seconds. + Username for the JDBC source.
    @@ -1490,7 +1600,7 @@ For more information on table properties, please visit https://iceberg.apache.or
    -### `MYSQL` Read +### `BIGQUERY` Read
    @@ -1501,140 +1611,63 @@ For more information on table properties, please visit https://iceberg.apache.or - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - jdbc_url - - str - - Connection URL for the JDBC source. -
    - connection_init_sql - - list[str] - - Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this. -
    - connection_properties - - str - - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". -
    - disable_auto_commit - - boolean - - Whether to disable auto commit on read. Defaults to true if not provided. The need for this config varies depending on the database platform. Informix requires this to be set to false while Postgres requires this to be set to true. -
    - fetch_size - - int32 - - This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors. -
    - location + kms_key str - Name of the table to read from. -
    - num_partitions - - int32 - - The number of partitions -
    - output_parallelization - - boolean - - Whether to reshuffle the resulting PCollection so results are distributed to all workers. + Use this Cloud KMS key to encrypt your data
    - partition_column + query str - Name of a column of numeric type that will be used for partitioning. + The SQL query to be executed to read from the BigQuery table.
    - password + row_restriction str - Password for the JDBC source. + Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query.
    - read_query + fields - str + list[str] - SQL query used to query the JDBC source. + Read only the specified fields (columns) from a BigQuery table. Fields may not be returned in the order specified. If no value is specified, then all fields are returned. Example: "col1, col2, col3"
    - username + table str - Username for the JDBC source. + The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE}
    -### `MYSQL` Write +### `BIGQUERY` Write
    @@ -1645,101 +1678,68 @@ For more information on table properties, please visit https://iceberg.apache.or - - - - - - - - - - - - - - -
    - jdbc_url + table str - Connection URL for the JDBC sink. -
    - autosharding - - boolean - - If true, enables using a dynamically determined number of shards to write. -
    - batch_size - - int64 - - n/a + The bigquery table to write to. Format: [${PROJECT}:]${DATASET}.${TABLE}
    - connection_init_sql + drop list[str] - Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this. -
    - connection_properties - - str - - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". + A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'.
    - location + keep - str + list[str] - Name of the table to write to. + A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'.
    - password + kms_key str - Password for the JDBC source. + Use this Cloud KMS key to encrypt your data
    - username + only str - Username for the JDBC source. + The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'.
    - write_statement + triggering_frequency_seconds - str + int64 - SQL query used to insert records into the JDBC sink. + Determines how often to 'commit' progress into BigQuery. Default is every 5 seconds.
    From 9a476d46587d3d52bfb8ada32a3bd045d54d7876 Mon Sep 17 00:00:00 2001 From: Amar3tto Date: Thu, 30 Apr 2026 14:49:14 +0000 Subject: [PATCH 032/490] Adding release-2.73.0-postrelease to protected branches in .asf.yaml --- .asf.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.asf.yaml b/.asf.yaml index 79f1d932b8da..e22c1d6c1d66 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -51,6 +51,7 @@ github: protected_branches: master: {} + release-2.73.0-postrelease: {} release-2.73: {} release-2.72.0-postrelease: {} release-2.72: {} From 762845321ecb6b9b9f27f4fa6a745052267599f1 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 17 Apr 2026 00:00:57 +0400 Subject: [PATCH 033/490] Update Beam website to release 2.73.0 --- website/www/site/config.toml | 2 +- .../www/site/content/en/blog/beam-2.73.0.md | 65 +++++++++++++++++++ .../site/content/en/get-started/downloads.md | 16 +++-- 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 website/www/site/content/en/blog/beam-2.73.0.md diff --git a/website/www/site/config.toml b/website/www/site/config.toml index 777eddc83ee0..6b171bf63284 100644 --- a/website/www/site/config.toml +++ b/website/www/site/config.toml @@ -104,7 +104,7 @@ github_project_repo = "https://github.com/apache/beam" [params] description = "Apache Beam is an open source, unified model and set of language-specific SDKs for defining and executing data processing workflows, and also data ingestion and integration flows, supporting Enterprise Integration Patterns (EIPs) and Domain Specific Languages (DSLs). Dataflow pipelines simplify the mechanics of large-scale batch and streaming data processing and can run on a number of runtimes like Apache Flink, Apache Spark, and Google Cloud Dataflow (a cloud service). Beam also brings DSL in different languages, allowing users to easily implement their data integration processes." -release_latest = "2.72.0" +release_latest = "2.73.0" # The repository and branch where the files live in Github or Colab. This is used # to serve and stage from your local branch, but publish to the master branch. # e.g. https://github.com/{{< param branch_repo >}}/path/to/notebook.ipynb diff --git a/website/www/site/content/en/blog/beam-2.73.0.md b/website/www/site/content/en/blog/beam-2.73.0.md new file mode 100644 index 000000000000..4255ef563340 --- /dev/null +++ b/website/www/site/content/en/blog/beam-2.73.0.md @@ -0,0 +1,65 @@ +--- +title: "Apache Beam 2.73.0" +date: 2026-04-?? 9:00:00 -0700 +categories: + - blog + - release +authors: + - vterentev +--- + + +We are happy to present the new 2.73.0 release of Beam. +This release includes both improvements and new functionality. +See the [download page](/get-started/downloads/#2730-2026-04-??) for this release. + + + +For more information on changes in 2.73.0, check out the [detailed release notes](https://github.com/apache/beam/milestone/39). + +## Highlights + +### I/Os + +* DebeziumIO (Java): added `OffsetRetainer` interface and `FileSystemOffsetRetainer` implementation to persist and restore CDC offsets across pipeline restarts, and exposed `withStartOffset` / `withOffsetRetainer` on `DebeziumIO.Read` and the cross-language `ReadBuilder` ([#28248](https://github.com/apache/beam/issues/28248)). + +### New Features / Improvements + +* Added `ADKAgentModelHandler` for running Google Agent Development Kit (ADK) agents (Python) ([#37917](https://github.com/apache/beam/issues/37917)). +* (Python) Added exception chaining to preserve error context in CloudSQLEnrichmentHandler, processes utilities, and core transforms ([#37422](https://github.com/apache/beam/issues/37422)). +* (Python) Added a pipeline option `--experiments=pip_no_build_isolation` to disable build isolation when installing dependencies in the runtime environment ([#37331](https://github.com/apache/beam/issues/37331)). +* (Go) Added OrderedListState support to the Go SDK stateful DoFn API ([#37629](https://github.com/apache/beam/issues/37629)). +* Added support for large pipeline options via a file (Python) ([#37370](https://github.com/apache/beam/issues/37370)). +* Supported infer schema from dataclass (Python) ([#22085](https://github.com/apache/beam/issues/22085)). Default coder for typehint-ed (or set with_output_type) for non-frozen dataclasses changed to RowCoder. To preserve the old behavior (fast primitive coder), explicitly register the type with FastPrimitiveCoder. +* Updates minimum Go version to 1.26.1 ([#37897](https://github.com/apache/beam/issues/37897)). +* (Python) Added image embedding support in `apache_beam.ml.rag` package ([#37628](https://github.com/apache/beam/issues/37628)). +* (Python) Added support for Python version 3.14 ([#37247](https://github.com/apache/beam/issues/37247)). + +### Breaking Changes + +* The Python SDK container's `boot.go` now passes pipeline options through a file instead of the `PIPELINE_OPTIONS` environment variable. If a user pairs a new Python SDK container with an older SDK version (which does not support the file-based approach), the pipeline options will not be recognized and the pipeline will fail. Users must ensure their SDK and container versions are synchronized ([#37370](https://github.com/apache/beam/issues/37370)). + +### Bugfixes + +* Fixed ProcessManager not reaping child processes, causing zombie process accumulation on long-running Flink deployments (Java) ([#37930](https://github.com/apache/beam/issues/37930)). + +### Security Fixes + +* Fixed [CVE-2023-46604](https://www.cve.org/CVERecord?id=CVE-2023-46604) (CVSS 10.0) and [CVE-2022-41678](https://www.cve.org/CVERecord?id=CVE-2022-41678) by upgrading ActiveMQ from 5.14.5 to 5.19.2 (Java) ([#37943](https://github.com/apache/beam/issues/37943)). +* Fixed [CVE-2024-1597](https://www.cve.org/CVERecord?id=CVE-2024-1597), [CVE-2022-31197](https://www.cve.org/CVERecord?id=CVE-2022-31197), and [CVE-2022-21724](https://www.cve.org/CVERecord?id=CVE-2022-21724) by upgrading PostgreSQL JDBC Driver from 42.2.16 to 42.6.2 (Java) ([#37942](https://github.com/apache/beam/issues/37942)). + +## List of Contributors + +According to git shortlog, the following people contributed to the 2.73.0 release. Thank you to all contributors! + +Abdelrahman Ibrahim, Ahmed Abualsaud, Alex Malao, Alexander Nieuwenhuijse, Andres Tiko, Andrew Crites, Arun Pandian, Bentsi Leviav, Bruno Volpato, Chamikara Jayalath, Chandra Kiran Bolla, Danny McCormick, Deji Ibrahim, Derrick Williams, Elia LIU, Esmelealem, Hannes Gustafsson, Jack McCluskey, Joey Tran, Kenneth Knowles, M Junaid Shaukat, Mansi Singh, Matej Aleksandrov, Mathijs Deelen, Mattie Fu, Praneet Nadella, Radek Stankiewicz, Radosław Stankiewicz, Reuven Lax, RuiLong J., S. Veyrié, Sakthivel Subramanian, Sam Whittle, Shubham Thakur, Shunping Huang, Subramanya V, Tarun Annapareddy, Tobias Kaymak, Valentyn Tymofieiev, Vitaly Terentyev, XQ Hu, Yi Hu, ZIHAN DAI, claudevdm, kishorepola, parveensania diff --git a/website/www/site/content/en/get-started/downloads.md b/website/www/site/content/en/get-started/downloads.md index cf37974973fe..7063903bde9e 100644 --- a/website/www/site/content/en/get-started/downloads.md +++ b/website/www/site/content/en/get-started/downloads.md @@ -95,16 +95,24 @@ versions denoted `0.x.y`. ### Current release -#### 2.72.0 (2026-03-30) +#### 2.73.0 (2026-04-??) -Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.72.0/apache-beam-2.72.0-source-release.zip). -[SHA-512](https://downloads.apache.org/beam/2.72.0/apache-beam-2.72.0-source-release.zip.sha512). -[signature](https://downloads.apache.org/beam/2.72.0/apache-beam-2.72.0-source-release.zip.asc). +Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.73.0/apache-beam-2.73.0-source-release.zip). +[SHA-512](https://downloads.apache.org/beam/2.73.0/apache-beam-2.73.0-source-release.zip.sha512). +[signature](https://downloads.apache.org/beam/2.73.0/apache-beam-2.73.0-source-release.zip.asc). [Release notes](https://github.com/apache/beam/releases/tag/v2.71.0) ### Archived releases +#### 2.72.0 (2026-03-30) + +Official [source code download](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip). +[SHA-512](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip.sha512). +[signature](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip.asc). + +[Release notes](https://github.com/apache/beam/releases/tag/v2.70.0) + #### 2.71.0 (2026-01-22) Official [source code download](https://archive.apache.org/dist/beam/2.71.0/apache-beam-2.71.0-source-release.zip). From f71b3995d18c7f3f8453309323ba5e9418344680 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Wed, 29 Apr 2026 16:49:13 +0400 Subject: [PATCH 034/490] Update release date --- CHANGES.md | 2 +- website/www/site/content/en/blog/beam-2.72.0.md | 2 +- website/www/site/content/en/blog/beam-2.73.0.md | 6 +++--- website/www/site/content/en/get-started/downloads.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e8e11e830d14..7b4671326049 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -100,7 +100,7 @@ [comment]: # ( When updating known issues after release, make sure also update website blog in website/www/site/content/blog.) * ([#X](https://github.com/apache/beam/issues/X)). -# [2.73.0] - 2026-04-?? +# [2.73.0] - 2026-04-29 ## Highlights diff --git a/website/www/site/content/en/blog/beam-2.72.0.md b/website/www/site/content/en/blog/beam-2.72.0.md index 63b6c2d6091f..c479f8debe74 100644 --- a/website/www/site/content/en/blog/beam-2.72.0.md +++ b/website/www/site/content/en/blog/beam-2.72.0.md @@ -25,7 +25,7 @@ See the [download page](/get-started/downloads/#2720-2026-03-30) for this releas -For more information on changes in 2.72.0, check out the [detailed release notes](https://github.com/apache/beam/milestone/39). +For more information on changes in 2.72.0, check out the [detailed release notes](https://github.com/apache/beam/milestone/40). ## Highlights diff --git a/website/www/site/content/en/blog/beam-2.73.0.md b/website/www/site/content/en/blog/beam-2.73.0.md index 4255ef563340..754d8b4b081e 100644 --- a/website/www/site/content/en/blog/beam-2.73.0.md +++ b/website/www/site/content/en/blog/beam-2.73.0.md @@ -1,6 +1,6 @@ --- title: "Apache Beam 2.73.0" -date: 2026-04-?? 9:00:00 -0700 +date: 2026-04-29 9:00:00 -0700 categories: - blog - release @@ -21,11 +21,11 @@ limitations under the License. We are happy to present the new 2.73.0 release of Beam. This release includes both improvements and new functionality. -See the [download page](/get-started/downloads/#2730-2026-04-??) for this release. +See the [download page](/get-started/downloads/#2730-2026-04-29) for this release. -For more information on changes in 2.73.0, check out the [detailed release notes](https://github.com/apache/beam/milestone/39). +For more information on changes in 2.73.0, check out the [detailed release notes](https://github.com/apache/beam/milestone/41). ## Highlights diff --git a/website/www/site/content/en/get-started/downloads.md b/website/www/site/content/en/get-started/downloads.md index 7063903bde9e..c3a3eff7f533 100644 --- a/website/www/site/content/en/get-started/downloads.md +++ b/website/www/site/content/en/get-started/downloads.md @@ -95,7 +95,7 @@ versions denoted `0.x.y`. ### Current release -#### 2.73.0 (2026-04-??) +#### 2.73.0 (2026-04-29) Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.73.0/apache-beam-2.73.0-source-release.zip). [SHA-512](https://downloads.apache.org/beam/2.73.0/apache-beam-2.73.0-source-release.zip.sha512). From 3694c7f3f79a2c22e2e33a283c108373048ae12b Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Thu, 30 Apr 2026 00:21:44 +0400 Subject: [PATCH 035/490] Update changes --- website/www/site/content/en/blog/beam-2.73.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/www/site/content/en/blog/beam-2.73.0.md b/website/www/site/content/en/blog/beam-2.73.0.md index 754d8b4b081e..c8ca78b513c8 100644 --- a/website/www/site/content/en/blog/beam-2.73.0.md +++ b/website/www/site/content/en/blog/beam-2.73.0.md @@ -35,6 +35,7 @@ For more information on changes in 2.73.0, check out the [detailed release notes ### New Features / Improvements +* (Python) Added BigQuery CDC streaming source ([#37724](https://github.com/apache/beam/issues/37724)) * Added `ADKAgentModelHandler` for running Google Agent Development Kit (ADK) agents (Python) ([#37917](https://github.com/apache/beam/issues/37917)). * (Python) Added exception chaining to preserve error context in CloudSQLEnrichmentHandler, processes utilities, and core transforms ([#37422](https://github.com/apache/beam/issues/37422)). * (Python) Added a pipeline option `--experiments=pip_no_build_isolation` to disable build isolation when installing dependencies in the runtime environment ([#37331](https://github.com/apache/beam/issues/37331)). @@ -48,6 +49,7 @@ For more information on changes in 2.73.0, check out the [detailed release notes ### Breaking Changes * The Python SDK container's `boot.go` now passes pipeline options through a file instead of the `PIPELINE_OPTIONS` environment variable. If a user pairs a new Python SDK container with an older SDK version (which does not support the file-based approach), the pipeline options will not be recognized and the pipeline will fail. Users must ensure their SDK and container versions are synchronized ([#37370](https://github.com/apache/beam/issues/37370)). +* Python DoFn.with_exception_handling now respects user DoFn typehints. This can break update compatibility if coders change. It can also break pipeline compilation if existing typehints are incorrect. To update safely sepcify the pipeline option `--update_compatibility_version=2.72.0`. To fix typehints replace any incorrect typehints that were previously ignored ([#37590](https://github.com/apache/beam/issues/37590)) ### Bugfixes From 80a7686b012d3d0e6e90fecdbe928e70fdeffe70 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 30 Apr 2026 10:57:47 -0400 Subject: [PATCH 036/490] Revert "fix preCommit Spotless rsync install (#38160)" (#38196) This reverts commit dfe6bf351fdb6485784900f8eebd5a09acf6191b. --- .github/workflows/beam_PreCommit_Spotless.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/beam_PreCommit_Spotless.yml b/.github/workflows/beam_PreCommit_Spotless.yml index 06619d83f142..c9a58c900806 100644 --- a/.github/workflows/beam_PreCommit_Spotless.yml +++ b/.github/workflows/beam_PreCommit_Spotless.yml @@ -85,8 +85,6 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - - name: Install rsync - run: sudo apt-get update && sudo apt-get install -y rsync - name: run Spotless PreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: From 5d63a6b0c63c9118b7c4f6d55087119e4b1f751b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:28:19 -0400 Subject: [PATCH 037/490] Bump pytest from 8.4.2 to 9.0.3 in /sdks/python/container/ml/py311 (#38177) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/ml/py311/base_image_requirements.txt | 2 +- sdks/python/container/ml/py311/gpu_image_requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/python/container/ml/py311/base_image_requirements.txt b/sdks/python/container/ml/py311/base_image_requirements.txt index 4fe9cd55aaff..27025b1130e3 100644 --- a/sdks/python/container/ml/py311/base_image_requirements.txt +++ b/sdks/python/container/ml/py311/base_image_requirements.txt @@ -176,7 +176,7 @@ pymongo==4.16.0 PyMySQL==1.1.2 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 diff --git a/sdks/python/container/ml/py311/gpu_image_requirements.txt b/sdks/python/container/ml/py311/gpu_image_requirements.txt index 17fe5025f09b..01caaf42565a 100644 --- a/sdks/python/container/ml/py311/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py311/gpu_image_requirements.txt @@ -233,7 +233,7 @@ pymongo==4.16.0 PyMySQL==1.1.2 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 From f12dfabf00241dfbfc4e2ce4d1ef97168a6d53a4 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 30 Apr 2026 11:31:01 -0400 Subject: [PATCH 038/490] Do not set representsation for pythonsdk_any type when typehint is Any (#38325) * Do not set representsation for pythonsdk_any type when typehint is Any * fix formatting --- sdks/python/apache_beam/typehints/schemas.py | 62 +++++++++++-------- .../apache_beam/typehints/schemas_test.py | 19 +++++- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/sdks/python/apache_beam/typehints/schemas.py b/sdks/python/apache_beam/typehints/schemas.py index eb2990d4222e..2a7576946833 100644 --- a/sdks/python/apache_beam/typehints/schemas.py +++ b/sdks/python/apache_beam/typehints/schemas.py @@ -258,34 +258,38 @@ def schema_field( description=description) -def _python_any_schema_pb2(): +def _python_any_schema_pb2(has_repr): # A portable schema matches FastPrimitivesCoder encoded values + if has_repr: + representation = schema_pb2.FieldType( + nullable=False, + row_type=schema_pb2.RowType( + schema=schema_pb2.Schema( + fields=[ + schema_pb2.Field( + name=_PYTHON_ANY_FIELD_TYPE_BYTE, + type=schema_pb2.FieldType( + atomic_type=schema_pb2.BYTE, nullable=False)), + schema_pb2.Field( + name=_PYTHON_ANY_FIELD_PAYLOAD, + type=schema_pb2.FieldType( + atomic_type=schema_pb2.BYTES, nullable=False)) + ], + options=[ + schema_pb2.Option( + name=_SCHEMA_OPTION_STATIC_ENCODING, + type=schema_pb2.FieldType( + atomic_type=schema_pb2.BOOLEAN), + value=schema_pb2.FieldValue( + atomic_value=schema_pb2.AtomicTypeValue( + boolean=True))) + ]))) if has_repr else None + else: + representation = None + return schema_pb2.FieldType( logical_type=schema_pb2.LogicalType( - urn=PYTHON_ANY_URN, - representation=schema_pb2.FieldType( - nullable=False, - row_type=schema_pb2.RowType( - schema=schema_pb2.Schema( - fields=[ - schema_pb2.Field( - name=_PYTHON_ANY_FIELD_TYPE_BYTE, - type=schema_pb2.FieldType( - atomic_type=schema_pb2.BYTE, nullable=False)), - schema_pb2.Field( - name=_PYTHON_ANY_FIELD_PAYLOAD, - type=schema_pb2.FieldType( - atomic_type=schema_pb2.BYTES, nullable=False)) - ], - options=[ - schema_pb2.Option( - name=_SCHEMA_OPTION_STATIC_ENCODING, - type=schema_pb2.FieldType( - atomic_type=schema_pb2.BOOLEAN), - value=schema_pb2.FieldValue( - atomic_value=schema_pb2.AtomicTypeValue( - boolean=True))) - ])))), + urn=PYTHON_ANY_URN, representation=representation), nullable=True) @@ -388,14 +392,18 @@ def typing_to_runner_api(self, type_: type) -> schema_pb2.FieldType: return schema_pb2.FieldType( array_type=schema_pb2.ArrayType(element_type=element_type)) + elif type_ == Any: + return _python_any_schema_pb2(has_repr=False) + try: if LogicalType.is_known_logical_type(type_): logical_type = type_ else: logical_type = LogicalType.from_typing(type_) except ValueError: - # Unknown type, just treat it like Any - return _python_any_schema_pb2() + # Unknown type, use pythonsdk_any with a representation compatible with + # FastPrimitiveCoder encoded UNKNOWN type + return _python_any_schema_pb2(has_repr=True) else: argument_type = None argument = None diff --git a/sdks/python/apache_beam/typehints/schemas_test.py b/sdks/python/apache_beam/typehints/schemas_test.py index 8032e4701c25..3a6d81fab5b9 100644 --- a/sdks/python/apache_beam/typehints/schemas_test.py +++ b/sdks/python/apache_beam/typehints/schemas_test.py @@ -582,9 +582,26 @@ def test_proto_survives_typing_roundtrip(self, fieldtype_proto): fieldtype_proto, schema_registry=SchemaTypeRegistry()), schema_registry=SchemaTypeRegistry())) + def test_any_maps_to_any(self): + # python_any for typing.Any logical type's representation is delibrately set + # absent to prevent the usage crossing language boundary, as its encoded + # form isn't predictable from foreign SDK. + self.assertEqual( + typing_to_runner_api(Any), + schemas._python_any_schema_pb2(has_repr=False)) + def test_unknown_primitive_maps_to_any(self): self.assertEqual( - typing_to_runner_api(np.uint32), schemas._python_any_schema_pb2()) + typing_to_runner_api(np.uint32), + schemas._python_any_schema_pb2(has_repr=True)) + + def test_unknown_user_type_maps_to_any(self): + class MyUnknownType: + pass + + self.assertEqual( + typing_to_runner_api(MyUnknownType), + schemas._python_any_schema_pb2(has_repr=True)) def test_unknown_atomic_raise_valueerror(self): self.assertRaises( From b946b7665b2b37771e74a60ca5ddbb46c91f1845 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:32:31 -0400 Subject: [PATCH 039/490] Bump pytest from 8.4.2 to 9.0.3 in /sdks/python/container/py311 (#38174) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/py311/base_image_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/container/py311/base_image_requirements.txt b/sdks/python/container/py311/base_image_requirements.txt index bfc365e80dc8..c7bb3fd7280a 100644 --- a/sdks/python/container/py311/base_image_requirements.txt +++ b/sdks/python/container/py311/base_image_requirements.txt @@ -156,7 +156,7 @@ pymongo==4.16.0 PyMySQL==1.1.2 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 From 64f95da8423e574a1d9e633a3963f9f27af6a477 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 30 Apr 2026 12:53:12 -0400 Subject: [PATCH 040/490] Ignore container changes from dependabot (#38344) --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7a40726ed9b..c1dc085df00d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,6 +23,8 @@ updates: directory: "/sdks/python" # Location of package manifests schedule: interval: "daily" + exclude-paths: + - "/sdks/python/container/**" - package-ecosystem: "gradle" directory: "/" # Location of package manifests schedule: From 0b5edd0d5a63a79f58a6b2047fac8155d2280701 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 30 Apr 2026 16:13:27 -0400 Subject: [PATCH 041/490] Clean up remaining references to Samza runner (#38326) * Clean up remaining references to Samza runner * Fix compatibility matrix ordering * Revert javascript change --- ineffective --- .agent/skills/adding-new-metadata/SKILL.md | 3 +- .agent/skills/runners/SKILL.md | 1 - .../github_runs_prefetcher/code/config.yaml | 7 +- .../runner-concepts/description.md | 97 ------ release/src/main/scripts/jenkins_jobs.txt | 115 -------- release/src/main/scripts/mass_comment.py | 176 ----------- scripts/beam-sql.sh | 5 - .../ci/release/comment_pr_trigger_phrases.sh | 27 -- .../resources/jenkins_trigger_phrases.txt | 19 -- .../release/test/resources/mass_comment.txt | 106 ------- website/www/site/data/capability_matrix.yaml | 275 +++++------------- .../partials/section-menu/en/roadmap.html | 1 - 12 files changed, 74 insertions(+), 758 deletions(-) delete mode 100644 release/src/main/scripts/jenkins_jobs.txt delete mode 100644 release/src/main/scripts/mass_comment.py delete mode 100755 scripts/ci/release/comment_pr_trigger_phrases.sh delete mode 100644 scripts/ci/release/test/resources/jenkins_trigger_phrases.txt delete mode 100644 scripts/ci/release/test/resources/mass_comment.txt diff --git a/.agent/skills/adding-new-metadata/SKILL.md b/.agent/skills/adding-new-metadata/SKILL.md index 72b061f1df82..fd050d8c7fdb 100644 --- a/.agent/skills/adding-new-metadata/SKILL.md +++ b/.agent/skills/adding-new-metadata/SKILL.md @@ -88,7 +88,6 @@ You must ensure that when a DoFn processes an element and outputs a new element, ### Timers If metadata needs to survive timer firings (e.g., knowing an `@OnTimer` fired because of a system drain), it must be added to Timer data structures. This is a bit of uncharted area which was only implemented for CausedByDrain metadata that comes from backend, not from persisted metadata. In order to persist all WindowedValue metadata across timer, more work has to be done, below are some pointers: * `runners/core-java/src/main/java/org/apache/beam/runners/core/TimerInternals.java` and implementations (e.g., `WindmillTimerInternals.java` in Dataflow). -* `runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerData.java` (or generic `TimerData`). * **Action:** Add the field to `TimerData`, next to `CausedByDrain`. Propagate it when setting the timer and expose it when the timer fires so it bubbles up. * Eventually, metadata from Timer lands in WindowedValue, so it can be exposed to users. Keep field names, types, and getters similar to WindowedValue as much as possible, as common interface may be introduced eventually. @@ -116,4 +115,4 @@ User needs to access the metadata in their `DoFn` (e.g., `@ProcessElement public 9. [ ] Update `ReduceFnRunner` and `OutputAndTimeBoundedSplittableProcessElementInvoker` for complex transform propagation. 10. [ ] If required by timers, update `TimerData` and `TimerInternals`. 11. [ ] If exposed to the user, update `DoFnSignatures` and `ByteBuddyDoFnInvokerFactory`. -12. [ ] Update other runners (Flink, Spark, Samza) to ensure they propagate the new `WindowedValue` fields correctly in their specific operators/runners. +12. [ ] Update other runners (Flink, Spark) to ensure they propagate the new `WindowedValue` fields correctly in their specific operators/runners. diff --git a/.agent/skills/runners/SKILL.md b/.agent/skills/runners/SKILL.md index f92943ab097c..b5e25f9898ee 100644 --- a/.agent/skills/runners/SKILL.md +++ b/.agent/skills/runners/SKILL.md @@ -34,7 +34,6 @@ Runners execute Beam pipelines on distributed processing backends. Each runner t | Dataflow | `runners/google-cloud-dataflow-java/` | Google Cloud Dataflow | | Flink | `runners/flink/` | Apache Flink | | Spark | `runners/spark/` | Apache Spark | -| Samza | `runners/samza/` | Apache Samza | | Jet | `runners/jet/` | Hazelcast Jet | | Twister2 | `runners/twister2/` | Twister2 | diff --git a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml index 3b4038bae64c..154bd1fe8884 100644 --- a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml +++ b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml @@ -76,7 +76,6 @@ categories: - "PostCommit Python ValidatesRunner Dataflow" - "PostCommit Python ValidatesRunner Spark" - "PostCommit Python ValidatesRunner Flink" - - "PostCommit Python ValidatesRunner Samza" - "Build python source distribution and wheels" - "Java Tests" - "PostCommit Java" @@ -113,7 +112,6 @@ categories: - "PreCommit Java Kafka IO Direct" - "PostCommit Java Examples Direct" - "PreCommit Java JDBC IO Direct" - - "PostCommit Java ValidatesRunner Samza" - "PreCommit Java Mqtt IO Direct" - "PreCommit Java Kinesis IO Direct" - "PreCommit Java MongoDb IO Direct" @@ -138,7 +136,6 @@ categories: - "PreCommit Java Thrift IO Direct" - "PreCommit Java Snowflake IO Direct" - "PreCommit Java Solr IO Direct" - - "PostCommit Java PVR Samza" - "PreCommit Java Tika IO Direct" - "PostCommit Java SingleStoreIO IT" - "PostCommit Java ValidatesRunner Direct" @@ -209,7 +206,6 @@ categories: - "PerformanceTests BigQueryIO Batch Java Json" - "PerformanceTests SQLBigQueryIO Batch Java" - "PerformanceTests XmlIOIT" - - "PostCommit XVR Samza" - "PerformanceTests ManyFiles TextIOIT" - "PerformanceTests XmlIOIT HDFS" - "PerformanceTests ParquetIOIT" @@ -291,8 +287,7 @@ categories: tests: - "PerformanceTests MongoDBIO IT" - "PreCommit GoPortable" - - "PreCommit GoPrism" - - "PostCommit Go VR Samza" + - "PreCommit GoPrism" - "PostCommit Go Dataflow ARM" - "LoadTests Go CoGBK Dataflow Batch" - "LoadTests Go Combine Dataflow Batch" diff --git a/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md b/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md index 31743b29b8cd..2ee57e07f990 100644 --- a/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md +++ b/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md @@ -390,103 +390,6 @@ python -m apache_beam.examples.wordcount --input /path/to/inputfile \ ``` {{end}} -{{if (eq .Sdk "java" "go")}} -### Samza runner - -The Apache Samza Runner can be used to execute Beam pipelines using Apache Samza. The Samza Runner executes Beam pipeline in a Samza application and can run locally. The application can further be built into a .tgz file, and deployed to a YARN cluster or Samza standalone cluster with Zookeeper. - -The Samza Runner and Samza are suitable for large scale, stateful streaming jobs, and provide: - -* First class support for local state (with RocksDB store). This allows fast state access for high frequency streaming jobs. -* Fault-tolerance with support for incremental checkpointing of state instead of full snapshots. This enables Samza to scale to applications with very large state. -* A fully asynchronous processing engine that makes remote calls efficient. -* Flexible deployment model for running the applications in any hosting environment with Zookeeper. -* Features like canaries, upgrades and rollbacks that support extremely large deployments with minimal downtime. - -Additionally, you can read more about the Samza Runner [here](https://beam.apache.org/documentation/runners/samza/) - -#### Run example -{{end}} - -{{if (eq .Sdk "go")}} - -Need import: -``` -"github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" -``` - -It is necessary to give an endpoint where the runner is raised with `--endpoint`: -``` -$ go install github.com/apache/beam/sdks/v2/go/examples/wordcount -# As part of the initial setup, for non linux users - install package unix before run -$ go get -u golang.org/x/sys/unix -$ wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ ---output gs:///counts \ ---runner samza \ ---project your-gcp-project \ ---region your-gcp-region \ ---temp_location gs:///tmp/ \ ---staging_location gs:///binaries/ \ ---worker_harness_container_image=apache/beam_go_sdk:latest \ ---endpoint=localhost:8081 -``` -{{end}} - -{{if (eq .Sdk "java")}} -You can specify your dependency on the Samza Runner by adding the following to your `pom.xml`: - -``` - - org.apache.beam - beam-runners-samza - 2.42.0 - runtime - - - - - org.apache.samza - samza-api - ${samza.version} - - - - org.apache.samza - samza-core_2.11 - ${samza.version} - - - - org.apache.samza - samza-kafka_2.11 - ${samza.version} - runtime - - - - org.apache.samza - samza-kv_2.11 - ${samza.version} - runtime - - - - org.apache.samza - samza-kv-rocksdb_2.11 - ${samza.version} - runtime - -``` - -Console: -``` -$ mvn exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Psamza-runner \ - -Dexec.args="--runner=SamzaRunner \ - --inputFile=/path/to/input \ - --output=/path/to/counts" -``` - ### Nemo runner The Apache Nemo Runner can be used to execute Beam pipelines using Apache Nemo. The Nemo Runner can optimize Beam pipelines with the Nemo compiler through various optimization passes and execute them in a distributed fashion using the Nemo runtime. You can also deploy a self-contained application for local mode or run using resource managers like YARN or Mesos. diff --git a/release/src/main/scripts/jenkins_jobs.txt b/release/src/main/scripts/jenkins_jobs.txt deleted file mode 100644 index ae4c13b24d4c..000000000000 --- a/release/src/main/scripts/jenkins_jobs.txt +++ /dev/null @@ -1,115 +0,0 @@ -Run Chicago Taxi on Dataflow,beam_PostCommit_Python_Chicago_Taxi_Dataflow_PR -Run Chicago Taxi on Flink,beam_PostCommit_Python_Chicago_Taxi_Flink_PR -Run Dataflow Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Dataflow_PR -Run Dataflow Runner Tpcds Tests,beam_PostCommit_Java_Tpcds_Dataflow_PR -Run Dataflow Runner V2 Java 11 Nexmark Tests,beam_PostCommit_Java_Nexmark_DataflowV2_Java11_PR -Run Dataflow Runner V2 Java 17 Nexmark Tests,beam_PostCommit_Java_Nexmark_DataflowV2_Java17_PR -Run Dataflow Runner V2 Nexmark Tests,beam_PostCommit_Java_Nexmark_DataflowV2_PR -Run Dataflow Streaming ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_PR -Run Dataflow ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Dataflow_Java11_PR -Run Dataflow ValidatesRunner Java 17,beam_PostCommit_Java_ValidatesRunner_Dataflow_Java17_PR -Run Dataflow ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Dataflow_PR -Run Direct Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Direct_PR -Run Direct ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Direct_Java11_PR -Run Direct ValidatesRunner Java 17,beam_PostCommit_Java_ValidatesRunner_Direct_Java17_PR -Run Direct ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Direct_PR -Run Flink Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Flink_PR -Run Flink Runner Tpcds Tests,beam_PostCommit_Java_Tpcds_Flink_PR -Run Flink ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Flink_Java11_PR -Run Flink ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Flink_PR -Run Go Flink ValidatesRunner,beam_PostCommit_Go_VR_Flink_PR -Run Go PostCommit,beam_PostCommit_Go_PR -Run Go Samza ValidatesRunner,beam_PostCommit_Go_VR_Samza_PR -Run Go Spark ValidatesRunner,beam_PostCommit_Go_VR_Spark_PR -Run Java 11 Examples on Dataflow Runner V2,beam_PostCommit_Java_Examples_Dataflow_V2_java11_PR -Run Java 17 Examples on Dataflow Runner V2,beam_PostCommit_Java_Examples_Dataflow_V2_java17_PR -Run Java Dataflow V2 ValidatesRunner Streaming,beam_PostCommit_Java_VR_Dataflow_V2_Streaming_PR -Run Java Dataflow V2 ValidatesRunner,beam_PostCommit_Java_VR_Dataflow_V2_PR -Run Java Examples on Dataflow Runner V2,beam_PostCommit_Java_Examples_Dataflow_V2_PR -Run Java Examples_Direct,beam_PostCommit_Java_Examples_Direct_PR -Run Java Examples_Flink,beam_PostCommit_Java_Examples_Flink_PR -Run Java Examples_Spark,beam_PostCommit_Java_Examples_Spark_PR -Run Java Flink PortableValidatesRunner Streaming,beam_PostCommit_Java_PVR_Flink_Streaming_PR -Run Java InfluxDbIO_IT,beam_PostCommit_Java_InfluxDbIO_IT_PR -Run Java PostCommit,beam_PostCommit_Java_PR -Run Java PreCommit,beam_PreCommit_Java_Phrase -Run Java Samza PortableValidatesRunner,beam_PostCommit_Java_PVR_Samza_PR -Run Java Sickbay,beam_PostCommit_Java_Sickbay_PR -Run Java SingleStoreIO_IT,beam_PostCommit_Java_SingleStoreIO_IT_PR -Run Java Spark PortableValidatesRunner Batch,beam_PostCommit_Java_PVR_Spark_Batch_PR -Run Java Spark v3 PortableValidatesRunner Streaming,beam_PostCommit_Java_PVR_Spark3_Streaming_PR -Run Java examples on Dataflow Java 11,beam_PostCommit_Java_Examples_Dataflow_Java11_PR -Run Java examples on Dataflow Java 17,beam_PostCommit_Java_Examples_Dataflow_Java17_PR -Run Java_Amazon-Web-Services2_IO_Direct PreCommit,beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct_Phrase -Run Java_Amazon-Web-Services_IO_Direct PreCommit,beam_PreCommit_Java_Amazon-Web-Services_IO_Direct_Phrase -Run Java_Azure_IO_Direct PreCommit,beam_PreCommit_Java_Azure_IO_Direct_Phrase -Run Java_GCP_IO_Direct PreCommit,beam_PreCommit_Java_GCP_IO_Direct_Phrase -Run Java_Google-ads_IO_Direct PreCommit,beam_PreCommit_Java_Google-ads_IO_Direct_Phrase -Run Java_IOs_Direct PreCommit,beam_PreCommit_Java_IOs_Direct_Phrase -Run Java_Kinesis_IO_Direct PreCommit,beam_PreCommit_Java_Kinesis_IO_Direct_Phrase -Run Java_PVR_Flink_Batch PreCommit,beam_PreCommit_Java_PVR_Flink_Batch_Phrase -Run Java_Pulsar_IO_Direct PreCommit,beam_PreCommit_Java_Pulsar_IO_Direct_Phrase -Run Java_hadoop_IO_Direct PreCommit,beam_PreCommit_Java_Hadoop_IO_Direct_Phrase -Run Javadoc PostCommit,beam_PostCommit_Javadoc_PR -Run Jpms Dataflow Java 11 PostCommit,beam_PostCommit_Java_Jpms_Dataflow_Java11_PR -Run Jpms Dataflow Java 17 PostCommit,beam_PostCommit_Java_Jpms_Dataflow_Java17_PR -Run Jpms Direct Java 11 PostCommit,beam_PostCommit_Java_Jpms_Direct_Java11_PR -Run Jpms Direct Java 17 PostCommit,beam_PostCommit_Java_Jpms_Direct_Java17_PR -Run Jpms Flink Java 11 PostCommit,beam_PostCommit_Java_Jpms_Flink_Java11_PR -Run Jpms Spark Java 11 PostCommit,beam_PostCommit_Java_Jpms_Spark_Java11_PR -Run PortableJar_Flink PostCommit,beam_PostCommit_PortableJar_Flink_PR -Run PortableJar_Spark PostCommit,beam_PostCommit_PortableJar_Spark_PR -Run PostCommit_Java_Avro_Versions,beam_PostCommit_Java_Avro_Versions_PR -Run PostCommit_Java_Dataflow,beam_PostCommit_Java_DataflowV1_PR -Run PostCommit_Java_DataflowV2,beam_PostCommit_Java_DataflowV2_PR -Run PostCommit_Java_Hadoop_Versions,beam_PostCommit_Java_Hadoop_Versions_PR -Run Python 3.10 PostCommit Sickbay,beam_PostCommit_Sickbay_Python310_PR -Run Python 3.10 PostCommit,beam_PostCommit_Python310_PR -Run Python 3.11 PostCommit Sickbay,beam_PostCommit_Sickbay_Python311_PR -Run Python 3.11 PostCommit,beam_PostCommit_Python311_PR -Run Python 3.8 PostCommit Sickbay,beam_PostCommit_Sickbay_Python38_PR -Run Python 3.8 PostCommit,beam_PostCommit_Python38_PR -Run Python 3.9 PostCommit Sickbay,beam_PostCommit_Sickbay_Python39_PR -Run Python 3.9 PostCommit,beam_PostCommit_Python39_PR -Run Python Dataflow ValidatesContainer,beam_PostCommit_Py_ValCont_PR -Run Python Dataflow ValidatesRunner,beam_PostCommit_Py_VR_Dataflow_PR -Run Python Direct Runner Nexmark Tests,beam_PostCommit_Python_Nexmark_Direct_PR -Run Python Examples_Dataflow,beam_PostCommit_Python_Examples_Dataflow_PR -Run Python Examples_Direct,beam_PostCommit_Python_Examples_Direct_PR -Run Python Examples_Flink,beam_PostCommit_Python_Examples_Flink_PR -Run Python Examples_Spark,beam_PostCommit_Python_Examples_Spark_PR -Run Python Flink ValidatesRunner,beam_PostCommit_Python_VR_Flink_PR -Run Python MongoDBIO_IT,beam_PostCommit_Python_MongoDBIO_IT_PR -Run Python PreCommit,beam_PreCommit_Python_Phrase -Run Python RC Dataflow ValidatesContainer,beam_PostCommit_Py_ValCont_with_RC_PR -Run Python Samza ValidatesRunner,beam_PostCommit_Python_VR_Samza_PR -Run Python Spark ValidatesRunner,beam_PostCommit_Python_VR_Spark_PR -Run PythonDocker PreCommit,beam_PreCommit_PythonDocker_Phrase -Run Python_Coverage PreCommit,beam_PreCommit_Python_Coverage_Phrase -Run Python_Dataframes PreCommit,beam_PreCommit_Python_Dataframes_Phrase -Run Python_Examples PreCommit,beam_PreCommit_Python_Examples_Phrase -Run Python_Integration PreCommit,beam_PreCommit_Python_Integration_Phrase -Run Python_PVR_Flink PreCommit,beam_PreCommit_Python_PVR_Flink_Phrase -Run Python_Runners PreCommit,beam_PreCommit_Python_Runners_Phrase -Run Python_Transforms PreCommit,beam_PreCommit_Python_Transforms_Phrase -Run Python_Xlang_Gcp_Dataflow PostCommit,beam_PostCommit_Python_Xlang_Gcp_Dataflow_PR -Run Python_Xlang_Gcp_Direct PostCommit,beam_PostCommit_Python_Xlang_Gcp_Direct_PR -Run Python_Xlang_IO_Dataflow PostCommit,beam_PostCommit_Python_Xlang_IO_Dataflow_PR -Run SQL PostCommit,beam_PostCommit_SQL_PR -Run Samza ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Samza_PR -Run Spark Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Spark_PR -Run Spark Runner Tpcds Tests,beam_PostCommit_Java_Tpcds_Spark_PR -Run Spark StructuredStreaming ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming_PR -Run Spark ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Spark_Java11_PR -Run Spark ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Spark_PR -Run TransformService_Direct PostCommit,beam_PostCommit_TransformService_Direct_PR -Run Twister2 ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Twister2_PR -Run ULR Loopback ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_ULR_PR -Run XVR_Direct PostCommit,beam_PostCommit_XVR_Direct_PR -Run XVR_Flink PostCommit,beam_PostCommit_XVR_Flink_PR -Run XVR_GoUsingJava_Dataflow PostCommit,beam_PostCommit_XVR_GoUsingJava_Dataflow_PR -Run XVR_JavaUsingPython_Dataflow PostCommit,beam_PostCommit_XVR_JavaUsingPython_Dataflow_PR -Run XVR_PythonUsingJavaSQL_Dataflow PostCommit,beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow_PR -Run XVR_PythonUsingJava_Dataflow PostCommit,beam_PostCommit_XVR_PythonUsingJava_Dataflow_PR -Run XVR_Samza PostCommit,beam_PostCommit_XVR_Samza_PR -Run XVR_Spark3 PostCommit,beam_PostCommit_XVR_Spark3_PR diff --git a/release/src/main/scripts/mass_comment.py b/release/src/main/scripts/mass_comment.py deleted file mode 100644 index 7dec20dbbba5..000000000000 --- a/release/src/main/scripts/mass_comment.py +++ /dev/null @@ -1,176 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""Script for mass-commenting Jenkins test triggers on a Beam PR.""" - -import os -import requests -import socket -import time - - -def executeGHGraphqlQuery(accessToken, query): - '''Runs graphql query on GitHub.''' - url = 'https://api.github.com/graphql' - headers = {'Authorization': 'Bearer %s' % accessToken} - r = requests.post(url=url, json={'query': query}, headers=headers) - return r.json() - - -def getSubjectId(accessToken, prNumber): - query = ''' -query FindPullRequestID { - repository(owner:"apache", name:"beam") { - pullRequest(number:%s) { - id - } - } -} -''' % prNumber - response = executeGHGraphqlQuery(accessToken, query) - return response['data']['repository']['pullRequest']['id'] - - -def addPrComment(accessToken, subjectId, commentBody): - '''Adds a pr comment to the PR defined by subjectId''' - query = ''' -mutation AddPullRequestComment { - addComment(input:{subjectId:"%s",body: "%s"}) { - commentEdge { - node { - createdAt - body - } - } - subject { - id - } - } -} -''' % (subjectId, commentBody) - return executeGHGraphqlQuery(accessToken, query) - -def getPrStatuses(accessToken, prNumber): - query = ''' -query GetPRChecks { - repository(name: "beam", owner: "apache") { - pullRequest(number: %s) { - commits(last: 1) { - nodes { - commit { - status { - contexts { - targetUrl - context - } - } - } - } - } - } - } -} -''' % (prNumber) - return executeGHGraphqlQuery(accessToken, query) - - -def postComments(accessToken, subjectId, commentsToAdd): - ''' - Main workhorse method. Posts comments to GH. - ''' - - for comment in commentsToAdd: - jsonData = addPrComment(accessToken, subjectId, comment[0]) - print(jsonData) - # Space out comments 30 seconds apart to avoid overwhelming Jenkins - time.sleep(30) - - -def probeGitHubIsUp(): - ''' - Returns True if GitHub responds to simple queries. Else returns False. - ''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex(('github.com', 443)) - return True if result == 0 else False - -def getRemainingComments(accessToken, pr, initialComments): - ''' - Filters out the comments that already have statuses associated with them from initial comments - ''' - queryResult = getPrStatuses(accessToken, pr) - pull = queryResult["data"]["repository"]["pullRequest"] - commit = pull["commits"]["nodes"][0]["commit"] - check_urls = str(list(map(lambda c : c["targetUrl"], commit["status"]["contexts"]))) - remainingComments = [] - for comment in initialComments: - if f'/{comment[1]}_Phrase/' not in check_urls and f'/{comment[1]}_PR/' not in check_urls \ - and f'/{comment[1]}_Commit/' not in check_urls and f'/{comment[1]}/' not in check_urls \ - and 'Sickbay' not in comment[1]: - print(comment) - remainingComments.append(comment) - return remainingComments - -################################################################################ -if __name__ == '__main__': - ''' - This script is supposed to be invoked directly. - However for testing purposes and to allow importing, - wrap work code in module check. - ''' - print("Started.") - comments = [] - dirname = os.path.dirname(__file__) - with open(os.path.join(dirname, 'jenkins_jobs.txt')) as file: - comments = [line.strip() for line in file if len(line.strip()) > 0] - - for i in range(len(comments)): - parts = comments[i].split(',') - comments[i] = (parts[0], parts[1]) - - if not probeGitHubIsUp(): - print("GitHub is unavailable, skipping fetching data.") - exit() - - print("GitHub is available start fetching data.") - - accessToken = input("Enter your Github access token: ") - - pr = input("Enter the Beam PR number to test (e.g. 11403): ") - subjectId = getSubjectId(accessToken, pr) - - # TODO(yathu): also auto rerun failed GitHub Action workflow - remainingComments = getRemainingComments(accessToken, pr, comments) - if len(remainingComments) == 0: - print('Jobs have been started for all comments. If you would like to retry all jobs, create a new commit before running this script.') - while len(remainingComments) > 0: - postComments(accessToken, subjectId, remainingComments) - # Sleep 60 seconds to allow checks to start to status - time.sleep(60) - remainingComments = getRemainingComments(accessToken, pr, remainingComments) - if len(remainingComments) > 0: - print(f'{len(remainingComments)} comments must be reposted because no check has been created for them: {str(remainingComments)}') - print('Sleeping for 1 hour to allow Jenkins to recover and to give it time to status.') - for i in range(60): - time.sleep(60) - print(f'{i} minutes elapsed, {60-i} minutes remaining') - remainingComments = getRemainingComments(accessToken, pr, remainingComments) - if len(remainingComments) == 0: - print(f'{len(remainingComments)} comments still must be reposted: {str(remainingComments)}') - print('Trying to repost comments.') - - print('Done.') diff --git a/scripts/beam-sql.sh b/scripts/beam-sql.sh index 38907b0d2d26..b83e8bc4ecae 100755 --- a/scripts/beam-sql.sh +++ b/scripts/beam-sql.sh @@ -197,7 +197,6 @@ function list_runners() { echo " dataflow - DataflowRunner (runs on Google Cloud Dataflow)" echo " flink - FlinkRunner (runs on Apache Flink)" echo " spark - SparkRunner (runs on Apache Spark)" - echo " samza - SamzaRunner (runs on Apache Samza)" echo " jet - JetRunner (runs on Hazelcast Jet)" echo " twister2 - Twister2Runner (runs on Twister2)" echo "" @@ -239,10 +238,6 @@ function list_runners() { echo " spark-3 - SparkRunner (Spark 3.x)" echo " Runs on Apache Spark 3.x clusters." ;; - "samza") - echo " samza - SamzaRunner" - echo " Runs on Apache Samza." - ;; "jet") echo " jet - JetRunner" echo " Runs on Hazelcast Jet." diff --git a/scripts/ci/release/comment_pr_trigger_phrases.sh b/scripts/ci/release/comment_pr_trigger_phrases.sh deleted file mode 100755 index f31b3054e70e..000000000000 --- a/scripts/ci/release/comment_pr_trigger_phrases.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This script adds comments from a txt file, in the given GitHub Pull Request. -# test/resources/mass_comment.txt file has the Trigger Phrases for running Gradle Build and PostCommit/PreCommit tests. - -file="./test/resources/mass_comment.txt" -GITHUB_PR=$1 -while IFS= read -r trigger_phrase -do - gh pr comment "$GITHUB_PR" --body "$trigger_phrase" - sleep 30 -done <"$file" diff --git a/scripts/ci/release/test/resources/jenkins_trigger_phrases.txt b/scripts/ci/release/test/resources/jenkins_trigger_phrases.txt deleted file mode 100644 index 4f59885de8b4..000000000000 --- a/scripts/ci/release/test/resources/jenkins_trigger_phrases.txt +++ /dev/null @@ -1,19 +0,0 @@ -Run Release Gradle Build -Run CommunityMetrics PreCommit -Run Direct ValidatesRunner Java 11 -Run Direct ValidatesRunner Java 17 -Run Java 11 Examples on Dataflow Runner V2 -Run Java 17 Examples on Dataflow Runner V2 -Run Java PreCommit -Run Java examples on Dataflow Java 11 -Run Java examples on Dataflow Java 17 -Run Javadoc PostCommit -Run Jpms Dataflow Java 17 PostCommit -Run Jpms Direct Java 17 PostCommit -Run Python Flink ValidatesRunner -Run Python PreCommit -Run Python Samza ValidatesRunner -Run Python Spark ValidatesRunner -Run SQL_Java17 PreCommit -Run XVR_Flink PostCommit -Run XVR_GoUsingJava_Dataflow PostCommit diff --git a/scripts/ci/release/test/resources/mass_comment.txt b/scripts/ci/release/test/resources/mass_comment.txt deleted file mode 100644 index 1f6f340eb0b7..000000000000 --- a/scripts/ci/release/test/resources/mass_comment.txt +++ /dev/null @@ -1,106 +0,0 @@ -Run Release Gradle Build -Run CommunityMetrics PreCommit -Run Dataflow Runner Nexmark Tests -Run Dataflow Runner V2 Java 11 Nexmark Tests -Run Dataflow Runner V2 Java 17 Nexmark Tests -Run Dataflow Runner V2 Nexmark Tests -Run Dataflow Streaming ValidatesRunner -Run Dataflow ValidatesRunner Java 11 -Run Dataflow ValidatesRunner Java 17 -Run Dataflow ValidatesRunner -Run Direct Runner Nexmark Tests -Run Direct ValidatesRunner Java 11 -Run Direct ValidatesRunner Java 17 -Run Direct ValidatesRunner in Java 11 -Run Direct ValidatesRunner -Run Flink Runner Nexmark Tests -Run Flink ValidatesRunner Java 11 -Run Flink ValidatesRunner -Run Go Flink ValidatesRunner -Run Go PostCommit -Run Go PreCommit -Run Go Samza ValidatesRunner -Run Go Spark ValidatesRunner -Run GoPortable PreCommit -Run Java 11 Examples on Dataflow Runner V2 -Run Java 17 Examples on Dataflow Runner V2 -Run Java Dataflow V2 ValidatesRunner Streaming -Run Java Dataflow V2 ValidatesRunner -Run Java Examples on Dataflow Runner V2 -Run Java Examples_Direct -Run Java Examples_Flink -Run Java Examples_Spark -Run Java Flink PortableValidatesRunner Streaming -Run Java Portability examples on Dataflow with Java 11 -Run Java PostCommit -Run Java PreCommit -Run Java Samza PortableValidatesRunner -Run Java Spark PortableValidatesRunner Batch -Run Java Spark v2 PortableValidatesRunner Streaming -Run Java Spark v3 PortableValidatesRunner Streaming -Run Java examples on Dataflow Java 11 -Run Java examples on Dataflow Java 17 -Run Java examples on Dataflow with Java 11 -Run Java_Examples_Dataflow PreCommit -Run Java_Examples_Dataflow_Java11 PreCommit -Run Java_Examples_Dataflow_Java17 PreCommit -Run Java_PVR_Flink_Batch PreCommit -Run Java_PVR_Flink_Docker PreCommit -Run Javadoc PostCommit -Run Jpms Dataflow Java 11 PostCommit -Run Jpms Dataflow Java 17 PostCommit -Run Jpms Direct Java 11 PostCommit -Run Jpms Direct Java 17 PostCommit -Run Jpms Flink Java 11 PostCommit -Run Jpms Spark Java 11 PostCommit -Run PortableJar_Flink PostCommit -Run PortableJar_Spark PostCommit -Run Portable_Python PreCommit -Run PostCommit_Java_Dataflow -Run PostCommit_Java_DataflowV2 -Run PostCommit_Java_Hadoop_Versions -Run Python 3.8 PostCommit -Run Python 3.9 PostCommit -Run Python 3.10 PostCommit -Run Python 3.11 PostCommit -Run Python Dataflow V2 ValidatesRunner -Run Python Dataflow ValidatesContainer -Run Python Dataflow ValidatesRunner -Run Python Examples_Dataflow -Run Python Examples_Direct -Run Python Examples_Flink -Run Python Examples_Spark -Run Python Flink ValidatesRunner -Run Python PreCommit -Run Python Samza ValidatesRunner -Run Python Spark ValidatesRunner -Run PythonDocker PreCommit -Run PythonDocs PreCommit -Run PythonFormatter PreCommit -Run PythonLint PreCommit -Run Python_PVR_Flink PreCommit -Run Python_Xlang_Gcp_Direct PostCommit -Run RAT PreCommit -Run SQL PostCommit -Run SQL PreCommit -Run SQL_Java11 PreCommit -Run SQL_Java17 PreCommit -Run Samza ValidatesRunner -Run Spark Runner Nexmark Tests -Run Spark StructuredStreaming ValidatesRunner -Run Spark ValidatesRunner Java 11 -Run Spark ValidatesRunner -Run Spotless PreCommit -Run Twister2 ValidatesRunner -Run Typescript PreCommit -Run ULR Loopback ValidatesRunner -Run Whitespace PreCommit -Run Xlang_Generated_Transforms PreCommit -Run XVR_Direct PostCommit -Run XVR_Flink PostCommit -Run XVR_JavaUsingPython_Dataflow PostCommit -Run XVR_PythonUsingJavaSQL_Dataflow PostCommit -Run XVR_PythonUsingJava_Dataflow PostCommit -Run XVR_Samza PostCommit -Run XVR_Spark PostCommit -Run XVR_Spark3 PostCommit diff --git a/website/www/site/data/capability_matrix.yaml b/website/www/site/data/capability_matrix.yaml index 3a753262eeae..b7c236865ef3 100644 --- a/website/www/site/data/capability_matrix.yaml +++ b/website/www/site/data/capability_matrix.yaml @@ -30,8 +30,6 @@ capability-matrix: name: Twister2 - class: python direct name: Python Direct FnRunner - - class: go direct - name: Go Direct Runner categories: - description: What is being computed? @@ -82,10 +80,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: GroupByKey description: Grouping of key-value pairs per key, window, and pane. (See also other tabs.) values: @@ -125,10 +119,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Flatten description: Concatenates multiple homogenously typed collections together. values: @@ -168,10 +158,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Combine description: 'Application of an associative, commutative operation over all values ("globally") or over all values associated with each key ("per key"). Can be implemented using ParDo, but often more efficient implementations exist.' values: @@ -211,10 +197,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Composite Transforms description: Allows easy extensibility for library writers. In the near future, we expect there to be more information provided at this level -- customized metadata hooks for monitoring, additional runtime/environment hooks, etc. values: @@ -254,10 +236,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Side Inputs description: Side inputs are additional PCollections whose contents are computed during pipeline execution and then made accessible to DoFn code. The exact shape of the side input depends both on the PCollectionView used to describe the access pattern (interable, map, singleton) and the window of the element from the main input that is currently being processed. values: @@ -297,10 +275,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Source API description: Allows users to provide additional input sources. Supports both bounded and unbounded data. Includes hooks necessary to provide efficient parallelization (size estimation, progress information, dynamic splitting, etc). values: @@ -340,10 +314,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Metrics description: Allow transforms to gather simple metrics across bundles in a PTransform. Provide a mechanism to obtain both committed and attempted metrics. Semantically similar to using an additional output, but support partial results as the transform executes, and support both committed and attempted values. Will likely want to augment Metrics to be more useful for processing unbounded data by making them windowed. values: @@ -383,10 +353,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Stateful Processing description: Allows fine-grained access to per-key, per-window persistent state. Necessary for certain use cases (e.g. high-volume windows which store large amounts of data, but typically only access small portions of it; complex state machines; etc.) that are not easily or efficiently addressed via Combine or GroupByKey+ParDo. values: @@ -410,10 +376,6 @@ capability-matrix: l1: "No" l2: not implemented l3: - - class: samza - l1: "Partially" - l2: non-merging windows - l3: "States are backed up by either rocksDb KV store or in-memory hash map, and persist using changelog." - class: nemo l1: "No" l2: not implemented @@ -430,10 +392,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - description: Bounded Splittable DoFn Support Status anchor: what color-y: "fff" @@ -466,10 +424,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -486,10 +440,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "Yes" - l2: - l3: - name: Side Inputs description: "" values: @@ -513,10 +463,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -533,10 +479,6 @@ capability-matrix: l1: l2: l3: - - class: go direct - l1: "Yes" - l2: - l3: - name: Splittable DoFn Initiated Checkpointing description: "" values: @@ -560,10 +502,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -580,10 +518,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Dynamic Splitting description: "" values: @@ -607,10 +541,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -627,10 +557,6 @@ capability-matrix: l1: "Yes" l2: Only with Python SDK l3: - - class: go direct - l1: "No" - l2: - l3: - name: Bundle Finalization description: "" values: @@ -654,10 +580,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -674,10 +596,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - description: Unbounded Splittable DoFn Support Status anchor: what color-y: "fff" @@ -710,10 +628,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -730,10 +644,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Side Inputs description: "" values: @@ -757,10 +667,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -777,10 +683,6 @@ capability-matrix: l1: l2: l3: - - class: go direct - l1: "Yes" - l2: - l3: - name: Splittable DoFn Initiated Checkpointing description: "" values: @@ -804,10 +706,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -824,10 +722,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Dynamic Splitting description: "" values: @@ -851,10 +745,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -871,10 +761,6 @@ capability-matrix: l1: "No" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Bundle Finalization description: "" values: @@ -898,10 +784,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -918,10 +800,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - description: Where in event time? anchor: where color-y: "fff" @@ -954,10 +832,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -970,6 +844,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Fixed windows description: Fixed-size, timestamp-based windows. (Hourly, Daily, etc) values: @@ -993,10 +871,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1009,6 +883,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Sliding windows description: Possibly overlapping fixed-size timestamp-based windows (Every minute, use the last ten minutes of data.) values: @@ -1032,10 +910,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1048,6 +922,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Session windows description: Based on bursts of activity separated by a gap size. Different per key. values: @@ -1071,10 +949,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1087,6 +961,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Custom windows description: All windows must implement BoundedWindow, which specifies a max timestamp. Each WindowFn assigns elements to an associated window. values: @@ -1110,10 +988,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1126,6 +1000,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Custom merging windows description: A custom WindowFn additionally specifies whether and how to merge windows. values: @@ -1149,10 +1027,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1165,6 +1039,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Timestamp control description: For a grouping transform, such as GBK or Combine, an OutputTimeFn specifies (1) how to combine input timestamps within a window and (2) how to merge aggregated timestamps when windows merge. values: @@ -1188,10 +1066,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1204,6 +1078,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - description: When in processing time? anchor: when @@ -1237,10 +1115,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1253,7 +1127,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" - + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Event-time triggers description: Triggers that fire in response to event-time completeness signals, such as watermarks progressing. values: @@ -1277,10 +1154,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1293,6 +1166,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Processing-time triggers description: Triggers that fire in response to processing-time advancing. @@ -1317,10 +1194,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1333,6 +1206,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Count triggers description: Triggers that fire after seeing at least N elements. @@ -1357,10 +1234,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1373,6 +1246,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Composite triggers description: Triggers which compose other triggers in more complex structures, such as logical AND, logical OR, early/on-time/late, etc. @@ -1397,10 +1274,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1413,6 +1286,10 @@ capability-matrix: l1: "Partially" l2: l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Allowed lateness description: A way to bound the useful lifetime of a window (in event time), after which any unemitted results may be materialized, the window contents may be garbage collected, and any addtional late data that arrive for the window may be discarded. @@ -1437,10 +1314,6 @@ capability-matrix: l1: "No" l2: no streaming support in the runner l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1453,6 +1326,10 @@ capability-matrix: l1: "Partially" l2: l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Timers description: A fine-grained mechanism for performing work at some point in the future, in either the event-time or processing-time domain. Useful for orchestrating delayed events, timeouts, etc in complex state per-key, per-window state machines. @@ -1477,10 +1354,6 @@ capability-matrix: l1: "No" l2: not implemented l3: "" - - class: samza - l1: "Partially" - l2: non-merging windows - l3: The Samza Runner supports timers in non-merging windows. - class: nemo l1: "No" l2: not implemented @@ -1493,6 +1366,10 @@ capability-matrix: l1: "Partially" l2: l3: "" + - class: python direct + l1: "Yes" + l2: "Partially" + l3: "" - description: How do refinements relate? anchor: how @@ -1526,10 +1403,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1542,6 +1415,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Accumulating description: Elements are accumulated in state across multiple pane firings for the same window. @@ -1566,10 +1443,6 @@ capability-matrix: l1: "No" l2: "" l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1582,6 +1455,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - description: Additional common features not yet part of the Beam model anchor: misc @@ -1615,10 +1492,6 @@ capability-matrix: l1: l2: l3: - - class: samza - l1: - l2: - l3: - class: nemo l1: l2: @@ -1631,6 +1504,10 @@ capability-matrix: l1: l2: l3: + - class: python direct + l1: + l2: + l3: - name: Checkpoint description: APIs and semantics for saving a pipeline checkpoint are under discussion. This would be a runner-specific materialization of the pipeline state required to resume or duplicate the pipeline. values: @@ -1654,10 +1531,6 @@ capability-matrix: l1: "No" l2: l3: not implemented - - class: samza - l1: "Partially" - l2: - l3: Samza has a native checkpoint capability. - class: nemo l1: l2: @@ -1670,6 +1543,10 @@ capability-matrix: l1: l2: l3: + - class: python direct + l1: + l2: + l3: - name: Key-ordered delivery description: The runner offers guarantees for the order in which elements are passed in between operations. See per-key ordering semantics. values: @@ -1681,10 +1558,6 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" - - class: prism - l1: "Unverified" - l2: - l3: - class: flink l1: "Partially" l2: @@ -1701,10 +1574,6 @@ capability-matrix: l1: "Unverified" l2: l3: - - class: samza - l1: "Partially" - l2: - l3: Samza may perform different shuffling algorithms for batch and streaming. Samza guarantees key-ordered delivery in streaming, though not in batch. - class: nemo l1: "Unverified" l2: diff --git a/website/www/site/layouts/partials/section-menu/en/roadmap.html b/website/www/site/layouts/partials/section-menu/en/roadmap.html index 8c8d14ecce28..09c326705d9f 100644 --- a/website/www/site/layouts/partials/section-menu/en/roadmap.html +++ b/website/www/site/layouts/partials/section-menu/en/roadmap.html @@ -33,7 +33,6 @@
  • Flink Runner
  • Nemo Runner
  • Spark Runner
  • -
  • Samza Runner
  • Twister2 Runner
  • From 9ae96bf14cc0814ab8b5db0627bb88ab34cb8672 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 30 Apr 2026 16:14:31 -0400 Subject: [PATCH 042/490] Remove Ubuntu 20.04 runner pools (#38335) --- .../arc/environments/beam.env | 58 +------------------ .../arc/variables.tf | 8 +-- 2 files changed, 7 insertions(+), 59 deletions(-) diff --git a/.github/gh-actions-self-hosted-runners/arc/environments/beam.env b/.github/gh-actions-self-hosted-runners/arc/environments/beam.env index 0c817c9989ee..90dadae8b470 100644 --- a/.github/gh-actions-self-hosted-runners/arc/environments/beam.env +++ b/.github/gh-actions-self-hosted-runners/arc/environments/beam.env @@ -34,9 +34,9 @@ subnetwork_cidr_range = "10.119.0.0/20" service_account_id = "beam-github-actions@apache-beam-testing.iam.gserviceaccount.com" runner_group = "beam" main_runner = { - name = "main-runner" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:d7cd81a1649bc665581951d2330c4b8acd19ed72" + name = "main-runner-2404" machine_type = "e2-standard-16" + runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:latest" min_node_count = "1" max_node_count = "30" min_replicas = "1" @@ -47,43 +47,9 @@ main_runner = { cpu = "2" memory = "3Gi" } - labels = ["self-hosted", "ubuntu-20.04", "main"] + labels = ["self-hosted", "ubuntu-24.04", "main"] } additional_runner_pools = [{ - name = "small-runner" - machine_type = "e2-standard-2" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:d7cd81a1649bc665581951d2330c4b8acd19ed72" - min_node_count = "1" - max_node_count = "15" - min_replicas = "1" - max_replicas = "15" - webhook_scaling = false - requests = { - cpu = "1500m" - memory = "5Gi" - } - labels = ["self-hosted", "ubuntu-20.04", "small"] - enable_selector = true - enable_taint = true -}, -{ - name = "highmem-runner" - machine_type = "c3-highmem-8" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:d7cd81a1649bc665581951d2330c4b8acd19ed72" - min_node_count = "1" - max_node_count = "15" - min_replicas = "1" - max_replicas = "15" - webhook_scaling = false - requests = { - cpu = "7.5" - memory = "5Gi" - } - labels = ["self-hosted", "ubuntu-20.04", "highmem"] - enable_selector = true - enable_taint = true -}, -{ name = "highmem-runner-22" machine_type = "c3-highmem-22" runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:latest" @@ -100,24 +66,6 @@ additional_runner_pools = [{ enable_selector = true enable_taint = true }, -{ - name = "main-runner-2404" - machine_type = "e2-standard-16" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:latest" - min_node_count = "1" - max_node_count = "30" - min_replicas = "1" - max_replicas = "240" - webhook_scaling = false - disk_size_gb = 200 - requests = { - cpu = "2" - memory = "3Gi" - } - labels = ["self-hosted", "ubuntu-24.04", "main"] - enable_selector = true - enable_taint = true -}, { name = "small-runner-2404" machine_type = "e2-standard-2" diff --git a/.github/gh-actions-self-hosted-runners/arc/variables.tf b/.github/gh-actions-self-hosted-runners/arc/variables.tf index 3caeffe5a523..6dd1120f83a5 100644 --- a/.github/gh-actions-self-hosted-runners/arc/variables.tf +++ b/.github/gh-actions-self-hosted-runners/arc/variables.tf @@ -90,8 +90,8 @@ variable "main_runner" { max_replicas = optional(number, 1) disk_size_gb = optional(number, 100) webhook_scaling = optional(bool, false) - runner_image = optional(string, "summerwind/actions-runner:v2.304.0-ubuntu-20.04-30355f7") - labels = optional(list(string), ["self-hosted", "ubuntu-20.04","main"]) + runner_image = optional(string, "summerwind/actions-runner:v2.334.0-ubuntu-24.04-012f1a5") + labels = optional(list(string), ["self-hosted", "ubuntu-24.04","main"]) enable_selector = optional(bool, false) enable_taint = optional(bool, false) requests = optional(object({ @@ -119,8 +119,8 @@ variable "additional_runner_pools" { max_replicas = optional(number, 1) disk_size_gb = optional(number, 100) webhook_scaling = optional(bool, false) - runner_image = optional(string, "summerwind/actions-runner:v2.304.0-ubuntu-20.04-30355f7") - labels = optional(list(string), ["self-hosted", "ubuntu-20.04","changeme"]) + runner_image = optional(string, "summerwind/actions-runner:v2.334.0-ubuntu-24.04-012f1a5") + labels = optional(list(string), ["self-hosted", "ubuntu-24.04","changeme"]) enable_selector = optional(bool, true) enable_taint = optional(bool, true) requests = optional(object({ From f010355a5715d41068bf57a86f4a715387b4a216 Mon Sep 17 00:00:00 2001 From: reuvenlax Date: Thu, 30 Apr 2026 14:04:00 -0700 Subject: [PATCH 043/490] Merge pull request #38332: only expand update graph if needed --- .../bigquery/StorageApiConvertMessages.java | 206 +++++++++--------- 1 file changed, 107 insertions(+), 99 deletions(-) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java index e0713311e2cd..02ef9e9c06a3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java @@ -147,109 +147,117 @@ public PCollectionTuple expand(PCollection> input) { .get(patchTableSchemaTag) .setCoder(KvCoder.of(destinationCoder, ProtoCoder.of(TableSchema.class))); result.get(elementsWaitingForSchemaTag).setCoder(KvCoder.of(destinationCoder, elementCoder)); + if (!hasSchemaUpdateOptions) { + // Don't expand the update graph if it's not needed. + return result; + } else { + final int numShards = + input + .getPipeline() + .getOptions() + .as(BigQueryOptions.class) + .getSchemaUpgradeBufferingShards(); - final int numShards = - input - .getPipeline() - .getOptions() - .as(BigQueryOptions.class) - .getSchemaUpgradeBufferingShards(); + // Throttle the stream to the patch-table function so that only a single update per table per + // two seconds gets processed (to match quotas). The combiner merges incremental schemas, so + // we + // won't miss any updates. + PCollection, ElementT>> tablesPatched = + result + .get(patchTableSchemaTag) + .apply( + "rewindow", + Window.>configure() + .triggering( + Repeatedly.forever( + AfterProcessingTime.pastFirstElementInPane() + .plusDelayOf(Duration.standardSeconds(2)))) + .discardingFiredPanes()) + .apply("merge schemas", Combine.fewKeys(new MergeSchemaCombineFn())) + .setCoder(KvCoder.of(destinationCoder, ProtoCoder.of(TableSchema.class))) + .apply( + "Patch table schema", + ParDo.of( + new PatchTableSchemaDoFn<>(operationName, bqServices, dynamicDestinations))) + .setCoder(KvCoder.of(destinationCoder, NullableCoder.of(elementCoder))) + // We need to make sure that all shards of the buffering transform are notified. + .apply( + "fanout to all shards", + FlatMapElements.via( + new SimpleFunction< + KV, + Iterable, ElementT>>>() { + @Override + public Iterable, ElementT>> apply( + KV elem) { + return IntStream.range(0, numShards) + .mapToObj( + i -> + KV.of( + StorageApiConvertMessages.AssignShardFn.getShardedKey( + elem.getKey(), i, numShards), + elem.getValue())) + .collect(Collectors.toList()); + } + })) + .setCoder( + KvCoder.of(ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))) + .apply( + Window., ElementT>>configure() + .triggering(DefaultTrigger.of())); - // Throttle the stream to the patch-table function so that only a single update per table per - // two seconds gets processed (to match quotas). The combiner merges incremental schemas, so we - // won't miss any updates. - PCollection, ElementT>> tablesPatched = - result - .get(patchTableSchemaTag) - .apply( - "rewindow", - Window.>configure() - .triggering( - Repeatedly.forever( - AfterProcessingTime.pastFirstElementInPane() - .plusDelayOf(Duration.standardSeconds(2)))) - .discardingFiredPanes()) - .apply("merge schemas", Combine.fewKeys(new MergeSchemaCombineFn())) - .setCoder(KvCoder.of(destinationCoder, ProtoCoder.of(TableSchema.class))) - .apply( - "Patch table schema", - ParDo.of( - new PatchTableSchemaDoFn<>(operationName, bqServices, dynamicDestinations))) - .setCoder(KvCoder.of(destinationCoder, NullableCoder.of(elementCoder))) - // We need to make sure that all shards of the buffering transform are notified. - .apply( - "fanout to all shards", - FlatMapElements.via( - new SimpleFunction< - KV, - Iterable, ElementT>>>() { - @Override - public Iterable, ElementT>> apply( - KV elem) { - return IntStream.range(0, numShards) - .mapToObj( - i -> - KV.of( - StorageApiConvertMessages.AssignShardFn.getShardedKey( - elem.getKey(), i, numShards), - elem.getValue())) - .collect(Collectors.toList()); - } - })) - .setCoder( - KvCoder.of(ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))) - .apply( - Window., ElementT>>configure() - .triggering(DefaultTrigger.of())); + // Any elements that are waiting for a schema update are sent to this stateful DoFn to be + // buffered. + // Note: we currently do not provide the DynamicDestinations object access to the side input + // in + // this path. + // This is because side inputs are not currently available from timer callbacks. Since side + // inputs are generally + // used for getSchema and in this case we read the schema from the table, this is unlikely to + // be + // a problem. + PCollection, ElementT>> shardedWaitingElements = + result + .get(elementsWaitingForSchemaTag) + // TODO: Consider using GroupIntoBatchs.withShardingKey to get auto sharding here + // instead of fixed sharding. + .apply("assignShard", ParDo.of(new AssignShardFn<>(numShards))) + .setCoder( + KvCoder.of( + ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))); - // Any elements that are waiting for a schema update are sent to this stateful DoFn to be - // buffered. - // Note: we currently do not provide the DynamicDestinations object access to the side input in - // this path. - // This is because side inputs are not currently available from timer callbacks. Since side - // inputs are generally - // used for getSchema and in this case we read the schema from the table, this is unlikely to be - // a problem. - PCollection, ElementT>> shardedWaitingElements = - result - .get(elementsWaitingForSchemaTag) - // TODO: Consider using GroupIntoBatchs.withShardingKey to get auto sharding here - // instead of fixed sharding. - .apply("assignShard", ParDo.of(new AssignShardFn<>(numShards))) - .setCoder( - KvCoder.of(ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))); + PCollectionList, ElementT>> waitingElementsList = + PCollectionList.of(shardedWaitingElements).and(tablesPatched); + PCollectionTuple retryResult = + waitingElementsList + .apply("Buffered flatten", Flatten.pCollections()) + .apply( + "bufferElements", + ParDo.of(new SchemaUpdateHoldingFn<>(elementCoder, convertMessagesDoFn)) + .withOutputTags( + successfulWritesTag, + TupleTagList.of(ImmutableList.of(failedWritesTag, BAD_RECORD_TAG)))); + retryResult.get(successfulWritesTag).setCoder(successCoder); + retryResult.get(failedWritesTag).setCoder(errorCoder); + retryResult.get(BAD_RECORD_TAG).setCoder(BadRecord.getCoder(input.getPipeline())); - PCollectionList, ElementT>> waitingElementsList = - PCollectionList.of(shardedWaitingElements).and(tablesPatched); - PCollectionTuple retryResult = - waitingElementsList - .apply("Buffered flatten", Flatten.pCollections()) - .apply( - "bufferElements", - ParDo.of(new SchemaUpdateHoldingFn<>(elementCoder, convertMessagesDoFn)) - .withOutputTags( - successfulWritesTag, - TupleTagList.of(ImmutableList.of(failedWritesTag, BAD_RECORD_TAG)))); - retryResult.get(successfulWritesTag).setCoder(successCoder); - retryResult.get(failedWritesTag).setCoder(errorCoder); - retryResult.get(BAD_RECORD_TAG).setCoder(BadRecord.getCoder(input.getPipeline())); - - // Flatten successes and failures from both the regular transform and the retry transform. - PCollection> allSuccesses = - PCollectionList.of(result.get(successfulWritesTag)) - .and(retryResult.get(successfulWritesTag)) - .apply("flattenSuccesses", Flatten.pCollections()); - PCollection allFailures = - PCollectionList.of(result.get(failedWritesTag)) - .and(retryResult.get(failedWritesTag)) - .apply("flattenFailures", Flatten.pCollections()); - PCollection allBadRecords = - PCollectionList.of(result.get(BAD_RECORD_TAG)) - .and(retryResult.get(BAD_RECORD_TAG)) - .apply("flattenBadRecords", Flatten.pCollections()); - return PCollectionTuple.of(successfulWritesTag, allSuccesses) - .and(failedWritesTag, allFailures) - .and(BAD_RECORD_TAG, allBadRecords); + // Flatten successes and failures from both the regular transform and the retry transform. + PCollection> allSuccesses = + PCollectionList.of(result.get(successfulWritesTag)) + .and(retryResult.get(successfulWritesTag)) + .apply("flattenSuccesses", Flatten.pCollections()); + PCollection allFailures = + PCollectionList.of(result.get(failedWritesTag)) + .and(retryResult.get(failedWritesTag)) + .apply("flattenFailures", Flatten.pCollections()); + PCollection allBadRecords = + PCollectionList.of(result.get(BAD_RECORD_TAG)) + .and(retryResult.get(BAD_RECORD_TAG)) + .apply("flattenBadRecords", Flatten.pCollections()); + return PCollectionTuple.of(successfulWritesTag, allSuccesses) + .and(failedWritesTag, allFailures) + .and(BAD_RECORD_TAG, allBadRecords); + } } static class AssignShardFn extends DoFn, KV, V>> { From 7c3642ab25db9d8cf875c364f91f69160a7d658a Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Thu, 30 Apr 2026 17:29:42 -0400 Subject: [PATCH 044/490] Cross-build java-extensions-avro with Avro 1.12 (#38109) * Add Avro version 1.12.0 to build.gradle * Update AvroUtilsTest.java * Update AvroUtilsTest.java * Updating to Avro 1.12.1 * Revert "Updating to Avro 1.12.1" This reverts commit efdc40a88ba0ddaaaaa1bd00cb526fe18b22455e. * Update AvroUtilsTest.java * Update AvroUtilsTest.java * retrigger CI * retrigger CI * Updates * Update build.gradle * Revert "Update build.gradle" This reverts commit 1721dcde47a81c973f00890eeaf6da54c107a7c9. * Revert "Updates" This reverts commit 32df001d3b418d3b05aab2c48a5d1aa8397082b3. * Update AvroUtilsTest.java * Update sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java Co-authored-by: Claire McGinty --------- Co-authored-by: Claire McGinty --- sdks/java/extensions/avro/build.gradle | 1 + .../avro/schemas/utils/AvroUtilsTest.java | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/sdks/java/extensions/avro/build.gradle b/sdks/java/extensions/avro/build.gradle index e3afb5ff52e4..6b24bf693dfb 100644 --- a/sdks/java/extensions/avro/build.gradle +++ b/sdks/java/extensions/avro/build.gradle @@ -40,6 +40,7 @@ def avroVersions = [ '182' : "1.8.2", '192' : "1.9.2", '1102': "1.10.2", + '1120': "1.12.0", ] avroVersions.each { k, v -> diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java index 57da8e77bb1b..282158bec03d 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.fasterxml.jackson.databind.ObjectMapper; import com.pholser.junit.quickcheck.From; import com.pholser.junit.quickcheck.Property; import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; @@ -284,10 +285,20 @@ public void avroToBeamRoundTrip( Iterable iterable = randomData(avroSchema, 10); List records = Lists.newArrayList((Iterable) iterable); + // AVRO-4139: GenericRecord.equals() throws "Can't compare maps!" for records with + // nested map types on Avro 1.12.0. Fall back to JSON tree comparison on that version + // only; keep direct equals for other versions. + boolean useJsonCompare = "1.12.0".equals(VERSION_AVRO); + ObjectMapper mapper = useJsonCompare ? new ObjectMapper() : null; + for (GenericRecord record : records) { Row row = AvroUtils.toBeamRowStrict(record, schema); GenericRecord out = AvroUtils.toGenericRecord(row, avroSchema); - assertEquals(record, out); + if (useJsonCompare) { + assertEquals(mapper.readTree(record.toString()), mapper.readTree(out.toString())); + } else { + assertEquals(record, out); + } } } From db0c4c9204150e2b2d6d3ea28838b07f41951239 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 1 May 2026 04:07:06 -0400 Subject: [PATCH 045/490] Revert "remove processContext usage across examples (java and kotlin)" (#38341) * Revert "remove processContext usage across examples (java and kotlin) (#37937)" This reverts commit 4151dded54fc3b120e8a2b382ce64f9eee576a06. * Restore SKILLs.md as it does not affect Beam playground --- .../iceberg/IcebergBatchWriteExample.java | 15 ++- .../beam/examples/SchemaTransformExample.java | 8 +- .../beam/examples/SqlTransformExample.java | 8 +- .../examples/ApproximateQuantilesExample.java | 8 +- .../examples/CoCombineTransformExample.java | 17 +-- .../beam/examples/CoGroupByKeyExample.java | 8 +- .../apache/beam/examples/CombineExample.java | 8 +- .../apache/beam/examples/CountExample.java | 8 +- .../beam/examples/CountPerKeyExample.java | 8 +- .../apache/beam/examples/CreateExample.java | 8 +- .../beam/examples/DebuggingWordCount.java | 13 +-- .../apache/beam/examples/DistinctExample.java | 8 +- .../beam/examples/FlatMapElementsExample.java | 8 +- .../examples/GroupIntoBatchesExample.java | 8 +- .../examples/KafkaPassengerCountJson.java | 19 ++-- .../apache/beam/examples/KafkaStreaming.java | 31 +++-- .../beam/examples/KafkaWordCountAvro.java | 9 +- .../beam/examples/KafkaWordCountJson.java | 9 +- .../org/apache/beam/examples/KeysExample.java | 8 +- .../apache/beam/examples/KvSwapExample.java | 8 +- .../apache/beam/examples/LatestExample.java | 8 +- .../beam/examples/MapElementsExample.java | 8 +- .../org/apache/beam/examples/MaxExample.java | 8 +- .../beam/examples/MaxPerKeyExample.java | 8 +- .../org/apache/beam/examples/MeanExample.java | 8 +- .../beam/examples/MeanPerKeyExample.java | 8 +- .../org/apache/beam/examples/MinExample.java | 8 +- .../beam/examples/MinPerKeyExample.java | 8 +- .../beam/examples/PartitionExample.java | 8 +- .../beam/examples/RateLimiterSimple.java | 8 +- .../apache/beam/examples/RegexExample.java | 8 +- .../apache/beam/examples/SampleExample.java | 8 +- .../org/apache/beam/examples/SumExample.java | 8 +- .../beam/examples/SumPerKeyExample.java | 8 +- .../apache/beam/examples/ToStringExample.java | 8 +- .../org/apache/beam/examples/TopExample.java | 8 +- .../apache/beam/examples/ValuesExample.java | 8 +- .../org/apache/beam/examples/ViewExample.java | 19 ++-- .../apache/beam/examples/WindowExample.java | 8 +- .../beam/examples/complete/AutoComplete.java | 60 ++++------ .../complete/StreamingWordExtract.java | 16 ++- .../apache/beam/examples/complete/TfIdf.java | 75 +++++------- .../complete/TopWikipediaSessions.java | 30 ++--- .../examples/complete/TrafficMaxLaneFlow.java | 30 ++--- .../beam/examples/complete/TrafficRoutes.java | 46 +++----- .../transforms/DataProtectors.java | 21 ++-- .../transforms/io/TokenizationBigQueryIO.java | 7 +- .../transforms/io/TokenizationBigTableIO.java | 6 +- .../datatokenization/utils/CsvConverters.java | 107 ++++++------------ .../utils/ErrorConverters.java | 30 ++--- .../examples/complete/game/GameStats.java | 45 ++++---- .../complete/game/HourlyTeamScore.java | 6 +- .../examples/complete/game/LeaderBoard.java | 18 +-- .../complete/game/StatefulTeamScore.java | 18 ++- .../examples/complete/game/UserScore.java | 14 +-- .../complete/game/utils/WriteToBigQuery.java | 22 +--- .../complete/game/utils/WriteToText.java | 21 +--- .../game/utils/WriteWindowedToBigQuery.java | 15 +-- .../cookbook/BigQueryStreamingTornadoes.java | 21 ++-- .../examples/cookbook/BigQueryTornadoes.java | 16 +-- .../cookbook/CombinePerKeyExamples.java | 14 +-- .../examples/cookbook/FilterExamples.java | 31 +++-- .../beam/examples/cookbook/JoinExamples.java | 32 +++--- .../examples/cookbook/MaxPerKeyExamples.java | 17 ++- .../cookbook/MinimalBigQueryTornadoes.java | 18 ++- .../examples/cookbook/TriggerExample.java | 47 +++----- .../beam/examples/snippets/Snippets.java | 40 +++---- .../subprocess/ExampleEchoPipeline.java | 10 +- .../examples/cookbook/TriggerExampleTest.java | 8 +- .../subprocess/ExampleEchoPipelineTest.java | 10 +- .../examples/kotlin/DebuggingWordCount.kt | 12 +- .../kotlin/cookbook/BigQueryTornadoes.kt | 15 ++- .../kotlin/cookbook/CombinePerKeyExamples.kt | 13 +-- .../kotlin/cookbook/FilterExamples.kt | 30 +++-- .../examples/kotlin/cookbook/JoinExamples.kt | 31 +++-- .../kotlin/cookbook/MaxPerKeyExamples.kt | 15 ++- .../kotlin/cookbook/TriggerExample.kt | 42 +++---- .../beam/examples/kotlin/snippets/Snippets.kt | 13 +-- 78 files changed, 547 insertions(+), 855 deletions(-) diff --git a/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java b/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java index 167d3017c3d9..2a5f85e524ed 100644 --- a/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java +++ b/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java @@ -28,8 +28,6 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -76,8 +74,9 @@ private static Row flattenAnalyticsRow(Row row) { static class ExtractBrowserTransactionsFn extends DoFn> { @ProcessElement - public void processElement(@Element Row row, OutputReceiver> receiver) { - receiver.output( + public void processElement(ProcessContext c) { + Row row = c.element(); + c.output( KV.of( Preconditions.checkStateNotNull(row.getString("browser")), Preconditions.checkStateNotNull(row.getInt64("transactions")))); @@ -86,13 +85,13 @@ public void processElement(@Element Row row, OutputReceiver> re static class FormatCountsFn extends DoFn, Row> { @ProcessElement - public void processElement(@Element KV element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { Row row = Row.withSchema(AGGREGATED_SCHEMA) - .withFieldValue("browser", element.getKey()) - .withFieldValue("transaction_count", element.getValue()) + .withFieldValue("browser", c.element().getKey()) + .withFieldValue("transaction_count", c.element().getValue()) .build(); - receiver.output(row); + c.output(row); } } diff --git a/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java b/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java index 84155683e9cc..5b8e4a34cf2c 100644 --- a/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java +++ b/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java @@ -42,8 +42,6 @@ import org.apache.beam.sdk.schemas.transforms.Select; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; @@ -103,9 +101,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java b/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java index 9ca2dda313c6..9c2302c3de2f 100644 --- a/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java +++ b/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java @@ -41,8 +41,6 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; @@ -97,9 +95,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java b/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java index 0f968e88d392..9e2a96b1eca9 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java @@ -24,8 +24,6 @@ import org.apache.beam.sdk.transforms.ApproximateQuantiles; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.slf4j.Logger; @@ -72,9 +70,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java b/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java index 1aafd50a1621..0ee7d7bcac89 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java @@ -46,8 +46,6 @@ import org.apache.beam.sdk.transforms.CombineFns; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; @@ -187,16 +185,13 @@ public Long apply(Long input) { new DoFn< KV, KV>>>() { @ProcessElement - public void processElement( - @Element KV element, - OutputReceiver>>> receiver) - throws Exception { - CombineFns.CoCombineResult e = element.getValue(); + public void processElement(ProcessContext c) throws Exception { + CombineFns.CoCombineResult e = c.element().getValue(); ArrayList> o = new ArrayList>(); o.add(KV.of(minTag.getId(), e.get(minTag))); o.add(KV.of(maxTag.getId(), e.get(maxTag))); o.add(KV.of(sumTag.getId(), e.get(sumTag))); - receiver.output(KV.of(element.getKey(), o)); + c.output(KV.of(c.element().getKey(), o)); } })); @@ -215,9 +210,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java index 46905ad71daf..c77708b5de20 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.join.CoGbkResult; import org.apache.beam.sdk.transforms.join.CoGroupByKey; @@ -86,9 +84,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java b/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java index 5f6901ea5ec4..24bed27c2360 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java @@ -23,8 +23,6 @@ import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.values.PCollection; @@ -70,9 +68,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CountExample.java b/examples/java/src/main/java/org/apache/beam/examples/CountExample.java index f95ec3fe4759..cb0bd0ecf943 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CountExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CountExample.java @@ -23,8 +23,6 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.slf4j.Logger; @@ -65,9 +63,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java index f02c262ccbab..9bf9bf1ef00f 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java @@ -23,8 +23,6 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -69,9 +67,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java b/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java index b5eb5ba3e9de..5943ffa489d4 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java @@ -27,8 +27,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -81,9 +79,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java b/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java index 07c5be76d753..7c54e238da33 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java +++ b/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java @@ -46,8 +46,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -117,19 +115,18 @@ public FilterTextFn(String pattern) { private final Counter unmatchedWords = Metrics.counter(FilterTextFn.class, "unmatchedWords"); @ProcessElement - public void processElement( - @Element KV element, OutputReceiver> receiver) { - if (filter.matcher(element.getKey()).matches()) { + public void processElement(ProcessContext c) { + if (filter.matcher(c.element().getKey()).matches()) { // Log at the "DEBUG" level each element that we match. When executing this pipeline // these log lines will appear only if the log level is set to "DEBUG" or lower. - LOG.debug("Matched: {}", element.getKey()); + LOG.debug("Matched: {}", c.element().getKey()); matchedWords.inc(); - receiver.output(element); + c.output(c.element()); } else { // Log at the "TRACE" level each element that is not matched. Different log levels // can be used to control the verbosity of logging providing an effective mechanism // to filter less important information. - LOG.trace("Did not match: {}", element.getKey()); + LOG.trace("Did not match: {}", c.element().getKey()); unmatchedWords.inc(); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java b/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java index 28fd4af87066..d3ff9d663f44 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java @@ -23,8 +23,6 @@ import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.Distinct; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.slf4j.Logger; @@ -70,9 +68,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java index 4e9493ac1cf2..71d05ac7ade3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java @@ -24,8 +24,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.FlatMapElements; import org.apache.beam.sdk.transforms.InferableFunction; import org.apache.beam.sdk.transforms.ParDo; @@ -80,9 +78,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java b/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java index 212079579ee0..78e898b9c173 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.GroupIntoBatches; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -76,9 +74,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java index 5b26455e3c5f..20f70232ae94 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java @@ -55,8 +55,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.transforms.Values; @@ -111,13 +109,10 @@ public static void main(String[] args) { ParDo.of( new DoFn>() { @ProcessElement - public void processElement( - @Element String element, OutputReceiver> receiver) - throws JsonProcessingException { + public void processElement(ProcessContext c) throws JsonProcessingException { final VendorToPassengerDTO result = - om.readValue(element, new TypeReference() {}); - receiver.output( - KV.of(result.getVendorIdField(), result.getPassengerCountField())); + om.readValue(c.element(), new TypeReference() {}); + c.output(KV.of(result.getVendorIdField(), result.getPassengerCountField())); } })) .apply( @@ -129,11 +124,11 @@ public void processElement( new DoFn, KV>() { @ProcessElement public void processElement( - OutputReceiver> out, - @Element KV element) { + ProcessContext c, OutputReceiver> out) { System.out.printf( - "Vendor: %s, Passengers: %s%n", element.getKey(), element.getValue()); - out.output(element); + "Vendor: %s, Passengers: %s%n", + c.element().getKey(), c.element().getValue()); + out.output(c.element()); } })); p.run().waitUntilFinish(); diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java index 327d321d8ad3..f0b09226865a 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java @@ -49,8 +49,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; @@ -177,9 +175,8 @@ static class RandomUserScoreGeneratorFn extends DoFn private static final Random RANDOM = new Random(); @ProcessElement - public void processElement( - @Element Object element, OutputReceiver> receiver) { - receiver.output(generate()); + public void processElement(ProcessContext c) { + c.output(generate()); } public KV generate() { @@ -293,17 +290,17 @@ public Map extractOutput(Map accumulator) { static class LogResults extends DoFn, Map> { @ProcessElement - public void processElement( - PaneInfo pane, - IntervalWindow w, - @Element Map scores, - OutputReceiver> receiver) - throws Exception { + public void processElement(ProcessContext c, IntervalWindow w) throws Exception { + Map map = c.element(); + if (map == null) { + c.output(c.element()); + return; + } String startTime = w.start().toString(dateTimeFormatter); String endTime = w.end().toString(dateTimeFormatter); - PaneInfo.Timing timing = pane.getTiming(); + PaneInfo.Timing timing = c.pane().getTiming(); switch (timing) { case EARLY: @@ -319,7 +316,7 @@ public void processElement( throw new RuntimeException("Unknown timing value"); } - for (Map.Entry entry : scores.entrySet()) { + for (Map.Entry entry : map.entrySet()) { System.out.printf("%10s: %-10s%n", entry.getKey(), entry.getValue()); } @@ -329,7 +326,7 @@ public void processElement( System.out.println(); } - receiver.output(scores); + c.output(c.element()); } } @@ -343,9 +340,9 @@ public PCollection expand(PCollection input) { static class LogErrorFn extends DoFn { @ProcessElement - public void processElement(@Element BadRecord badRecord, OutputReceiver receiver) { - System.out.println(badRecord); - receiver.output(badRecord); + public void processElement(@Element BadRecord record, OutputReceiver receiver) { + System.out.println(record); + receiver.output(record); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java index fad5668945e7..9e2da248017d 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java @@ -51,8 +51,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -105,11 +103,10 @@ public static void main(String[] args) { ParDo.of( new DoFn() { @ProcessElement - public void processElement( - @Element String element, OutputReceiver receiver) { - for (String word : element.split(TOKENIZER_PATTERN, 0)) { + public void processElement(ProcessContext c) { + for (String word : c.element().split(TOKENIZER_PATTERN, 0)) { if (!word.isEmpty()) { - receiver.output(word); + c.output(word); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java index 036cc6702123..355614b43869 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java @@ -52,8 +52,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -106,11 +104,10 @@ public static void main(String[] args) { ParDo.of( new DoFn() { @ProcessElement - public void processElement( - @Element String element, OutputReceiver receiver) { - for (String word : element.split(TOKENIZER_PATTERN, 0)) { + public void processElement(ProcessContext c) { + for (String word : c.element().split(TOKENIZER_PATTERN, 0)) { if (!word.isEmpty()) { - receiver.output(word); + c.output(word); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java b/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java index c9dc40170a57..155834bc0a43 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Keys; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -72,9 +70,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java b/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java index 192dc893795b..090779de7c36 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.KvSwap; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -71,9 +69,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java b/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java index ef4c50d24c73..5e9662a2f1ab 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Latest; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.WithTimestamps; @@ -86,9 +84,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java index 60908cf83a48..93d885750ae8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -78,9 +76,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java b/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java index 845b107a2214..9173d11754a3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; @@ -65,9 +63,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java index 5ad156a343e1..f5eda8179929 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -69,9 +67,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java b/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java index fbb51636253a..a31907977dfb 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; @@ -65,9 +63,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java index 97f598d4abb4..aecccee067e4 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -69,9 +67,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MinExample.java b/examples/java/src/main/java/org/apache/beam/examples/MinExample.java index db46f1323121..a76bcdc5ee3f 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MinExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MinExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; @@ -65,9 +63,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java index e8c3c51d3c23..d3c0312feaf3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -69,9 +67,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("LogOutput: {} {}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("LogOutput: {} {}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java b/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java index c202aedf76c0..b34f2bdd16bf 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java @@ -32,8 +32,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Partition; import org.apache.beam.sdk.values.PCollection; @@ -194,9 +192,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java b/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java index 57b7a0848499..3ec8fcec0bd8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java @@ -31,8 +31,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.checkerframework.checker.nullness.qual.Nullable; @@ -100,8 +98,8 @@ public void teardown() { } @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) - throws Exception { + public void processElement(ProcessContext c) throws Exception { + String element = c.element(); try { Preconditions.checkNotNull(rateLimiter).allow(1); } catch (Exception e) { @@ -111,7 +109,7 @@ public void processElement(@Element String element, OutputReceiver recei // Simulate external API call LOG.info("Processing: {}", element); Thread.sleep(100); - receiver.output("Processed: " + element); + c.output("Processed: " + element); } } // [END RateLimiterSimpleJava] diff --git a/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java b/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java index d80bc9fedbaa..a0d467718bed 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Regex; import org.apache.beam.sdk.values.PCollection; @@ -77,9 +75,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java b/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java index 8d9db0cf3a86..ed1d90606d3b 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sample; import org.apache.beam.sdk.values.KV; @@ -78,9 +76,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/SumExample.java b/examples/java/src/main/java/org/apache/beam/examples/SumExample.java index a571d8dc5596..00fcc8697926 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SumExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SumExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.values.PCollection; @@ -65,9 +63,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java index a2c92ef0c80e..45d4a9ffd852 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.values.KV; @@ -69,9 +67,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java b/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java index 54e783231908..23e3db6cfd96 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.ToString; import org.apache.beam.sdk.values.KV; @@ -76,9 +74,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/TopExample.java b/examples/java/src/main/java/org/apache/beam/examples/TopExample.java index f602aa345569..520af0f66550 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/TopExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/TopExample.java @@ -23,8 +23,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Top; import org.apache.beam.sdk.values.PCollection; @@ -66,9 +64,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java b/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java index 1b9839ac03f0..3fc9e84fcb39 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java @@ -22,8 +22,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Values; import org.apache.beam.sdk.values.KV; @@ -72,9 +70,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java b/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java index d40d6cc333ef..01c1b1c3f31d 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java @@ -23,9 +23,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.View; import org.apache.beam.sdk.values.KV; @@ -86,8 +83,10 @@ public static void main(String[] args) { @ProcessElement public void processElement( @Element KV person, - @SideInput("citiesToCountries") Map citiesToCountries, - OutputReceiver> out) { + OutputReceiver> out, + ProcessContext context) { + Map citiesToCountries = + context.sideInput(citiesToCountriesView); String city = person.getValue(); String country = citiesToCountries.get(city); if (country == null) { @@ -96,7 +95,7 @@ public void processElement( out.output(KV.of(person.getKey(), country)); } }) - .withSideInput("citiesToCountries", citiesToCountriesView)); + .withSideInputs(citiesToCountriesView)); // [END main_section] output.apply("Log", ParDo.of(new LogOutput<>("Output: "))); @@ -113,11 +112,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement( - @Element KV element, OutputReceiver> receiver) - throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java b/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java index d6a3dff2fa04..244a42bc9613 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java @@ -25,8 +25,6 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.FixedWindows; import org.apache.beam.sdk.transforms.windowing.Window; @@ -89,9 +87,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) throws Exception { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) throws Exception { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java b/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java index 24a3c1b16dcf..11114043e830 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java @@ -52,8 +52,6 @@ import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Filter; import org.apache.beam.sdk.transforms.Flatten; import org.apache.beam.sdk.transforms.PTransform; @@ -133,11 +131,10 @@ public PCollection>> expand(PCollection, CompletionCandidate>() { @ProcessElement - public void processElement( - @Element KV element, - OutputReceiver receiver) { - receiver.output( - new CompletionCandidate(element.getKey(), element.getValue())); + public void processElement(ProcessContext c) { + c.output( + new CompletionCandidate( + c.element().getKey(), c.element().getValue())); } })); @@ -213,11 +210,9 @@ public int partitionFor(KV> elem, int numParti private static class FlattenTops extends DoFn>, CompletionCandidate> { @ProcessElement - public void processElement( - @Element KV> element, - OutputReceiver receiver) { - for (CompletionCandidate cc : element.getValue()) { - receiver.output(cc); + public void processElement(ProcessContext c) { + for (CompletionCandidate cc : c.element().getValue()) { + c.output(cc); } } } @@ -272,12 +267,10 @@ public AllPrefixes(int minPrefix, int maxPrefix) { } @ProcessElement - public void processElement( - @Element CompletionCandidate element, - OutputReceiver> receiver) { - String word = element.value; + public void processElement(ProcessContext c) { + String word = c.element().value; for (int i = minPrefix; i <= Math.min(word.length(), maxPrefix); i++) { - receiver.output(KV.of(word.substring(0, i), element)); + c.output(KV.of(word.substring(0, i), c.element())); } } } @@ -339,24 +332,23 @@ public String toString() { /** Takes as input a set of strings, and emits each #hashtag found therein. */ static class ExtractHashtags extends DoFn { @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) { - Matcher m = Pattern.compile("#\\S+").matcher(element); + public void processElement(ProcessContext c) { + Matcher m = Pattern.compile("#\\S+").matcher(c.element()); while (m.find()) { - receiver.output(m.group().substring(1)); + c.output(m.group().substring(1)); } } } static class FormatForBigquery extends DoFn>, TableRow> { @ProcessElement - public void processElement( - @Element KV> element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { List completions = new ArrayList<>(); - for (CompletionCandidate cc : element.getValue()) { + for (CompletionCandidate cc : c.element().getValue()) { completions.add(new TableRow().set("count", cc.getCount()).set("tag", cc.getValue())); } - TableRow row = new TableRow().set("prefix", element.getKey()).set("tags", completions); - receiver.output(row); + TableRow row = new TableRow().set("prefix", c.element().getKey()).set("tags", completions); + c.output(row); } /** Defines the BigQuery schema used for the output. */ @@ -394,16 +386,15 @@ public FormatForDatastore(String kind, String ancestorKey) { } @ProcessElement - public void processElement( - @Element KV> element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { Entity.Builder entityBuilder = Entity.newBuilder(); com.google.datastore.v1.Key key = - makeKey(makeKey(kind, ancestorKey).build(), kind, element.getKey()).build(); + makeKey(makeKey(kind, ancestorKey).build(), kind, c.element().getKey()).build(); entityBuilder.setKey(key); List candidates = new ArrayList<>(); Map properties = new HashMap<>(); - for (CompletionCandidate tag : element.getValue()) { + for (CompletionCandidate tag : c.element().getValue()) { Entity.Builder tagEntity = Entity.newBuilder(); properties.put("tag", makeValue(tag.value).build()); properties.put("count", makeValue(tag.count).build()); @@ -411,7 +402,7 @@ public void processElement( } properties.put("candidates", makeValue(candidates).build()); entityBuilder.putAllProperties(properties); - receiver.output(entityBuilder.build()); + c.output(entityBuilder.build()); } } @@ -536,12 +527,11 @@ public static void runAutocompletePipeline(Options options) throws IOException { ParDo.of( new DoFn>, Long>() { @ProcessElement - public void process( - @Element KV> elm, - OutputReceiver receiver) { + public void process(ProcessContext c) { + KV> elm = c.element(); Long listHash = - elm.getValue().stream().mapToLong(cc -> cc.hashCode()).sum(); - receiver.output(Long.valueOf(elm.getKey().hashCode()) + listHash); + c.element().getValue().stream().mapToLong(cc -> cc.hashCode()).sum(); + c.output(Long.valueOf(elm.getKey().hashCode()) + listHash); } })) .apply(Sum.longsGlobally()); diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java b/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java index ea5f2e5399e7..e4ce5e3eb17e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java @@ -34,8 +34,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; /** @@ -57,12 +55,12 @@ public class StreamingWordExtract { /** A {@link DoFn} that tokenizes lines of text into individual words. */ static class ExtractWords extends DoFn { @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) { - String[] words = element.split(ExampleUtils.TOKENIZER_PATTERN, -1); + public void processElement(ProcessContext c) { + String[] words = c.element().split(ExampleUtils.TOKENIZER_PATTERN, -1); for (String word : words) { if (!word.isEmpty()) { - receiver.output(word); + c.output(word); } } } @@ -71,8 +69,8 @@ public void processElement(@Element String element, OutputReceiver recei /** A {@link DoFn} that uppercases a word. */ static class Uppercase extends DoFn { @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) { - receiver.output(element.toUpperCase()); + public void processElement(ProcessContext c) { + c.output(c.element().toUpperCase()); } } @@ -80,8 +78,8 @@ public void processElement(@Element String element, OutputReceiver recei static class StringToRowConverter extends DoFn { /** In this example, put the whole string into single BigQuery field. */ @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) { - receiver.output(new TableRow().set("string_field", element)); + public void processElement(ProcessContext c) { + c.output(new TableRow().set("string_field", c.element())); } static TableSchema getSchema() { diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java index a61bd4c88651..b3e5fd04fa7e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java @@ -56,9 +56,6 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Distinct; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.Flatten; import org.apache.beam.sdk.transforms.Keys; import org.apache.beam.sdk.transforms.PTransform; @@ -239,11 +236,9 @@ public PCollection>> expand( ParDo.of( new DoFn, KV>() { @ProcessElement - public void processElement( - @Element KV element, - OutputReceiver> receiver) { - URI uri = element.getKey(); - String line = element.getValue(); + public void processElement(ProcessContext c) { + URI uri = c.element().getKey(); + String line = c.element().getValue(); for (String word : line.split("\\W+", -1)) { // Log INFO messages when the word “love” is found. if ("love".equalsIgnoreCase(word)) { @@ -251,7 +246,7 @@ public void processElement( } if (!word.isEmpty()) { - receiver.output(KV.of(uri, word.toLowerCase())); + c.output(KV.of(uri, word.toLowerCase())); } } } @@ -286,13 +281,11 @@ public void processElement( ParDo.of( new DoFn, Long>, KV>>() { @ProcessElement - public void processElement( - @Element KV, Long> element, - OutputReceiver>> receiver) { - URI uri = element.getKey().getKey(); - String word = element.getKey().getValue(); - Long occurrences = element.getValue(); - receiver.output(KV.of(uri, KV.of(word, occurrences))); + public void processElement(ProcessContext c) { + URI uri = c.element().getKey().getKey(); + String word = c.element().getKey().getValue(); + Long occurrences = c.element().getValue(); + c.output(KV.of(uri, KV.of(word, occurrences))); } })); @@ -329,18 +322,16 @@ public void processElement( ParDo.of( new DoFn, KV>>() { @ProcessElement - public void processElement( - @Element KV element, - OutputReceiver>> receiver) { - URI uri = element.getKey(); - Long wordTotal = element.getValue().getOnly(wordTotalsTag); + public void processElement(ProcessContext c) { + URI uri = c.element().getKey(); + Long wordTotal = c.element().getValue().getOnly(wordTotalsTag); for (KV wordAndCount : - element.getValue().getAll(wordCountsTag)) { + c.element().getValue().getAll(wordCountsTag)) { String word = wordAndCount.getKey(); Long wordCount = wordAndCount.getValue(); Double termFrequency = wordCount.doubleValue() / wordTotal.doubleValue(); - receiver.output(KV.of(word, KV.of(uri, termFrequency))); + c.output(KV.of(word, KV.of(uri, termFrequency))); } } })); @@ -357,19 +348,17 @@ public void processElement( ParDo.of( new DoFn, KV>() { @ProcessElement - public void processElement( - @SideInput("totalDocuments") Long documentTotal, - @Element KV element, - OutputReceiver> receiver) { - String word = element.getKey(); - Long documentCount = element.getValue(); + public void processElement(ProcessContext c) { + String word = c.element().getKey(); + Long documentCount = c.element().getValue(); + Long documentTotal = c.sideInput(totalDocuments); Double documentFrequency = documentCount.doubleValue() / documentTotal.doubleValue(); - receiver.output(KV.of(word, documentFrequency)); + c.output(KV.of(word, documentFrequency)); } }) - .withSideInput("totalDocuments", totalDocuments)); + .withSideInputs(totalDocuments)); // Join the term frequency and document frequency // collections, each keyed on the word. @@ -391,17 +380,15 @@ public void processElement( ParDo.of( new DoFn, KV>>() { @ProcessElement - public void processElement( - @Element KV element, - OutputReceiver>> receiver) { - String word = element.getKey(); - Double df = element.getValue().getOnly(dfTag); + public void processElement(ProcessContext c) { + String word = c.element().getKey(); + Double df = c.element().getValue().getOnly(dfTag); - for (KV uriAndTf : element.getValue().getAll(tfTag)) { + for (KV uriAndTf : c.element().getValue().getAll(tfTag)) { URI uri = uriAndTf.getKey(); Double tf = uriAndTf.getValue(); Double tfIdf = tf * Math.log(1 / df); - receiver.output(KV.of(word, KV.of(uri, tfIdf))); + c.output(KV.of(word, KV.of(uri, tfIdf))); } } })); @@ -432,15 +419,13 @@ public PDone expand(PCollection>> wordToUriAndTfIdf) ParDo.of( new DoFn>, String>() { @ProcessElement - public void processElement( - @Element KV> element, - OutputReceiver receiver) { - receiver.output( + public void processElement(ProcessContext c) { + c.output( String.format( "%s,\t%s,\t%f", - element.getKey(), - element.getValue().getKey(), - element.getValue().getValue())); + c.element().getKey(), + c.element().getValue().getKey(), + c.element().getValue().getValue())); } })) .apply(TextIO.write().to(output).withSuffix(".csv")); diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java index d156777e0f97..b06cd8da9d43 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java @@ -48,8 +48,6 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -95,7 +93,8 @@ public class TopWikipediaSessions { /** Extracts user and timestamp from a TableRow representing a Wikipedia edit. */ static class ExtractUserAndTimestamp extends DoFn { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); int timestamp; // TODO(BEAM-5390): Avoid this workaround. try { @@ -106,7 +105,7 @@ public void processElement(@Element TableRow row, OutputReceiver receive String userName = (String) row.get("contributor_username"); if (userName != null) { // Sets the implicit timestamp field to be used in windowing. - receiver.outputWithTimestamp(userName, new Instant(timestamp * 1000L)); + c.outputWithTimestamp(userName, new Instant(timestamp * 1000L)); } } } @@ -144,24 +143,18 @@ public PCollection>> expand(PCollection> static class SessionsToStringsDoFn extends DoFn, KV> { @ProcessElement - public void processElement( - BoundedWindow window, - @Element KV element, - OutputReceiver> receiver) { - receiver.output(KV.of(element.getKey() + " : " + window, element.getValue())); + public void processElement(ProcessContext c, BoundedWindow window) { + c.output(KV.of(c.element().getKey() + " : " + window, c.element().getValue())); } } static class FormatOutputDoFn extends DoFn>, String> { @ProcessElement - public void processElement( - BoundedWindow window, - @Element List> element, - OutputReceiver receiver) { - for (KV item : element) { + public void processElement(ProcessContext c, BoundedWindow window) { + for (KV item : c.element()) { String session = item.getKey(); long count = item.getValue(); - receiver.output(session + " : " + count + " : " + ((IntervalWindow) window).start()); + c.output(session + " : " + count + " : " + ((IntervalWindow) window).start()); } } } @@ -194,11 +187,10 @@ public PCollection expand(PCollection input) { ParDo.of( new DoFn() { @ProcessElement - public void processElement( - @Element String element, OutputReceiver receiver) { - if (Math.abs((long) element.hashCode()) + public void processElement(ProcessContext c) { + if (Math.abs((long) c.element().hashCode()) <= Integer.MAX_VALUE * samplingThreshold) { - receiver.output(element); + c.output(c.element()); } } })) diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java index b22e3fed6698..6f75e2e03d99 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java @@ -58,9 +58,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SerializableFunction; @@ -185,14 +182,13 @@ static class ExtractTimestamps extends DoFn { DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss"); @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) - throws Exception { - String[] items = element.split(",", -1); + public void processElement(DoFn.ProcessContext c) throws Exception { + String[] items = c.element().split(",", -1); if (items.length > 0) { try { String timestamp = items[0]; - receiver.outputWithTimestamp(element, new Instant(dateTimeFormat.parseMillis(timestamp))); + c.outputWithTimestamp(c.element(), new Instant(dateTimeFormat.parseMillis(timestamp))); } catch (IllegalArgumentException e) { // Skip the invalid input. } @@ -210,9 +206,8 @@ public void processElement(@Element String element, OutputReceiver recei static class ExtractFlowInfoFn extends DoFn> { @ProcessElement - public void processElement( - @Element String element, OutputReceiver> receiver) { - String[] items = element.split(",", -1); + public void processElement(ProcessContext c) { + String[] items = c.element().split(",", -1); if (items.length < 48) { // Skip the invalid input. return; @@ -241,7 +236,7 @@ public void processElement( laneAvgOccupancy, laneAvgSpeed, totalFlow); - receiver.output(KV.of(stationId, laneInfo)); + c.output(KV.of(stationId, laneInfo)); } } } @@ -275,15 +270,12 @@ public LaneInfo apply(Iterable input) { */ static class FormatMaxesFn extends DoFn, TableRow> { @ProcessElement - public void processElement( - @Element KV element, - @Timestamp Instant timestamp, - OutputReceiver receiver) { + public void processElement(ProcessContext c) { - LaneInfo laneInfo = element.getValue(); + LaneInfo laneInfo = c.element().getValue(); TableRow row = new TableRow() - .set("station_id", element.getKey()) + .set("station_id", c.element().getKey()) .set("direction", laneInfo.getDirection()) .set("freeway", laneInfo.getFreeway()) .set("lane_max_flow", laneInfo.getLaneFlow()) @@ -292,8 +284,8 @@ public void processElement( .set("avg_speed", laneInfo.getLaneAS()) .set("total_flow", laneInfo.getTotalFlow()) .set("recorded_timestamp", laneInfo.getRecordedTimestamp()) - .set("window_timestamp", timestamp.toString()); - receiver.output(row); + .set("window_timestamp", c.timestamp().toString()); + c.output(row); } /** Defines the BigQuery schema used for the output. */ diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java index 38ba4322f3e6..958415626863 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java @@ -63,9 +63,6 @@ import org.apache.beam.sdk.options.Description; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.GroupByKey; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -190,13 +187,12 @@ static class ExtractTimestamps extends DoFn { DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss"); @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) - throws Exception { - String[] items = element.split(","); + public void processElement(DoFn.ProcessContext c) throws Exception { + String[] items = c.element().split(","); String timestamp = tryParseTimestamp(items); if (timestamp != null) { try { - receiver.outputWithTimestamp(element, new Instant(dateTimeFormat.parseMillis(timestamp))); + c.outputWithTimestamp(c.element(), new Instant(dateTimeFormat.parseMillis(timestamp))); } catch (IllegalArgumentException e) { // Skip the invalid input. } @@ -211,11 +207,8 @@ public void processElement(@Element String element, OutputReceiver recei static class ExtractStationSpeedFn extends DoFn> { @ProcessElement - public void processElement( - @Timestamp Instant timestamp, - @Element String element, - OutputReceiver> receiver) { - String[] items = element.split(","); + public void processElement(ProcessContext c) { + String[] items = c.element().split(","); String stationType = tryParseStationType(items); // For this analysis, use only 'main line' station types if ("ML".equals(stationType)) { @@ -223,10 +216,11 @@ public void processElement( String stationId = tryParseStationId(items); // For this simple example, filter out everything but some hardwired routes. if (avgSpeed != null && stationId != null && sdStations.containsKey(stationId)) { - StationSpeed stationSpeed = new StationSpeed(stationId, avgSpeed, timestamp.getMillis()); + StationSpeed stationSpeed = + new StationSpeed(stationId, avgSpeed, c.timestamp().getMillis()); // The tuple key is the 'route' name stored in the 'sdStations' hash. KV outputValue = KV.of(sdStations.get(stationId), stationSpeed); - receiver.output(outputValue); + c.output(outputValue); } } } @@ -240,16 +234,13 @@ public void processElement( */ static class GatherStats extends DoFn>, KV> { @ProcessElement - public void processElement( - @Element KV> element, - OutputReceiver> receiver) - throws IOException { - String route = element.getKey(); + public void processElement(ProcessContext c) throws IOException { + String route = c.element().getKey(); double speedSum = 0.0; int speedCount = 0; int speedups = 0; int slowdowns = 0; - List infoList = Lists.newArrayList(element.getValue()); + List infoList = Lists.newArrayList(c.element().getValue()); // StationSpeeds sort by embedded timestamp. Collections.sort(infoList); Map prevSpeeds = new HashMap<>(); @@ -277,25 +268,22 @@ public void processElement( double speedAvg = speedSum / speedCount; boolean slowdownEvent = slowdowns >= 2 * speedups; RouteInfo routeInfo = new RouteInfo(route, speedAvg, slowdownEvent); - receiver.output(KV.of(route, routeInfo)); + c.output(KV.of(route, routeInfo)); } } /** Format the results of the slowdown calculations to a TableRow, to save to BigQuery. */ static class FormatStatsFn extends DoFn, TableRow> { @ProcessElement - public void processElement( - @Element KV element, - @Timestamp Instant timestamp, - OutputReceiver receiver) { - RouteInfo routeInfo = element.getValue(); + public void processElement(ProcessContext c) { + RouteInfo routeInfo = c.element().getValue(); TableRow row = new TableRow() .set("avg_speed", routeInfo.getAvgSpeed()) .set("slowdown_event", routeInfo.getSlowdownEvent()) - .set("route", element.getKey()) - .set("window_timestamp", timestamp.toString()); - receiver.output(row); + .set("route", c.element().getKey()) + .set("window_timestamp", c.timestamp().toString()); + c.output(row); } /** Defines the BigQuery schema used for the output. */ diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java index 6e5321dc54ab..cf097c8ea979 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java @@ -37,9 +37,6 @@ import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.MultiOutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.GroupIntoBatches; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -202,24 +199,20 @@ public void close() { @ProcessElement @SuppressWarnings("argument") - public void process( - @Element KV> element, - OutputReceiver mainReceiver, - MultiOutputReceiver multiReceiver) { + public void process(@Element KV> element, ProcessContext context) { Iterable rows = element.getValue(); try { for (Row outputRow : getTokenizedRow(rows)) { - mainReceiver.output(outputRow); + context.output(outputRow); } } catch (Exception e) { for (Row outputRow : rows) { - multiReceiver - .get(failureTag) - .output( - FailsafeElement.of(outputRow, outputRow) - .setErrorMessage(e.getMessage()) - .setStacktrace(Throwables.getStackTraceAsString(e))); + context.output( + failureTag, + FailsafeElement.of(outputRow, outputRow) + .setErrorMessage(e.getMessage()) + .setStacktrace(Throwables.getStackTraceAsString(e))); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java index 667e26fa9c2d..fe8f4c1afad8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java @@ -26,8 +26,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy; import org.apache.beam.sdk.io.gcp.bigquery.WriteResult; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; @@ -92,8 +90,9 @@ public static FailsafeElement wrapBigQueryInsertError( public static class RowToTableRowFn extends DoFn { @ProcessElement - public void processElement(@Element Row row, OutputReceiver receiver) { - receiver.output(BigQueryUtils.toTableRow(row)); + public void processElement(ProcessContext context) { + Row row = context.element(); + context.output(BigQueryUtils.toTableRow(row)); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java index 435620a4b011..d7d1c3e97232 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java @@ -75,10 +75,8 @@ static class TransformToBigTableFormat extends DoFn>> out, - PipelineOptions pipelineOptions) { - DataTokenizationOptions options = pipelineOptions.as(DataTokenizationOptions.class); + @Element Row in, OutputReceiver>> out, ProcessContext c) { + DataTokenizationOptions options = c.getPipelineOptions().as(DataTokenizationOptions.class); // Mapping every field in provided Row to Mutation.SetCell, which will create/update // cell content with provided data Set mutations = diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java index 9fead4e1a7fd..d827e4b30cb3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java @@ -264,6 +264,8 @@ public static Builder newBuilder() { @Override public PCollectionTuple expand(PCollectionTuple lines) { + PCollectionView headersView = null; + // Convert csv lines into Failsafe elements so that we can recover over multiple transforms. PCollection> lineFailsafeElements = lines @@ -283,14 +285,16 @@ public PCollectionTuple expand(PCollectionTuple lines) { return lineFailsafeElements.apply( "LineToDocumentUsingSchema", - ParDo.of(new FailsafeElementToJsonFn(schema, delimiter(), udfDeadletterTag())) + ParDo.of( + new FailsafeElementToJsonFn( + headersView, schema, delimiter(), udfDeadletterTag())) .withOutputTags(udfOutputTag(), TupleTagList.of(udfDeadletterTag()))); } // Run if using headers - PCollectionView headersView = - lines.get(headerTag()).apply(Sample.any(1)).apply(View.asSingleton()); + headersView = lines.get(headerTag()).apply(Sample.any(1)).apply(View.asSingleton()); + PCollectionView finalHeadersView = headersView; lines .get(headerTag()) .apply( @@ -298,24 +302,23 @@ public PCollectionTuple expand(PCollectionTuple lines) { ParDo.of( new DoFn() { @ProcessElement - public void processElement( - @SideInput("finalHeadersView") String headers, - @Element String element) { - if (!element.equals(headers)) { + public void processElement(ProcessContext c) { + String headers = c.sideInput(finalHeadersView); + if (!c.element().equals(headers)) { LOG.error("Headers do not match, consistency cannot be guaranteed"); throw new RuntimeException( "Headers do not match, consistency cannot be guaranteed"); } } }) - .withSideInput("finalHeadersView", headersView)); + .withSideInputs(finalHeadersView)); return lineFailsafeElements.apply( "LineToDocumentWithHeaders", ParDo.of( - new FailsafeElementToJsonWithHeadersFn( - jsonSchemaPath(), delimiter(), udfDeadletterTag())) - .withSideInput("finalHeadersView", headersView) + new FailsafeElementToJsonFn( + headersView, jsonSchemaPath(), delimiter(), udfDeadletterTag())) + .withSideInputs(headersView) .withOutputTags(udfOutputTag(), TupleTagList.of(udfDeadletterTag()))); } @@ -354,90 +357,45 @@ public static class FailsafeElementToJsonFn @Nullable public final String jsonSchema; public final String delimiter; public final TupleTag> udfDeadletterTag; + @Nullable private final PCollectionView headersView; private Counter successCounter = Metrics.counter(FailsafeElementToJsonFn.class, SUCCESSFUL_TO_JSON_COUNTER); private Counter failedCounter = Metrics.counter(FailsafeElementToJsonFn.class, FAILED_TO_JSON_COUNTER); FailsafeElementToJsonFn( + PCollectionView headersView, String jsonSchema, String delimiter, TupleTag> udfDeadletterTag) { + this.headersView = headersView; this.jsonSchema = jsonSchema; this.delimiter = delimiter; this.udfDeadletterTag = udfDeadletterTag; } @ProcessElement - public void processElement( - @Element FailsafeElement element, - OutputReceiver> receiver, - MultiOutputReceiver multiReceiver) { - List header = null; - List record = Arrays.asList(element.getOriginalPayload().split(this.delimiter)); - - try { - String json = buildJsonString(header, record, this.jsonSchema); - receiver.output(FailsafeElement.of(element.getOriginalPayload(), json)); - successCounter.inc(); - } catch (Exception e) { - failedCounter.inc(); - multiReceiver - .get(this.udfDeadletterTag) - .output( - FailsafeElement.of(element) - .setErrorMessage(e.getMessage()) - .setStacktrace(Throwables.getStackTraceAsString(e))); - } - } - } - - public static class FailsafeElementToJsonWithHeadersFn - extends DoFn, FailsafeElement> { - - @Nullable public final String jsonSchema; - public final String delimiter; - public final TupleTag> udfDeadletterTag; - private Counter successCounter = - Metrics.counter(FailsafeElementToJsonWithHeadersFn.class, SUCCESSFUL_TO_JSON_COUNTER); - private Counter failedCounter = - Metrics.counter(FailsafeElementToJsonWithHeadersFn.class, FAILED_TO_JSON_COUNTER); - - FailsafeElementToJsonWithHeadersFn( - String jsonSchema, - String delimiter, - TupleTag> udfDeadletterTag) { - this.jsonSchema = jsonSchema; - this.delimiter = delimiter; - this.udfDeadletterTag = udfDeadletterTag; - } - - @ProcessElement - public void processElement( - @Element FailsafeElement element, - OutputReceiver> receiver, - MultiOutputReceiver multiReceiver, - @SideInput("finalHeadersView") String headersStr) { + public void processElement(ProcessContext context) { + FailsafeElement element = context.element(); List header = null; - if (headersStr != null) { - header = Arrays.asList(headersStr.split(this.delimiter)); + if (this.headersView != null) { + header = Arrays.asList(context.sideInput(this.headersView).split(this.delimiter)); } List record = Arrays.asList(element.getOriginalPayload().split(this.delimiter)); try { String json = buildJsonString(header, record, this.jsonSchema); - receiver.output(FailsafeElement.of(element.getOriginalPayload(), json)); + context.output(FailsafeElement.of(element.getOriginalPayload(), json)); successCounter.inc(); } catch (Exception e) { failedCounter.inc(); - multiReceiver - .get(this.udfDeadletterTag) - .output( - FailsafeElement.of(element) - .setErrorMessage(e.getMessage()) - .setStacktrace(Throwables.getStackTraceAsString(e))); + context.output( + this.udfDeadletterTag, + FailsafeElement.of(element) + .setErrorMessage(e.getMessage()) + .setStacktrace(Throwables.getStackTraceAsString(e))); } } } @@ -449,9 +407,9 @@ public void processElement( static class LineToFailsafeElementFn extends DoFn> { @ProcessElement - public void processElement( - @Element String message, OutputReceiver> receiver) { - receiver.output(FailsafeElement.of(message, message)); + public void processElement(ProcessContext context) { + String message = context.element(); + context.output(FailsafeElement.of(message, message)); } } @@ -552,12 +510,13 @@ static class GetCsvHeadersFn extends DoFn { } @ProcessElement - public void processElement(@Element ReadableFile file, MultiOutputReceiver outputReceiver) { + public void processElement(ProcessContext context, MultiOutputReceiver outputReceiver) { + ReadableFile f = context.element(); String headers; List records = null; String delimiter = String.valueOf(this.csvFormat.getDelimiter()); try { - String csvFileString = file.readFullyAsUTF8String(); + String csvFileString = f.readFullyAsUTF8String(); StringReader reader = new StringReader(csvFileString); CSVParser parser = CSVParser.parse(reader, this.csvFormat.withFirstRecordAsHeader()); records = diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java index 99981cb17b84..01377add0858 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java @@ -29,9 +29,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.bigquery.WriteResult; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -46,7 +43,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.DateTimeZone; import org.joda.time.Duration; -import org.joda.time.Instant; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -127,17 +123,16 @@ public FailedStringToCsvRowFn() { } @ProcessElement - public void processElement( - @Element FailsafeElement failsafeElement, - @Timestamp Instant timestamp, - OutputReceiver receiver) { + public void processElement(ProcessContext context) { + FailsafeElement failsafeElement = context.element(); ArrayList outputRow = new ArrayList<>(); final String message = failsafeElement.getOriginalPayload(); // Format the timestamp for insertion - String timestampStr = TIMESTAMP_FORMATTER.print(timestamp.toDateTime(DateTimeZone.UTC)); + String timestamp = + TIMESTAMP_FORMATTER.print(context.timestamp().toDateTime(DateTimeZone.UTC)); - outputRow.add(timestampStr); + outputRow.add(timestamp); outputRow.add(MoreObjects.firstNonNull(failsafeElement.getErrorMessage(), "")); // Only set the payload if it's populated on the message. @@ -145,7 +140,7 @@ public void processElement( outputRow.add(message); } - receiver.output(String.join(csvDelimiter, outputRow)); + context.output(String.join(csvDelimiter, outputRow)); } } @@ -203,20 +198,19 @@ public static class FailedStringToTableRowFn DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"); @ProcessElement - public void processElement( - @Timestamp Instant timestamp, - @Element FailsafeElement failsafeElement, - OutputReceiver receiver) { + public void processElement(ProcessContext context) { + FailsafeElement failsafeElement = context.element(); final String message = failsafeElement.getOriginalPayload(); // Format the timestamp for insertion - String timestampStr = TIMESTAMP_FORMATTER.print(timestamp.toDateTime(DateTimeZone.UTC)); + String timestamp = + TIMESTAMP_FORMATTER.print(context.timestamp().toDateTime(DateTimeZone.UTC)); // Build the table row @SuppressWarnings("nullness") // TableRow.set not annotated but does accept nulls final TableRow failedRow = new TableRow() - .set("timestamp", timestampStr) + .set("timestamp", timestamp) .set("errorMessage", failsafeElement.getErrorMessage()) .set("stacktrace", failsafeElement.getStacktrace()); @@ -227,7 +221,7 @@ public void processElement( .set("payloadBytes", message.getBytes(StandardCharsets.UTF_8)); } - receiver.output(failedRow); + context.output(failedRow); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java index 0018201f3686..a3ed04bb1c48 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java @@ -34,9 +34,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.PTransform; @@ -129,23 +126,21 @@ public PCollection> expand(PCollection> Metrics.counter("main", "SpammerUsers"); @ProcessElement - public void processElement( - @SideInput("globalMeanScore") Double gmc, - @Element KV element, - OutputReceiver> receiver) { - Integer score = element.getValue(); + public void processElement(ProcessContext c) { + Integer score = c.element().getValue(); + Double gmc = c.sideInput(globalMeanScore); if (score > (gmc * SCORE_WEIGHT)) { LOG.info( "user {} spammer score {} with mean {}", - element.getKey(), + c.element().getKey(), score, gmc); numSpammerUsers.inc(); - receiver.output(element); + c.output(c.element()); } } }) - .withSideInput("globalMeanScore", globalMeanScore)); + .withSideInputs(globalMeanScore)); return filtered; } } @@ -154,10 +149,10 @@ public void processElement( /** Calculate and output an element's session duration. */ private static class UserSessionInfoFn extends DoFn, Integer> { @ProcessElement - public void processElement(BoundedWindow window, OutputReceiver receiver) { + public void processElement(ProcessContext c, BoundedWindow window) { IntervalWindow w = (IntervalWindow) window; int duration = new Duration(w.start(), w.end()).toPeriod().toStandardMinutes().getMinutes(); - receiver.output(duration); + c.output(duration); } } @@ -197,21 +192,22 @@ public interface Options extends LeaderBoard.Options { configureWindowedWrite() { Map>> tableConfigure = new HashMap<>(); tableConfigure.put( - "team", new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); + "team", new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.element().getKey())); tableConfigure.put( - "total_score", new WriteToBigQuery.FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); + "total_score", + new WriteToBigQuery.FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); tableConfigure.put( "window_start", new WriteToBigQuery.FieldInfo<>( "STRING", - (e, w, t, p) -> { + (c, w) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); })); tableConfigure.put( "processing_time", new WriteToBigQuery.FieldInfo<>( - "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); return tableConfigure; } @@ -226,12 +222,12 @@ protected static Map> configureSession "window_start", new WriteToBigQuery.FieldInfo<>( "STRING", - (e, w, t, p) -> { + (c, w) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); })); tableConfigure.put( - "mean_duration", new WriteToBigQuery.FieldInfo<>("FLOAT", (e, w, t, p) -> e)); + "mean_duration", new WriteToBigQuery.FieldInfo<>("FLOAT", (c, w) -> c.element())); return tableConfigure; } @@ -292,17 +288,14 @@ public static void main(String[] args) throws Exception { ParDo.of( new DoFn() { @ProcessElement - public void processElement( - @SideInput("spammersView") Map spammers, - @Element GameActionInfo element, - OutputReceiver receiver) { + public void processElement(ProcessContext c) { // If the user is not in the spammers Map, output the data element. - if (spammers.get(element.getUser().trim()) == null) { - receiver.output(element); + if (c.sideInput(spammersView).get(c.element().getUser().trim()) == null) { + c.output(c.element()); } } }) - .withSideInput("spammersView", spammersView)) + .withSideInputs(spammersView)) // Extract and sum teamname/score pairs from the event data. .apply("ExtractTeamScore", new ExtractAndSumScore("team")) // [END DocInclude_FilterAndCalc] diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java index a94f699282a8..c57d9ba6b8c8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java @@ -112,11 +112,11 @@ public interface Options extends UserScore.Options { */ protected static Map>> configureOutput() { Map>> config = new HashMap<>(); - config.put("team", (e, w, t, p) -> e.getKey()); - config.put("total_score", (e, w, t, p) -> e.getValue()); + config.put("team", (c, w) -> c.element().getKey()); + config.put("total_score", (c, w) -> c.element().getValue()); config.put( "window_start", - (e, w, t, p) -> { + (c, w) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); }); diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java index 3750b33aa791..832c0ad79e76 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java @@ -135,24 +135,25 @@ public interface Options extends ExampleOptions, StreamingOptions { Map>> tableConfigure = new HashMap<>(); tableConfigure.put( - "team", new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); + "team", new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.element().getKey())); tableConfigure.put( - "total_score", new WriteToBigQuery.FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); + "total_score", + new WriteToBigQuery.FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); tableConfigure.put( "window_start", new WriteToBigQuery.FieldInfo<>( "STRING", - (e, w, t, p) -> { + (c, w) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); })); tableConfigure.put( "processing_time", new WriteToBigQuery.FieldInfo<>( - "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); tableConfigure.put( "timing", - new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> p.getTiming().toString())); + new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.pane().getTiming().toString())); return tableConfigure; } @@ -164,9 +165,10 @@ public interface Options extends ExampleOptions, StreamingOptions { configureBigQueryWrite() { Map>> tableConfigure = new HashMap<>(); tableConfigure.put( - "user", new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); + "user", new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.element().getKey())); tableConfigure.put( - "total_score", new WriteToBigQuery.FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); + "total_score", + new WriteToBigQuery.FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); return tableConfigure; } @@ -182,7 +184,7 @@ public interface Options extends ExampleOptions, StreamingOptions { tableConfigure.put( "processing_time", new WriteToBigQuery.FieldInfo<>( - "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); return tableConfigure; } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java index 3caa1e619526..b28db261ab0e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java @@ -37,8 +37,6 @@ import org.apache.beam.sdk.state.StateSpecs; import org.apache.beam.sdk.state.ValueState; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -103,12 +101,12 @@ public interface Options extends LeaderBoard.Options { private static Map>> configureCompleteWindowedTableWrite() { Map>> tableConfigure = new HashMap<>(); - tableConfigure.put("team", new FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); - tableConfigure.put("total_score", new FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); + tableConfigure.put("team", new FieldInfo<>("STRING", (c, w) -> c.element().getKey())); + tableConfigure.put("total_score", new FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); tableConfigure.put( "processing_time", new FieldInfo<>( - "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); return tableConfigure; } @@ -205,11 +203,9 @@ public UpdateTeamScoreFn(int thresholdScore) { */ @ProcessElement public void processElement( - @Element KV element, - @StateId(TOTAL_SCORE) ValueState totalScore, - OutputReceiver> receiver) { - String teamName = element.getKey(); - GameActionInfo gInfo = element.getValue(); + ProcessContext c, @StateId(TOTAL_SCORE) ValueState totalScore) { + String teamName = c.element().getKey(); + GameActionInfo gInfo = c.element().getValue(); // ValueState cells do not contain a default value. If the state is possibly not written, make // sure to check for null on read. @@ -222,7 +218,7 @@ public void processElement( // the new total is 2002, and the threshold is 1000, 1999 / 1000 = 1, 2002 / 1000 = 2. // Therefore, this team passed the threshold. if (oldTotalScore / this.thresholdScore < totalScore.read() / this.thresholdScore) { - receiver.output(KV.of(teamName, totalScore.read())); + c.output(KV.of(teamName, totalScore.read())); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java index 054ce7a52935..b30b4665d265 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java @@ -34,8 +34,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -163,18 +161,18 @@ static class ParseEventFn extends DoFn { private final Counter numParseErrors = Metrics.counter("main", "ParseErrors"); @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) { - String[] components = element.split(",", -1); + public void processElement(ProcessContext c) { + String[] components = c.element().split(",", -1); try { String user = components[0].trim(); String team = components[1].trim(); Integer score = Integer.parseInt(components[2].trim()); Long timestamp = Long.parseLong(components[3].trim()); GameActionInfo gInfo = new GameActionInfo(user, team, score, timestamp); - receiver.output(gInfo); + c.output(gInfo); } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) { numParseErrors.inc(); - LOG.info("Parse error on {}", element, e); + LOG.info("Parse error on {}", c.element(), e); } } } @@ -234,8 +232,8 @@ public interface Options extends PipelineOptions { */ protected static Map>> configureOutput() { Map>> config = new HashMap<>(); - config.put("user", (e, w, t, p) -> e.getKey()); - config.put("total_score", (e, w, t, p) -> e.getValue()); + config.put("user", (c, w) -> c.element().getKey()); + config.put("total_score", (c, w) -> c.element().getValue()); return config; } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java index 5486025083c9..eef4bc932682 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java @@ -30,16 +30,11 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PDone; -import org.joda.time.Instant; /** * Generate, format, and write BigQuery table row information. Use provided information about the @@ -69,11 +64,11 @@ public WriteToBigQuery( } /** - * A {@link Serializable} function from an element and {@link BoundedWindow} to the value for that - * field. + * A {@link Serializable} function from a {@link DoFn.ProcessContext} and {@link BoundedWindow} to + * the value for that field. */ public interface FieldFn extends Serializable { - Object apply(InputT element, BoundedWindow window, Instant timestamp, PaneInfo pane); + Object apply(DoFn.ProcessContext context, BoundedWindow window); } /** Define a class to hold information about output table field definitions. */ @@ -101,21 +96,16 @@ FieldFn getFieldFn() { protected class BuildRowFn extends DoFn { @ProcessElement - public void processElement( - @Element InputT element, - @Timestamp Instant timestamp, - PaneInfo pane, - BoundedWindow window, - OutputReceiver receiver) { + public void processElement(ProcessContext c, BoundedWindow window) { TableRow row = new TableRow(); for (Map.Entry> entry : fieldInfo.entrySet()) { String key = entry.getKey(); FieldInfo fcnInfo = entry.getValue(); FieldFn fcn = fcnInfo.getFieldFn(); - row.set(key, fcn.apply(element, window, timestamp, pane)); + row.set(key, fcn.apply(c, window)); } - receiver.output(row); + c.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java index 35471163c036..330769e0c79e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java @@ -32,9 +32,6 @@ import org.apache.beam.sdk.io.fs.ResolveOptions.StandardResolveOptions; import org.apache.beam.sdk.io.fs.ResourceId; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; @@ -73,32 +70,26 @@ public WriteToText( } /** - * A {@link Serializable} function from an element and {@link BoundedWindow} to the value for that - * field. + * A {@link Serializable} function from a {@link DoFn.ProcessContext} and {@link BoundedWindow} to + * the value for that field. */ public interface FieldFn extends Serializable { - Object apply( - InputT element, BoundedWindow window, org.joda.time.Instant timestamp, PaneInfo pane); + Object apply(DoFn.ProcessContext context, BoundedWindow window); } /** Convert each key/score pair into a row as specified by fieldFn. */ protected class BuildRowFn extends DoFn { @ProcessElement - public void processElement( - @Element InputT element, - @Timestamp org.joda.time.Instant timestamp, - PaneInfo pane, - BoundedWindow window, - OutputReceiver receiver) { + public void processElement(ProcessContext c, BoundedWindow window) { List fields = new ArrayList<>(); for (Map.Entry> entry : fieldFn.entrySet()) { String key = entry.getKey(); FieldFn fcn = entry.getValue(); - fields.add(key + ": " + fcn.apply(element, window, timestamp, pane)); + fields.add(key + ": " + fcn.apply(c, window)); } String result = fields.stream().collect(Collectors.joining(", ")); - receiver.output(result); + c.output(result); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java index 77a59f2c3b6e..36fa18a34e0d 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java @@ -24,12 +24,8 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PDone; @@ -48,20 +44,15 @@ public WriteWindowedToBigQuery( /** Convert each key/score pair into a BigQuery TableRow. */ protected class BuildRowFn extends DoFn { @ProcessElement - public void processElement( - @Element T element, - @Timestamp org.joda.time.Instant timestamp, - PaneInfo pane, - BoundedWindow window, - OutputReceiver receiver) { + public void processElement(ProcessContext c, BoundedWindow window) { TableRow row = new TableRow(); for (Map.Entry> entry : fieldInfo.entrySet()) { String key = entry.getKey(); FieldInfo fcnInfo = entry.getValue(); - row.set(key, fcnInfo.getFieldFn().apply(element, window, timestamp, pane)); + row.set(key, fcnInfo.getFieldFn().apply(c, window)); } - receiver.output(row); + c.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java index d662cde1f000..3b2653b7601e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java @@ -32,9 +32,6 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -88,9 +85,10 @@ public class BigQueryStreamingTornadoes { */ static class ExtractTornadoesFn extends DoFn { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); if (Boolean.TRUE.equals(row.get("tornado"))) { - receiver.output(Integer.parseInt((String) row.get("month"))); + c.output(Integer.parseInt((String) row.get("month"))); } } } @@ -101,16 +99,13 @@ public void processElement(@Element TableRow row, OutputReceiver receiv */ static class FormatCountsFn extends DoFn, TableRow> { @ProcessElement - public void processElement( - @Element KV element, - @Timestamp Instant timestamp, - OutputReceiver receiver) { + public void processElement(ProcessContext c) { TableRow row = new TableRow() - .set("ts", timestamp.toString()) - .set("month", element.getKey()) - .set("tornado_count", element.getValue()); - receiver.output(row); + .set("ts", c.timestamp().toString()) + .set("month", c.element().getKey()) + .set("tornado_count", c.element().getValue()); + c.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java index 67e0661a1a42..43d720e35268 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java @@ -32,8 +32,6 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -81,9 +79,10 @@ public class BigQueryTornadoes { */ static class ExtractTornadoesFn extends DoFn { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); if ((Boolean) row.get("tornado")) { - receiver.output(Integer.parseInt((String) row.get("month"))); + c.output(Integer.parseInt((String) row.get("month"))); } } } @@ -94,11 +93,12 @@ public void processElement(@Element TableRow row, OutputReceiver receiv */ static class FormatCountsFn extends DoFn, TableRow> { @ProcessElement - public void processElement( - @Element KV element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { TableRow row = - new TableRow().set("month", element.getKey()).set("tornado_count", element.getValue()); - receiver.output(row); + new TableRow() + .set("month", c.element().getKey()) + .set("tornado_count", c.element().getValue()); + c.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java index 5f5b5d06b1f8..2a581d769dc7 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java @@ -33,8 +33,6 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SerializableFunction; @@ -80,11 +78,12 @@ static class ExtractLargeWordsFn extends DoFn> { private final Counter smallerWords = Metrics.counter(ExtractLargeWordsFn.class, "smallerWords"); @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver> receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); String playName = (String) row.get("corpus"); String word = (String) row.get("word"); if (word.length() >= MIN_WORD_LENGTH) { - receiver.output(KV.of(word, playName)); + c.output(KV.of(word, playName)); } else { // Track how many smaller words we're not including. This information will be // visible in the Monitoring UI. @@ -99,11 +98,10 @@ public void processElement(@Element TableRow row, OutputReceiver, TableRow> { @ProcessElement - public void processElement( - @Element KV element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { TableRow row = - new TableRow().set("word", element.getKey()).set("all_plays", element.getValue()); - receiver.output(row); + new TableRow().set("word", c.element().getKey()).set("all_plays", c.element().getValue()); + c.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java index 26a6659a86b8..9187bb83d7da 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java @@ -31,9 +31,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -90,7 +87,8 @@ public class FilterExamples { */ static class ProjectionFn extends DoFn { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); // Grab year, month, day, mean_temp from the row Integer year = Integer.parseInt((String) row.get("year")); Integer month = Integer.parseInt((String) row.get("month")); @@ -103,7 +101,7 @@ public void processElement(@Element TableRow row, OutputReceiver recei .set("month", month) .set("day", day) .set("mean_temp", meanTemp); - receiver.output(outRow); + c.output(outRow); } } @@ -121,11 +119,12 @@ public FilterSingleMonthDataFn(Integer monthFilter) { } @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); Integer month; month = (Integer) row.get("month"); if (month.equals(this.monthFilter)) { - receiver.output(row); + c.output(row); } } } @@ -136,9 +135,10 @@ public void processElement(@Element TableRow row, OutputReceiver recei */ static class ExtractTempFn extends DoFn { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); Double meanTemp = Double.parseDouble(row.get("mean_temp").toString()); - receiver.output(meanTemp); + c.output(meanTemp); } } @@ -178,17 +178,16 @@ public PCollection expand(PCollection rows) { ParDo.of( new DoFn() { @ProcessElement - public void processElement( - @SideInput("globalMeanTemp") Double gTemp, - @Element TableRow element, - OutputReceiver receiver) { - Double meanTemp = Double.parseDouble(element.get("mean_temp").toString()); + public void processElement(ProcessContext c) { + Double meanTemp = + Double.parseDouble(c.element().get("mean_temp").toString()); + Double gTemp = c.sideInput(globalMeanTemp); if (meanTemp < gTemp) { - receiver.output(element); + c.output(c.element()); } } }) - .withSideInput("globalMeanTemp", globalMeanTemp)); + .withSideInputs(globalMeanTemp)); return filteredRows; } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java index e6f8573705a2..f78df0c09461 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java @@ -26,8 +26,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.join.CoGbkResult; import org.apache.beam.sdk.transforms.join.CoGroupByKey; @@ -92,14 +90,13 @@ static PCollection joinEvents( ParDo.of( new DoFn, KV>() { @ProcessElement - public void processElement( - @Element KV element, - OutputReceiver> receiver) { - String countryCode = element.getKey(); - String countryName = element.getValue().getOnly(countryInfoTag); - for (String eventInfo : element.getValue().getAll(eventInfoTag)) { + public void processElement(ProcessContext c) { + KV e = c.element(); + String countryCode = e.getKey(); + String countryName = e.getValue().getOnly(countryInfoTag); + for (String eventInfo : c.element().getValue().getAll(eventInfoTag)) { // Generate a string that combines information from both collection values - receiver.output( + c.output( KV.of( countryCode, "Country name: " + countryName + ", Event info: " + eventInfo)); @@ -114,11 +111,10 @@ public void processElement( ParDo.of( new DoFn, String>() { @ProcessElement - public void processElement( - @Element KV element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { String outputstring = - "Country code: " + element.getKey() + ", " + element.getValue(); - receiver.output(outputstring); + "Country code: " + c.element().getKey() + ", " + c.element().getValue(); + c.output(outputstring); } })); return formattedResults; @@ -130,13 +126,14 @@ public void processElement( */ static class ExtractEventDataFn extends DoFn> { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver> receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); String countryCode = (String) row.get("ActionGeo_CountryCode"); String sqlDate = (String) row.get("SQLDATE"); String actor1Name = (String) row.get("Actor1Name"); String sourceUrl = (String) row.get("SOURCEURL"); String eventInfo = "Date: " + sqlDate + ", Actor1: " + actor1Name + ", url: " + sourceUrl; - receiver.output(KV.of(countryCode, eventInfo)); + c.output(KV.of(countryCode, eventInfo)); } } @@ -146,10 +143,11 @@ public void processElement(@Element TableRow row, OutputReceiver> { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver> receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); String countryCode = (String) row.get("FIPSCC"); String countryName = (String) row.get("HumanName"); - receiver.output(KV.of(countryCode, countryName)); + c.output(KV.of(countryCode, countryName)); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java index 85df56a58258..8760d562d040 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java @@ -30,8 +30,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -75,22 +73,23 @@ public class MaxPerKeyExamples { */ static class ExtractTempFn extends DoFn> { @ProcessElement - public void processElement( - @Element TableRow row, OutputReceiver> receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); Integer month = Integer.parseInt((String) row.get("month")); Double meanTemp = Double.parseDouble(row.get("mean_temp").toString()); - receiver.output(KV.of(month, meanTemp)); + c.output(KV.of(month, meanTemp)); } } /** Format the results to a TableRow, to save to BigQuery. */ static class FormatMaxesFn extends DoFn, TableRow> { @ProcessElement - public void processElement( - @Element KV element, OutputReceiver receiver) { + public void processElement(ProcessContext c) { TableRow row = - new TableRow().set("month", element.getKey()).set("max_mean_temp", element.getValue()); - receiver.output(row); + new TableRow() + .set("month", c.element().getKey()) + .set("max_mean_temp", c.element().getValue()); + c.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java index 0c39c600b6cc..713af1d50953 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java @@ -26,8 +26,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -75,9 +73,10 @@ public class MinimalBigQueryTornadoes { */ static class ExtractTornadoesFn extends DoFn { @ProcessElement - public void processElement(@Element TableRow row, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + TableRow row = c.element(); if ((Boolean) row.get("tornado")) { - receiver.output(Integer.parseInt((String) row.get("month"))); + c.output(Integer.parseInt((String) row.get("month"))); } } } @@ -88,9 +87,8 @@ public void processElement(@Element TableRow row, OutputReceiver receiv */ static class FormatCountsFn extends DoFn, String> { @ProcessElement - public void processElement( - @Element KV element, OutputReceiver receiver) { - receiver.output(element.getKey() + ": " + element.getValue()); + public void processElement(ProcessContext c) { + c.output(c.element().getKey() + ": " + c.element().getValue()); } } @@ -136,9 +134,9 @@ static class LogOutput extends DoFn { } @ProcessElement - public void processElement(@Element T element, OutputReceiver receiver) { - LOG.info("{}{}", prefix, element); - receiver.output(element); + public void processElement(ProcessContext c) { + LOG.info("{}{}", prefix, c.element()); + c.output(c.element()); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java index 5270077ea037..cd3c6dd84157 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java @@ -38,9 +38,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.GroupByKey; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -49,7 +46,6 @@ import org.apache.beam.sdk.transforms.windowing.AfterWatermark; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.Repeatedly; import org.apache.beam.sdk.transforms.windowing.Window; import org.apache.beam.sdk.values.KV; @@ -368,18 +364,15 @@ public PCollection expand(PCollection> flowInfo) { new DoFn>, KV>() { @ProcessElement - public void processElement( - @Element KV> element, - OutputReceiver> receiver) - throws Exception { - Iterable flows = element.getValue(); + public void processElement(ProcessContext c) throws Exception { + Iterable flows = c.element().getValue(); Integer sum = 0; Long numberOfRecords = 0L; for (Integer value : flows) { sum += value; numberOfRecords++; } - receiver.output(KV.of(element.getKey(), sum + "," + numberOfRecords)); + c.output(KV.of(c.element().getKey(), sum + "," + numberOfRecords)); } })); PCollection output = results.apply(ParDo.of(new FormatTotalFlow(triggerType))); @@ -399,27 +392,21 @@ public FormatTotalFlow(String triggerType) { } @ProcessElement - public void processElement( - PaneInfo pane, - @Timestamp Instant timestamp, - BoundedWindow window, - @Element KV element, - OutputReceiver receiver) - throws Exception { - String[] values = element.getValue().split(",", -1); + public void processElement(ProcessContext c, BoundedWindow window) throws Exception { + String[] values = c.element().getValue().split(",", -1); TableRow row = new TableRow() .set("trigger_type", triggerType) - .set("freeway", element.getKey()) + .set("freeway", c.element().getKey()) .set("total_flow", Integer.parseInt(values[0])) .set("number_of_records", Long.parseLong(values[1])) .set("window", window.toString()) - .set("isFirst", pane.isFirst()) - .set("isLast", pane.isLast()) - .set("timing", pane.getTiming().toString()) - .set("event_time", timestamp.toString()) + .set("isFirst", c.pane().isFirst()) + .set("isLast", c.pane().isLast()) + .set("timing", c.pane().getTiming().toString()) + .set("event_time", c.timestamp().toString()) .set("processing_time", Instant.now().toString()); - receiver.output(row); + c.output(row); } } @@ -431,9 +418,8 @@ static class ExtractFlowInfo extends DoFn> { private static final int VALID_NUM_FIELDS = 50; @ProcessElement - public void processElement( - @Element String element, OutputReceiver> receiver) throws Exception { - String[] laneInfo = element.split(",", -1); + public void processElement(ProcessContext c) throws Exception { + String[] laneInfo = c.element().split(",", -1); if ("timestamp".equals(laneInfo[0])) { // Header row return; @@ -449,7 +435,7 @@ public void processElement( if (totalFlow == null || totalFlow <= 0) { return; } - receiver.output(KV.of(freeway, totalFlow)); + c.output(KV.of(freeway, totalFlow)); } } @@ -523,8 +509,7 @@ public void setup() { } @ProcessElement - public void processElement(@Element String element, OutputReceiver receiver) - throws Exception { + public void processElement(ProcessContext c) throws Exception { Instant timestamp = Instant.now(); if (random.nextDouble() < THRESHOLD) { int range = MAX_DELAY - MIN_DELAY; @@ -532,7 +517,7 @@ public void processElement(@Element String element, OutputReceiver recei long delayInMillis = TimeUnit.MINUTES.toMillis(delayInMinutes); timestamp = new Instant(timestamp.getMillis() - delayInMillis); } - receiver.outputWithTimestamp(element, timestamp); + c.outputWithTimestamp(c.element(), timestamp); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java b/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java index 99aada20669b..4f24c69f74b7 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java +++ b/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java @@ -76,10 +76,6 @@ import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.DoFn.BoundedPerElement; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; -import org.apache.beam.sdk.transforms.DoFn.SideInput; -import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.Latest; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; @@ -544,14 +540,14 @@ public static PCollection coGroupByKeyTuple( ParDo.of( new DoFn, String>() { @ProcessElement - public void processElement( - @Element KV e, OutputReceiver receiver) { + public void processElement(ProcessContext c) { + KV e = c.element(); String name = e.getKey(); Iterable emailsIter = e.getValue().getAll(emailsTag); Iterable phonesIter = e.getValue().getAll(phonesTag); String formattedResult = Snippets.formatCoGbkResults(name, emailsIter, phonesIter); - receiver.output(formattedResult); + c.output(formattedResult); } })); // [END CoGroupByKeyTuple] @@ -646,23 +642,20 @@ public void process( new DoFn>() { @ProcessElement - public void process( - @Timestamp Instant timestamp, - @Element Long element, - @SideInput("mapIterable") Iterable> si, - OutputReceiver> receiver) { + public void process(ProcessContext c, @Timestamp Instant timestamp) { + Iterable> si = c.sideInput(mapIterable); // Take an element from the side input iterable (likely length 1) Map keyMap = si.iterator().next(); - receiver.outputWithTimestamp(KV.of(1L, element), Instant.now()); + c.outputWithTimestamp(KV.of(1L, c.element()), Instant.now()); LOG.info( "Value is {} with timestamp {}, using key A from side input with time {}.", - element, + c.element(), timestamp.toString(DateTimeFormat.forPattern("HH:mm:ss")), keyMap.get("Key_A")); } }) - .withSideInput("mapIterable", mapIterable)); + .withSideInputs(mapIterable)); p.run(); } @@ -708,9 +701,9 @@ public static void accessingValueProviderInfoAfterRunSnip1(String[] args) { // Define the DoFn that logs the ValueProvider value. @ProcessElement - public void process(PipelineOptions options) { + public void process(ProcessContext c) { - MyOptions ops = options.as(MyOptions.class); + MyOptions ops = c.getPipelineOptions().as(MyOptions.class); // This example logs the ValueProvider value, but you could store it by // pushing it to an external database. @@ -950,13 +943,11 @@ public void process(@Element String src, OutputReceiver o) { ParDo.of( new DoFn() { @ProcessElement - public void process( - @SideInput("sideInput") List sideInputValue, - OutputReceiver receiver) { - receiver.output((long) sideInputValue.size()); + public void process(ProcessContext c) { + c.output((long) c.sideInput(sideInput).size()); } }) - .withSideInput("sideInput", sideInput)); + .withSideInputs(sideInput)); // [END PeriodicallyUpdatingSideInputs] return result; } @@ -1197,10 +1188,7 @@ private static class BundleFinalization { private static class BundleFinalizationDoFn extends DoFn { // [START BundleFinalize] @ProcessElement - public void processElement( - @Element String element, - OutputReceiver receiver, - BundleFinalizer bundleFinalizer) { + public void processElement(ProcessContext c, BundleFinalizer bundleFinalizer) { // ... produce output ... bundleFinalizer.afterBundleCommit( diff --git a/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java b/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java index b7fb7b82db31..4aa20fc10dfb 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java +++ b/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java @@ -28,8 +28,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.slf4j.Logger; @@ -86,13 +84,11 @@ public void setUp() throws Exception { } @ProcessElement - public void processElement( - @Element KV element, OutputReceiver> receiver) - throws Exception { + public void processElement(ProcessContext c) throws Exception { try { // Our Library takes a single command in position 0 which it will echo back in the result SubProcessCommandLineArgs commands = new SubProcessCommandLineArgs(); - Command command = new Command(0, String.valueOf(element.getValue())); + Command command = new Command(0, String.valueOf(c.element().getValue())); commands.putCommand(command); // The ProcessingKernel deals with the execution of the process @@ -101,7 +97,7 @@ public void processElement( // Run the command and work through the results List results = kernel.exec(commands); for (String s : results) { - receiver.output(KV.of(element.getKey(), s)); + c.output(KV.of(c.element().getKey(), s)); } } catch (Exception ex) { LOG.error("Error processing element ", ex); diff --git a/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java b/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java index 0d9e257fc6c0..19c83c6eb73c 100644 --- a/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java +++ b/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java @@ -29,8 +29,6 @@ import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.FixedWindows; import org.apache.beam.sdk.transforms.windowing.Window; @@ -148,8 +146,8 @@ static String canonicalFormat(TableRow row) { static class FormatResults extends DoFn { @ProcessElement - public void processElement(@Element TableRow element, OutputReceiver receiver) - throws Exception { + public void processElement(ProcessContext c) throws Exception { + TableRow element = c.element(); TableRow row = new TableRow() .set("trigger_type", element.get("trigger_type")) @@ -160,7 +158,7 @@ public void processElement(@Element TableRow element, OutputReceiver rec .set("isLast", element.get("isLast")) .set("timing", element.get("timing")) .set("window", element.get("window")); - receiver.output(canonicalFormat(row)); + c.output(canonicalFormat(row)); } } } diff --git a/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java b/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java index 9981b4040e3b..01055d9658a9 100644 --- a/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java +++ b/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java @@ -41,8 +41,6 @@ import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.DoFn.Element; -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -146,13 +144,11 @@ public void setUp() throws Exception { } @ProcessElement - public void processElement( - @Element KV element, OutputReceiver> receiver) - throws Exception { + public void processElement(ProcessContext c) throws Exception { try { // Our Library takes a single command in position 0 which it will echo back in the result SubProcessCommandLineArgs commands = new SubProcessCommandLineArgs(); - Command command = new Command(0, String.valueOf(element.getValue())); + Command command = new Command(0, String.valueOf(c.element().getValue())); commands.putCommand(command); // The ProcessingKernel deals with the execution of the process @@ -161,7 +157,7 @@ public void processElement( // Run the command and work through the results List results = kernel.exec(commands); for (String s : results) { - receiver.output(KV.of(element.getKey(), s)); + c.output(KV.of(c.element().getKey(), s)); } } catch (Exception ex) { LOG.error("Error processing element ", ex); diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt index 7be4df2c8a91..05cb404ac6d2 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt @@ -25,8 +25,6 @@ import org.apache.beam.sdk.options.Description import org.apache.beam.sdk.options.PipelineOptionsFactory import org.apache.beam.sdk.testing.PAssert import org.apache.beam.sdk.transforms.DoFn -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.ParDo import org.apache.beam.sdk.values.KV import org.slf4j.LoggerFactory @@ -86,18 +84,18 @@ public object DebuggingWordCount { private val unmatchedWords = Metrics.counter(FilterTextFn::class.java, "unmatchedWords") @ProcessElement - fun processElement(@Element element: KV, receiver: OutputReceiver>) { - if (filter.matcher(element.key).matches()) { + fun processElement(c: ProcessContext) { + if (filter.matcher(c.element().key).matches()) { // Log at the "DEBUG" level each element that we match. When executing this pipeline // these log lines will appear only if the log level is set to "DEBUG" or lower. - LOG.debug("Matched: ${element.key}") + LOG.debug("Matched: ${c.element().key}") matchedWords.inc() - receiver.output(element) + c.output(c.element()) } else { // Log at the "TRACE" level each element that is not matched. Different log levels // can be used to control the verbosity of logging providing an effective mechanism // to filter less important information. - LOG.trace("Did not match: ${element.key}") + LOG.trace("Did not match: ${c.element().key}") unmatchedWords.inc() } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt index 8edc98872350..ec56bc659970 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt @@ -27,8 +27,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.Count import org.apache.beam.sdk.transforms.DoFn -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.PTransform import org.apache.beam.sdk.transforms.ParDo import org.apache.beam.sdk.values.KV @@ -75,9 +73,10 @@ object BigQueryTornadoes { */ internal class ExtractTornadoesFn : DoFn() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { + val row = c.element() if (row["tornado"] as Boolean) { - receiver.output(Integer.parseInt(row["month"] as String)) + c.output(Integer.parseInt(row["month"] as String)) } } } @@ -88,11 +87,11 @@ object BigQueryTornadoes { */ internal class FormatCountsFn : DoFn, TableRow>() { @ProcessElement - fun processElement(@Element element: KV, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { val row = TableRow() - .set("month", element.key) - .set("tornado_count", element.value) - receiver.output(row) + .set("month", c.element().key) + .set("tornado_count", c.element().value) + c.output(row) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt index 4d89b692dd82..9e388031002d 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt @@ -26,8 +26,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.metrics.Metrics import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.* -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.values.KV import org.apache.beam.sdk.values.PCollection @@ -72,11 +70,12 @@ object CombinePerKeyExamples { private val smallerWords = Metrics.counter(ExtractLargeWordsFn::class.java, "smallerWords") @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { + fun processElement(c: ProcessContext) { + val row = c.element() val playName = row["corpus"] as String val word = row["word"] as String if (word.length >= MIN_WORD_LENGTH) { - receiver.output(KV.of(word, playName)) + c.output(KV.of(word, playName)) } else { // Track how many smaller words we're not including. This information will be // visible in the Monitoring UI. @@ -91,9 +90,9 @@ object CombinePerKeyExamples { */ internal class FormatShakespeareOutputFn : DoFn, TableRow>() { @ProcessElement - fun processElement(@Element element: KV, receiver: OutputReceiver) { - val row = TableRow().set("word", element.key).set("all_plays", element.value) - receiver.output(row) + fun processElement(c: ProcessContext) { + val row = TableRow().set("word", c.element().key).set("all_plays", c.element().value) + c.output(row) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt index 218188448965..2625f5bfec10 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt @@ -25,9 +25,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.* -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver -import org.apache.beam.sdk.transforms.DoFn.SideInput import org.apache.beam.sdk.values.PCollection import java.util.logging.Logger @@ -83,7 +80,8 @@ object FilterExamples { */ internal class ProjectionFn : DoFn() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { + val row = c.element() // Grab year, month, day, mean_temp from the row val year = Integer.parseInt(row["year"] as String) val month = Integer.parseInt(row["month"] as String) @@ -96,7 +94,7 @@ object FilterExamples { .set("month", month) .set("day", day) .set("mean_temp", meanTemp) - receiver.output(outRow) + c.output(outRow) } } @@ -110,10 +108,11 @@ object FilterExamples { internal class FilterSingleMonthDataFn(private var monthFilter: Int?) : DoFn() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { + val row = c.element() val month = row["month"] if (month == this.monthFilter) { - receiver.output(row) + c.output(row) } } } @@ -124,9 +123,10 @@ object FilterExamples { */ internal class ExtractTempFn : DoFn() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { + val row = c.element() val meanTemp = java.lang.Double.parseDouble(row["mean_temp"].toString()) - receiver.output(meanTemp) + c.output(meanTemp) } } @@ -158,17 +158,15 @@ object FilterExamples { ParDo.of( object : DoFn() { @ProcessElement - fun processElement( - @SideInput("globalMeanTemp") gTemp: Double, - @Element element: TableRow, - receiver: OutputReceiver) { - val meanTemp = java.lang.Double.parseDouble(element["mean_temp"].toString()) + fun processElement(c: ProcessContext) { + val meanTemp = java.lang.Double.parseDouble(c.element()["mean_temp"].toString()) + val gTemp = c.sideInput(globalMeanTemp) if (meanTemp < gTemp) { - receiver.output(element) + c.output(c.element()) } } }) - .withSideInput("globalMeanTemp", globalMeanTemp)) + .withSideInputs(globalMeanTemp)) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt index 7f81629ae463..2f2215e1d96a 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt @@ -26,8 +26,6 @@ import org.apache.beam.sdk.options.PipelineOptions import org.apache.beam.sdk.options.PipelineOptionsFactory import org.apache.beam.sdk.options.Validation import org.apache.beam.sdk.transforms.DoFn -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.ParDo import org.apache.beam.sdk.transforms.join.CoGbkResult import org.apache.beam.sdk.transforms.join.CoGroupByKey @@ -91,14 +89,13 @@ object JoinExamples { ParDo.of( object : DoFn, KV>() { @ProcessElement - fun processElement( - @Element element: KV, - receiver: OutputReceiver>) { - val countryCode = element.key - val countryName = element.value.getOnly(countryInfoTag) - for (ei in element.value.getAll(eventInfoTag)) { + fun processElement(c: ProcessContext) { + val e = c.element() + val countryCode = e.key + val countryName = e.value.getOnly(countryInfoTag) + for (ei in c.element().value.getAll(eventInfoTag)) { // Generate a string that combines information from both collection values - receiver.output( + c.output( KV.of( countryCode, "Country name: $countryName, Event info: $ei")) @@ -112,9 +109,9 @@ object JoinExamples { ParDo.of( object : DoFn, String>() { @ProcessElement - fun processElement(@Element element: KV, receiver: OutputReceiver) { - val outputString = "Country code: ${element.key}, ${element.value}" - receiver.output(outputString) + fun processElement(c: ProcessContext) { + val outputString = "Country code: ${c.element().key}, ${c.element().value}" + c.output(outputString) } })) } @@ -125,13 +122,14 @@ object JoinExamples { */ internal class ExtractEventDataFn : DoFn>() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { + fun processElement(c: ProcessContext) { + val row = c.element() val countryCode = row["ActionGeo_CountryCode"] as String val sqlDate = row["SQLDATE"] as String val actor1Name = row["Actor1Name"] as String val sourceUrl = row["SOURCEURL"] as String val eventInfo = "Date: $sqlDate, Actor1: $actor1Name, url: $sourceUrl" - receiver.output(KV.of(countryCode, eventInfo)) + c.output(KV.of(countryCode, eventInfo)) } } @@ -141,10 +139,11 @@ object JoinExamples { */ internal class ExtractCountryInfoFn : DoFn>() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { + fun processElement(c: ProcessContext) { + val row = c.element() val countryCode = row["FIPSCC"] as String val countryName = row["HumanName"] as String - receiver.output(KV.of(countryCode, countryName)) + c.output(KV.of(countryCode, countryName)) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt index ad16ecde9657..11418d3933cf 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt @@ -26,8 +26,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.DoFn -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.Max import org.apache.beam.sdk.transforms.PTransform import org.apache.beam.sdk.transforms.ParDo @@ -75,21 +73,22 @@ object MaxPerKeyExamples { */ internal class ExtractTempFn : DoFn>() { @ProcessElement - fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { + fun processElement(c: ProcessContext) { + val row = c.element() val month = Integer.parseInt(row["month"] as String) val meanTemp = java.lang.Double.parseDouble(row["mean_temp"].toString()) - receiver.output(KV.of(month, meanTemp)) + c.output(KV.of(month, meanTemp)) } } /** Format the results to a TableRow, to save to BigQuery. */ internal class FormatMaxesFn : DoFn, TableRow>() { @ProcessElement - fun processElement(@Element element: KV, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { val row = TableRow() - .set("month", element.key) - .set("max_mean_temp", element.value) - receiver.output(row) + .set("month", c.element().key) + .set("max_mean_temp", c.element().value) + c.output(row) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt index bb8c0900e319..4afa7d0dfc70 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt @@ -34,9 +34,6 @@ import org.apache.beam.sdk.options.Description import org.apache.beam.sdk.options.PipelineOptionsFactory import org.apache.beam.sdk.options.StreamingOptions import org.apache.beam.sdk.transforms.DoFn -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver -import org.apache.beam.sdk.transforms.DoFn.Timestamp import org.apache.beam.sdk.transforms.GroupByKey import org.apache.beam.sdk.transforms.PTransform import org.apache.beam.sdk.transforms.ParDo @@ -367,17 +364,15 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement( - @Element element: KV>, - receiver: OutputReceiver>) { - val flows = element.value + fun processElement(c: ProcessContext) { + val flows = c.element().value var sum = 0 var numberOfRecords = 0L for (value in flows) { sum += value numberOfRecords++ } - receiver.output(KV.of(element.key, "$sum,$numberOfRecords")) + c.output(KV.of(c.element().key, "$sum,$numberOfRecords")) } })) return results.apply(ParDo.of(FormatTotalFlow(triggerType))) @@ -392,25 +387,20 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement( - @Element element: KV, - window: BoundedWindow, - pane: PaneInfo, - @Timestamp timestamp: Instant, - receiver: OutputReceiver) { - val values = element.value.split(",".toRegex()).toTypedArray() + fun processElement(c: ProcessContext, window: BoundedWindow) { + val values = c.element().value.split(",".toRegex()).toTypedArray() val row = TableRow() .set("trigger_type", triggerType) - .set("freeway", element.key) + .set("freeway", c.element().key) .set("total_flow", Integer.parseInt(values[0])) .set("number_of_records", java.lang.Long.parseLong(values[1])) .set("window", window.toString()) - .set("isFirst", pane.isFirst) - .set("isLast", pane.isLast) - .set("timing", pane.timing.toString()) - .set("event_time", timestamp.toString()) + .set("isFirst", c.pane().isFirst) + .set("isLast", c.pane().isLast) + .set("timing", c.pane().timing.toString()) + .set("event_time", c.timestamp().toString()) .set("processing_time", Instant.now().toString()) - receiver.output(row) + c.output(row) } } @@ -422,8 +412,8 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement(@Element element: String, receiver: OutputReceiver>) { - val laneInfo = element.split(",".toRegex()).toTypedArray() + fun processElement(c: ProcessContext) { + val laneInfo = c.element().split(",".toRegex()).toTypedArray() if ("timestamp" == laneInfo[0]) { // Header row return @@ -439,7 +429,7 @@ object TriggerExample { if (totalFlow == null || totalFlow <= 0) { return } - receiver.output(KV.of(freeway, totalFlow)) + c.output(KV.of(freeway, totalFlow)) } companion object { @@ -496,7 +486,7 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement(@Element element: String, receiver: OutputReceiver) { + fun processElement(c: ProcessContext) { var timestamp = Instant.now() if (random.nextDouble() < THRESHOLD) { val range = MAX_DELAY - MIN_DELAY @@ -504,7 +494,7 @@ object TriggerExample { val delayInMillis = TimeUnit.MINUTES.toMillis(delayInMinutes.toLong()) timestamp = Instant(timestamp.millis - delayInMillis) } - receiver.outputWithTimestamp(element, timestamp) + c.outputWithTimestamp(c.element(), timestamp) } companion object { diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt index eb2099372482..d2f58c215a56 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt @@ -33,8 +33,6 @@ import org.apache.beam.sdk.io.gcp.bigquery.DynamicDestinations import org.apache.beam.sdk.io.gcp.bigquery.TableDestination import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.transforms.* -import org.apache.beam.sdk.transforms.DoFn.Element -import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.join.CoGbkResult import org.apache.beam.sdk.transforms.join.CoGroupByKey import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple @@ -362,12 +360,13 @@ object Snippets { ParDo.of( object : DoFn, String>() { @ProcessElement - fun processElement(@Element element: KV, receiver: OutputReceiver) { - val name = element.key - val emailsIter = element.value.getAll(emailsTag) - val phonesIter = element.value.getAll(phonesTag) + fun processElement(c: ProcessContext) { + val e = c.element() + val name = e.key + val emailsIter = e.value.getAll(emailsTag) + val phonesIter = e.value.getAll(phonesTag) val formattedResult = formatCoGbkResults(name, emailsIter, phonesIter) - receiver.output(formattedResult) + c.output(formattedResult) } })) } From d8da6c3a461213fd33f435d20c7ef3a75a14cbc9 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 1 May 2026 17:22:02 +0400 Subject: [PATCH 046/490] Update republish_released_docker_containers.yml to 2.73.0 (#38354) --- .github/workflows/republish_released_docker_containers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index 7feae365ce51..afba70e3d164 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -32,7 +32,7 @@ on: - cron: "0 6 * * 1" env: docker_registry: gcr.io - release: "${{ github.event.inputs.RELEASE || '2.72.0' }}" + release: "${{ github.event.inputs.RELEASE || '2.73.0' }}" rc: "${{ github.event.inputs.RC || '5' }}" jobs: From a2820152408b06405a97ec0642eb5f06f395c1b9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 1 May 2026 15:38:52 +0200 Subject: [PATCH 047/490] Mark dynamically generated ml_preprocessing test as no_xdist for precommit (#38353) --- sdks/python/apache_beam/yaml/examples/testing/examples_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/yaml/examples/testing/examples_test.py b/sdks/python/apache_beam/yaml/examples/testing/examples_test.py index ef900090c393..82127d1e27aa 100644 --- a/sdks/python/apache_beam/yaml/examples/testing/examples_test.py +++ b/sdks/python/apache_beam/yaml/examples/testing/examples_test.py @@ -394,7 +394,6 @@ def _java_deps_involved(spec_filename): for substr in ['java_deps', 'streaming_taxifare_prediction']) if _python_deps_involved(pipeline_spec_file): - test_yaml_example = pytest.mark.no_xdist(test_yaml_example) test_yaml_example = unittest.skipIf( sys.platform == 'win32', "Github virtualenv permissions issues.")( test_yaml_example) @@ -406,6 +405,7 @@ def _java_deps_involved(spec_filename): '-cloud' in os.environ.get('TOX_ENV_NAME', ''), 'Github actions environment issue.')( test_yaml_example) + test_yaml_example = pytest.mark.no_xdist(test_yaml_example) if _java_deps_involved(pipeline_spec_file): test_yaml_example = pytest.mark.xlang_sql_expansion_service( From 2b532f95fcf91eb44f8d4280e01031988f32e40e Mon Sep 17 00:00:00 2001 From: harshadkhetpal Date: Fri, 1 May 2026 21:02:40 +0530 Subject: [PATCH 048/490] fix: correct typo "occured" to "occurred" (#38087) --- sdks/python/apache_beam/runners/worker/data_sampler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/python/apache_beam/runners/worker/data_sampler.py b/sdks/python/apache_beam/runners/worker/data_sampler.py index c95c88f2dbdf..4e4f1a64a508 100644 --- a/sdks/python/apache_beam/runners/worker/data_sampler.py +++ b/sdks/python/apache_beam/runners/worker/data_sampler.py @@ -81,10 +81,10 @@ class ExceptionMetadata: # The repr-ified Exception. msg: str - # The transform where the exception occured. + # The transform where the exception occurred. transform_id: str - # The instruction when the exception occured. + # The instruction when the exception occurred. instruction_id: str From 81f618f4d8662a47d81a953242b55c0aefbb07c3 Mon Sep 17 00:00:00 2001 From: M Junaid Shaukat <154750865+junaiddshaukat@users.noreply.github.com> Date: Fri, 1 May 2026 20:38:38 +0500 Subject: [PATCH 049/490] Fix unhandled exception in KafkaIO SDF (#37449) (#37553) Replace checkState() with LOG.warn() in GenerateKafkaSourceDescriptor and WatchForKafkaTopicPartitions to prevent IllegalStateException from causing infinite retries when Kafka returns null or empty partition info for a topic. Topics with missing partitions are now gracefully skipped with a warning log instead of throwing. --- .../org/apache/beam/sdk/io/kafka/KafkaIO.java | 12 ++---- .../kafka/WatchForKafkaTopicPartitions.java | 15 ++++--- .../WatchForKafkaTopicPartitionsTest.java | 43 +++++++++++++++++++ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java index 518319a38e32..f9bfdd87aaac 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java @@ -2130,16 +2130,12 @@ public void processElement(OutputReceiver receiver) { } else { for (String topic : topics) { List partitionInfoList = consumer.partitionsFor(topic); - if (logTopicVerification == null || !logTopicVerification) { - checkState( - partitionInfoList != null && !partitionInfoList.isEmpty(), - "Could not find any partitions info for topic %s. Please check Kafka configuration and make sure that provided topics exist.", - topic); - } else { + if (partitionInfoList == null || partitionInfoList.isEmpty()) { LOG.warn( - "Could not find any partitions info for topic {}. Please check Kafka configuration " - + "and make sure that the provided topics exist.", + "Could not find any partitions info for topic {}. Please check Kafka " + + "configuration and make sure that the provided topics exist.", topic); + continue; } for (PartitionInfo p : partitionInfoList) { diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java index 490faafb22fa..3184b18267b2 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java @@ -18,7 +18,6 @@ package org.apache.beam.sdk.io.kafka; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects.firstNonNull; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.util.ArrayList; import java.util.List; @@ -45,6 +44,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A {@link PTransform} for continuously querying Kafka for new partitions, and emitting those @@ -57,6 +58,7 @@ */ class WatchForKafkaTopicPartitions extends PTransform> { + private static final Logger LOG = LoggerFactory.getLogger(WatchForKafkaTopicPartitions.class); private static final Duration DEFAULT_CHECK_DURATION = Duration.standardHours(1); private static final String COUNTER_NAMESPACE = "watch_kafka_topic_partition"; @@ -191,10 +193,13 @@ static List getAllTopicPartitions( if (topics != null && !topics.isEmpty()) { for (String topic : topics) { List partitionInfoList = kafkaConsumer.partitionsFor(topic); - checkState( - partitionInfoList != null && !partitionInfoList.isEmpty(), - "Could not find any partitions info for topic %s. Please check Kafka configuration and make sure that provided topics exist.", - topic); + if (partitionInfoList == null || partitionInfoList.isEmpty()) { + LOG.warn( + "Could not find any partitions info for topic {}. Please check Kafka " + + "configuration and make sure that the provided topics exist.", + topic); + continue; + } for (PartitionInfo partition : partitionInfoList) { current.add(new TopicPartition(topic, partition.partition())); } diff --git a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java index 595d040bf403..30ace6cd86d0 100644 --- a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java +++ b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java @@ -18,10 +18,12 @@ package org.apache.beam.sdk.io.kafka; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Collections; import java.util.Set; import java.util.regex.Pattern; import org.apache.beam.sdk.io.kafka.KafkaMocks.PartitionGrowthMockConsumer; @@ -108,6 +110,47 @@ public void testGetAllTopicPartitionsWithGivenTopics() throws Exception { (input) -> mockConsumer, null, givenTopics, null)); } + @Test + public void testGetAllTopicPartitionsWithNullPartitionInfo() throws Exception { + Set givenTopics = ImmutableSet.of("topic1"); + + Consumer mockConsumer = Mockito.mock(Consumer.class); + when(mockConsumer.partitionsFor("topic1")).thenReturn(null); + assertTrue( + WatchForKafkaTopicPartitions.getAllTopicPartitions( + (input) -> mockConsumer, null, givenTopics, null) + .isEmpty()); + } + + @Test + public void testGetAllTopicPartitionsWithEmptyPartitionInfo() throws Exception { + Set givenTopics = ImmutableSet.of("topic1"); + + Consumer mockConsumer = Mockito.mock(Consumer.class); + when(mockConsumer.partitionsFor("topic1")).thenReturn(Collections.emptyList()); + assertTrue( + WatchForKafkaTopicPartitions.getAllTopicPartitions( + (input) -> mockConsumer, null, givenTopics, null) + .isEmpty()); + } + + @Test + public void testGetAllTopicPartitionsSkipsMissingTopics() throws Exception { + Set givenTopics = ImmutableSet.of("topic1", "topic2"); + + Consumer mockConsumer = Mockito.mock(Consumer.class); + when(mockConsumer.partitionsFor("topic1")).thenReturn(null); + when(mockConsumer.partitionsFor("topic2")) + .thenReturn( + ImmutableList.of( + new PartitionInfo("topic2", 0, null, null, null), + new PartitionInfo("topic2", 1, null, null, null))); + assertEquals( + ImmutableList.of(new TopicPartition("topic2", 0), new TopicPartition("topic2", 1)), + WatchForKafkaTopicPartitions.getAllTopicPartitions( + (input) -> mockConsumer, null, givenTopics, null)); + } + @Test public void testGetAllTopicPartitionsWithGivenPattern() throws Exception { Consumer mockConsumer = Mockito.mock(Consumer.class); From c4693e55d67123fdee4df993f82aa42275a4dd04 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 1 May 2026 14:08:20 -0400 Subject: [PATCH 050/490] Move Python PreCommit middle versions to PostCommit (#38347) --- .github/workflows/README.md | 1 + .../beam_PostCommit_Python_Versions.yml | 149 ++++++++++++++++++ .github/workflows/beam_PreCommit_Python.yml | 12 +- .../beam_PreCommit_Python_Dataframes.yml | 2 +- .../beam_PreCommit_Python_Examples.yml | 2 +- .../beam_PreCommit_Python_Runners.yml | 2 +- .../beam_PreCommit_Python_Transforms.yml | 2 +- 7 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/beam_PostCommit_Python_Versions.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index f8c77b7180db..68b6a6bb7eb5 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -396,6 +396,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Python Xlang Gcp Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml) | N/A |`beam_PostCommit_Python_Xlang_Gcp_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang Gcp Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_Gcp_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang IO Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml) | N/A |`beam_PostCommit_Python_Xlang_IO_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml?query=event%3Aschedule) | +| [ PostCommit Python Versions ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Versions.yml) | N/A |`beam_PostCommit_Python_Versions.json`| [![.github/workflows/beam_PostCommit_Python_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Versions.yml?query=event%3Aschedule) | | [ PostCommit SQL ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_SQL.yml) | N/A |`beam_PostCommit_SQL.json`| [![.github/workflows/beam_PostCommit_SQL.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_SQL.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_SQL.yml?query=event%3Aschedule) | | [ PostCommit TransformService Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_TransformService_Direct.yml) | N/A |`beam_PostCommit_TransformService_Direct.json`| [![.github/workflows/beam_PostCommit_TransformService_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_TransformService_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_TransformService_Direct.yml?query=event%3Aschedule) | [ PostCommit Website Test](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Website_Test.yml) | N/A |`beam_PostCommit_Website_Test.json`| [![.github/workflows/beam_PostCommit_Website_Test.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Website_Test.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Website_Test.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_PostCommit_Python_Versions.yml b/.github/workflows/beam_PostCommit_Python_Versions.yml new file mode 100644 index 000000000000..b1fbc18cb6de --- /dev/null +++ b/.github/workflows/beam_PostCommit_Python_Versions.yml @@ -0,0 +1,149 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: PostCommit Python Versions +on: + pull_request_target: + branches: [ "master", "release-*" ] + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Python_Versions.json'] + push: + tags: ['v*'] + branches: ['master', 'release-*'] + paths: [ "model/**","sdks/python/**",".github/workflows/beam_PostCommit_Python_Versions.yml"] + schedule: + - cron: '0 3/6 * * *' + workflow_dispatch: + +#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +permissions: + actions: write + pull-requests: write + checks: write + contents: read + deployments: read + id-token: none + issues: write + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.event.pull_request.head.label || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + # Aggressive stability settings for flaky CI environment + PYTHONHASHSEED: "0" + OMP_NUM_THREADS: "1" + OPENBLAS_NUM_THREADS: "1" + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + GRPC_ENABLE_FORK_SUPPORT: "0" + # gRPC stability - more conservative for unstable networks + GRPC_ARG_KEEPALIVE_TIME_MS: "10000" + GRPC_ARG_KEEPALIVE_TIMEOUT_MS: "15000" + GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS: "1" + GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA: "0" + GRPC_ARG_MAX_RECONNECT_BACKOFF_MS: "30000" + # Beam-specific - very generous timeouts + BEAM_RETRY_MAX_ATTEMPTS: "5" + BEAM_RETRY_INITIAL_DELAY_MS: "5000" + BEAM_RETRY_MAX_DELAY_MS: "120000" + # Force stable execution + BEAM_TESTING_FORCE_SINGLE_BUNDLE: "true" + BEAM_TESTING_DETERMINISTIC_ORDER: "true" + +jobs: + beam_PostCommit_Python_Versions: + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + job_name: ['beam_PostCommit_Python_Versions'] + job_phrase: ['Run Python Version PostCommit'] + python_version: ['3.11','3.12','3.13'] # Run Python PreCommit tests on "middle" versions + if: | + github.event_name == 'push' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + - name: Setup repository + uses: ./.github/actions/setup-action + with: + comment_phrase: ${{ matrix.job_phrase }} ${{ matrix.python_version }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + java-version: default + python-version: ${{ matrix.python_version }} + - name: Set PY_VER_CLEAN + id: set_py_ver_clean + run: | + PY_VER=${{ matrix.python_version }} + PY_VER_CLEAN=${PY_VER//.} + echo "py_ver_clean=$PY_VER_CLEAN" >> $GITHUB_OUTPUT + - name: Run pythonPreCommit + env: + TOX_TESTENV_PASSENV: "DOCKER_*,TESTCONTAINERS_*,TC_*,BEAM_*,GRPC_*,OMP_*,OPENBLAS_*,PYTHONHASHSEED,PYTEST_*" + # Aggressive retry and timeout settings for flaky CI + PYTEST_ADDOPTS: "-v --tb=short --maxfail=5 --durations=30 --reruns=5 --reruns-delay=15 --timeout=600 --disable-warnings" + # Container stability - much more generous timeouts + TC_TIMEOUT: "300" + TC_MAX_TRIES: "15" + TC_SLEEP_TIME: "5" + # Additional gRPC stability for flaky environment + GRPC_ARG_KEEPALIVE_TIME_MS: "60000" + GRPC_ARG_KEEPALIVE_TIMEOUT_MS: "60000" + GRPC_ARG_MAX_CONNECTION_IDLE_MS: "60000" + GRPC_ARG_HTTP2_BDP_PROBE: "1" + GRPC_ARG_SO_REUSEPORT: "1" + # Force sequential execution to reduce load + PYTEST_XDIST_WORKER_COUNT: "1" + # Additional gRPC settings + GRPC_ARG_MAX_RECONNECT_BACKOFF_MS: "120000" + GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS: "2000" + BEAM_RUNNER_BUNDLE_TIMEOUT_MS: "600000" + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :sdks:python:test-suites:tox:py${{steps.set_py_ver_clean.outputs.py_ver_clean}}:preCommitPy${{steps.set_py_ver_clean.outputs.py_ver_clean}} + arguments: | + -PpythonVersion=${{ matrix.python_version }} \ + -Pposargs="--ignore=apache_beam/ml/" \ + - name: Archive Python Test Results + uses: actions/upload-artifact@v4 + if: failure() + with: + name: Python ${{ matrix.python_version }} Test Results + path: '**/pytest*.xml' + - name: Publish Python Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + commit: '${{ env.prsha || env.GITHUB_SHA }}' + comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} + files: '**/pytest*.xml' + large_files: true diff --git a/.github/workflows/beam_PreCommit_Python.yml b/.github/workflows/beam_PreCommit_Python.yml index 279c3c64ebab..6bb6cfb068a3 100644 --- a/.github/workflows/beam_PreCommit_Python.yml +++ b/.github/workflows/beam_PreCommit_Python.yml @@ -83,7 +83,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python'] job_phrase: ['Run Python PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || @@ -150,13 +150,3 @@ jobs: comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/pytest*.xml' large_files: true - - name: Cleanup - if: always() - run: | - # Kill any remaining processes - sudo pkill -f "gradle" || true - sudo pkill -f "java" || true - sudo pkill -f "python.*pytest" || true - # Clean up temp files - sudo rm -rf /tmp/beam-* || true - sudo rm -rf /tmp/gradle-* || true \ No newline at end of file diff --git a/.github/workflows/beam_PreCommit_Python_Dataframes.yml b/.github/workflows/beam_PreCommit_Python_Dataframes.yml index 2842317090cd..0442bd2306bc 100644 --- a/.github/workflows/beam_PreCommit_Python_Dataframes.yml +++ b/.github/workflows/beam_PreCommit_Python_Dataframes.yml @@ -64,7 +64,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Dataframes'] job_phrase: ['Run Python_Dataframes PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PreCommit_Python_Examples.yml b/.github/workflows/beam_PreCommit_Python_Examples.yml index b4cd132184c1..b79b7a49e202 100644 --- a/.github/workflows/beam_PreCommit_Python_Examples.yml +++ b/.github/workflows/beam_PreCommit_Python_Examples.yml @@ -65,7 +65,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Examples'] job_phrase: ['Run Python_Examples PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PreCommit_Python_Runners.yml b/.github/workflows/beam_PreCommit_Python_Runners.yml index 4637102c50e0..ce9f1ef99752 100644 --- a/.github/workflows/beam_PreCommit_Python_Runners.yml +++ b/.github/workflows/beam_PreCommit_Python_Runners.yml @@ -64,7 +64,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Runners'] job_phrase: ['Run Python_Runners PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PreCommit_Python_Transforms.yml b/.github/workflows/beam_PreCommit_Python_Transforms.yml index b4e56ed80bc4..ef26df6b8e30 100644 --- a/.github/workflows/beam_PreCommit_Python_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Python_Transforms.yml @@ -65,7 +65,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Transforms'] job_phrase: ['Run Python_Transforms PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || From bf3b0ef209fcd0a6dcd8e394e97f6bd9a5a87f2c Mon Sep 17 00:00:00 2001 From: bambadiouf1 Date: Fri, 1 May 2026 12:00:25 -0700 Subject: [PATCH 051/490] Add DiskProvisionedIops/ThroughputMibps pipeline options for the Java and GO SDKs and update Go client libraries (#38349) * Restore Java and Go changes for disk provisioned IOPS and throughput * Add CHANGES.md entry for disk provisioned IOPS and throughput * restore go changes * initialize options map in dataflow job to prevent nil pointer exceptions * go fmt * add testDiskProvisionedOptionsConfig unit test * Update pr id in changes.md --- CHANGES.md | 4 +- .../beam/gradle/BeamModulePlugin.groovy | 2 +- .../dataflow/DataflowPipelineTranslator.java | 7 + .../DataflowPipelineWorkerPoolOptions.java | 14 + .../DataflowPipelineTranslatorTest.java | 31 + .../options/DataflowPipelineOptionsTest.java | 9 + sdks/go.mod | 28 +- sdks/go.sum | 901 +----------------- sdks/go/container/boot.go | 14 +- sdks/go/container/boot_test.go | 14 +- sdks/go/pkg/beam/io/fhirio/common.go | 7 +- sdks/go/pkg/beam/runners/dataflow/dataflow.go | 174 ++-- .../beam/runners/dataflow/dataflow_test.go | 22 + .../beam/runners/dataflow/dataflowlib/job.go | 81 +- .../runners/dataflow/dataflowlib/job_test.go | 30 + sdks/go/test/integration/expansions.go | 2 +- 16 files changed, 319 insertions(+), 1021 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7b4671326049..9f35964cf145 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,7 @@ ## New Features / Improvements +* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Go) ([#38349](https://github.com/apache/beam/issues/38349)). * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to encode finished bitset. SentinelBitSetCoder and BitSetCoder are state @@ -104,6 +105,7 @@ ## Highlights + ## I/Os * DebeziumIO (Java): added `OffsetRetainer` interface and `FileSystemOffsetRetainer` implementation to persist and restore CDC offsets across pipeline restarts, and exposed `withStartOffset` / `withOffsetRetainer` on `DebeziumIO.Read` and the cross-language `ReadBuilder` ([#28248](https://github.com/apache/beam/issues/28248)). @@ -2429,4 +2431,4 @@ Schema Options, it will be removed in version `2.23.0`. ([BEAM-9704](https://iss ## Highlights -- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). +- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index a99e0de47934..080c20678dfd 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -742,7 +742,7 @@ class BeamModulePlugin implements Plugin { google_api_common : "com.google.api:api-common", // google_cloud_platform_libraries_bom sets version google_api_services_bigquery : "com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0", // [bomupgrader] sets version google_api_services_cloudresourcemanager : "com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0", // [bomupgrader] sets version - google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260118-$google_clients_version", + google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260405-$google_clients_version", google_api_services_healthcare : "com.google.apis:google-api-services-healthcare:v1-rev20240130-$google_clients_version", google_api_services_pubsub : "com.google.apis:google-api-services-pubsub:v1-rev20220904-$google_clients_version", google_api_services_storage : "com.google.apis:google-api-services-storage:v1-rev20260204-2.0.0", // [bomupgrader] sets version diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index c57b5e3b1a0e..4016f31a5475 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -489,6 +489,13 @@ public Job translate(List packages) { if (options.getDiskSizeGb() > 0) { workerPool.setDiskSizeGb(options.getDiskSizeGb()); } + if (options.getDiskProvisionedIops() != null && options.getDiskProvisionedIops() > 0) { + workerPool.setDiskProvisionedIops(options.getDiskProvisionedIops()); + } + if (options.getDiskProvisionedThroughputMibps() != null + && options.getDiskProvisionedThroughputMibps() > 0) { + workerPool.setDiskProvisionedThroughputMibps(options.getDiskProvisionedThroughputMibps()); + } AutoscalingSettings settings = new AutoscalingSettings(); if (options.getAutoscalingAlgorithm() != null) { settings.setAlgorithm(options.getAutoscalingAlgorithm().getAlgorithm()); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java index fd4af6d5e043..3fcf69f4cacf 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java @@ -193,6 +193,20 @@ public String getAlgorithm() { void setWorkerDiskType(String value); + /** Provisioned IOPS for the worker disk. */ + @Description("Provisioned IOPS for the worker disk.") + @Nullable + Long getDiskProvisionedIops(); + + void setDiskProvisionedIops(Long value); + + /** Provisioned throughput in MiB/s for the worker disk. */ + @Description("Provisioned throughput in MiB/s for the worker disk.") + @Nullable + Long getDiskProvisionedThroughputMibps(); + + void setDiskProvisionedThroughputMibps(Long value); + /** * Specifies whether worker pools should be started with public IP addresses. * diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java index b0ab553d237c..f8818931a68f 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java @@ -757,6 +757,37 @@ public void testDiskSizeGbConfig() throws IOException { assertEquals(diskSizeGb, job.getEnvironment().getWorkerPools().get(0).getDiskSizeGb()); } + @Test + public void testDiskProvisionedOptionsConfig() throws IOException { + final Long diskProvisionedIops = 1000L; + final Long diskProvisionedThroughputMibps = 100L; + + DataflowPipelineOptions options = buildPipelineOptions(); + options.setDiskProvisionedIops(diskProvisionedIops); + options.setDiskProvisionedThroughputMibps(diskProvisionedThroughputMibps); + + Pipeline p = buildPipeline(options); + p.traverseTopologically(new RecordingPipelineVisitor()); + SdkComponents sdkComponents = createSdkComponents(options); + RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(p, sdkComponents, true); + Job job = + DataflowPipelineTranslator.fromOptions(options) + .translate( + p, + pipelineProto, + sdkComponents, + DataflowRunner.fromOptions(options), + Collections.emptyList()) + .getJob(); + + assertEquals(1, job.getEnvironment().getWorkerPools().size()); + assertEquals( + diskProvisionedIops, job.getEnvironment().getWorkerPools().get(0).getDiskProvisionedIops()); + assertEquals( + diskProvisionedThroughputMibps, + job.getEnvironment().getWorkerPools().get(0).getDiskProvisionedThroughputMibps()); + } + /** A composite transform that returns an output that is unrelated to the input. */ private static class UnrelatedOutputCreator extends PTransform, PCollection> { diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java index b381396f2bcc..faa19eefddc5 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java @@ -322,4 +322,13 @@ public void destroy() { TimeoutException.class, () -> DefaultGcpRegionFactory.getRegionFromGcloudCli(1L)); } } + + @Test + public void testDiskProvisionedIopsAndThroughput() { + DataflowPipelineOptions options = PipelineOptionsFactory.as(DataflowPipelineOptions.class); + options.setDiskProvisionedIops(1000L); + options.setDiskProvisionedThroughputMibps(100L); + assertEquals(Long.valueOf(1000), options.getDiskProvisionedIops()); + assertEquals(Long.valueOf(100), options.getDiskProvisionedThroughputMibps()); + } } diff --git a/sdks/go.mod b/sdks/go.mod index 37701285734e..a73c8f3d44f2 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -25,12 +25,12 @@ go 1.26.0 toolchain go1.26.2 require ( - cloud.google.com/go/bigquery v1.72.0 - cloud.google.com/go/bigtable v1.41.0 - cloud.google.com/go/datastore v1.21.0 + cloud.google.com/go/bigquery v1.74.0 + cloud.google.com/go/bigtable v1.42.0 + cloud.google.com/go/datastore v1.22.0 cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.1 - cloud.google.com/go/spanner v1.87.0 + cloud.google.com/go/spanner v1.88.0 cloud.google.com/go/storage v1.59.2 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.7 @@ -56,12 +56,12 @@ require ( github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 golang.org/x/net v0.52.0 - golang.org/x/oauth2 v0.35.0 + golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 - google.golang.org/api v0.257.0 - google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 + google.golang.org/api v0.276.0 + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 @@ -77,13 +77,13 @@ require ( require ( cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/pubsub/v2 v2.0.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect - github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect + github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect @@ -124,8 +124,8 @@ require ( go.einride.tech/aip v0.73.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect @@ -141,7 +141,7 @@ require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.7.0 // indirect + cloud.google.com/go/longrunning v0.8.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect @@ -175,8 +175,8 @@ require ( github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.21.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 59d42c98adaf..074c3122592a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -5,7 +5,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -19,7 +18,6 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -34,421 +32,52 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/bigquery v1.72.0 h1:D/yLju+3Ens2IXx7ou1DJ62juBm+/coBInn4VVOg5Cw= -cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= -cloud.google.com/go/bigtable v1.41.0 h1:99KOWShm/MUyuIbXBeVscdWJFV7GdgiYwFUrB5Iu4BI= -cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/bigquery v1.74.0 h1:Q6bAMv+eyvufOpIrfrYxhM46qq1D3ZQTdgUDQqKS+n8= +cloud.google.com/go/bigquery v1.74.0/go.mod h1:iViO7Cx3A/cRKcHNRsHB3yqGAMInFBswrE9Pxazsc90= +cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= +cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastore v1.21.0 h1:dUrYq47ysCA4nM7u8kRT0WnbfXc6TzX49cP3TCwIiA0= -cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/datastore v1.22.0 h1:FOyx2Ag6ibD2wFkz9S8EiNrmBugia8pQOfpyJxi2yqA= +cloud.google.com/go/datastore v1.22.0/go.mod h1:aopSX+Whx0lHspWWBj+AjWt68/zjYsPfDe3LjWtqZg8= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= -cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= -cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI= cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -456,196 +85,26 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0= cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= -cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= +cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw= cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= @@ -655,7 +114,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= @@ -703,8 +161,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= @@ -713,29 +171,21 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apache/arrow/go/arrow v0.0.0-20200730104253-651201b0f516/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= @@ -831,17 +281,14 @@ github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqx github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc= @@ -854,15 +301,10 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -882,7 +324,6 @@ github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GK github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -903,7 +344,6 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -917,9 +357,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= @@ -927,9 +364,6 @@ github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9O github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -948,7 +382,6 @@ github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -962,7 +395,6 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -972,8 +404,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -987,7 +417,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1004,8 +433,6 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1041,7 +468,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -1055,7 +481,6 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -1072,7 +497,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -1100,7 +524,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -1122,28 +545,15 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -1151,8 +561,6 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= @@ -1160,7 +568,6 @@ github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO0 github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -1227,9 +634,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -1239,16 +644,13 @@ github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -1270,9 +672,6 @@ github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolM github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -1281,10 +680,6 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= @@ -1345,20 +740,15 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -1369,15 +759,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/proullon/ramsql v0.1.4 h1:yTFRTn46gFH/kPbzCx+mGjuFlyTBUeDr3h2ldwxddl0= github.com/proullon/ramsql v0.1.4/go.mod h1:CFGqeQHQpdRfWqYmWD3yXqPTEaHkF4zgXy1C6qDWc9E= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -1388,7 +773,6 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1404,9 +788,6 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -1431,7 +812,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= @@ -1466,7 +846,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -1494,10 +873,10 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -1515,8 +894,6 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHS go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= @@ -1553,11 +930,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1580,7 +955,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= @@ -1592,10 +966,6 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1620,11 +990,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= @@ -1662,7 +1029,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= @@ -1670,27 +1036,15 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= @@ -1718,19 +1072,8 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1742,10 +1085,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= @@ -1793,14 +1133,12 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1810,12 +1148,10 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1828,23 +1164,11 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1858,9 +1182,6 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= @@ -1878,8 +1199,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1890,9 +1209,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1950,22 +1266,17 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= @@ -1975,22 +1286,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -2035,30 +1340,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= -google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= +google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2104,13 +1387,10 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= @@ -2154,71 +1434,9 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= @@ -2250,21 +1468,8 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -2282,9 +1487,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2323,41 +1525,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/sdks/go/container/boot.go b/sdks/go/container/boot.go index b75201520f39..ab2da3169319 100644 --- a/sdks/go/container/boot.go +++ b/sdks/go/container/boot.go @@ -83,13 +83,13 @@ func configureGoogleCloudProfilerEnvVars(ctx context.Context, logger *tools.Logg } // Fallback to job_name from metadata - if profilerServiceName == "" { - if jobName, jobNameExists := metadata["job_name"]; jobNameExists { - profilerServiceName = jobName - } else { - return errors.New("required job_name missing from metadata, profiling will not be enabled without it") - } - } + if profilerServiceName == "" { + if jobName, jobNameExists := metadata["job_name"]; jobNameExists { + profilerServiceName = jobName + } else { + return errors.New("required job_name missing from metadata, profiling will not be enabled without it") + } + } jobID, idExists := metadata["job_id"] if !idExists { diff --git a/sdks/go/container/boot_test.go b/sdks/go/container/boot_test.go index 244f91fe42e7..bb94aca36be3 100644 --- a/sdks/go/container/boot_test.go +++ b/sdks/go/container/boot_test.go @@ -267,19 +267,19 @@ func TestConfigureGoogleCloudProfilerEnvVars(t *testing.T) { expectingError: true, }, { - name: "Missing profiler name and job_name", - options: `{ + name: "Missing profiler name and job_name", + options: `{ "beam:option:go_options:v1": { "options": { "dataflow_service_options": "enable_google_cloud_profiler" } } }`, - metadata: map[string]string{ - "job_id": "job-789", - }, - expectingError: true, - }, + metadata: map[string]string{ + "job_id": "job-789", + }, + expectingError: true, + }, } for _, tt := range tests { diff --git a/sdks/go/pkg/beam/io/fhirio/common.go b/sdks/go/pkg/beam/io/fhirio/common.go index 75781f7ba0cc..504e65270d58 100644 --- a/sdks/go/pkg/beam/io/fhirio/common.go +++ b/sdks/go/pkg/beam/io/fhirio/common.go @@ -127,11 +127,12 @@ func (c *fhirStoreClientImpl) search(storePath, resourceType string, queries map queryParams = append(queryParams, googleapi.QueryParameter(pageTokenParameterKey, pageToken)) } - searchRequest := &healthcare.SearchResourcesRequest{} + // Pass nil as the body because search parameters are passed via queryParams, + // and the new API expects an io.Reader instead of a specific struct. if resourceType == "" { - return c.fhirService().Search(storePath, searchRequest).Do(queryParams...) + return c.fhirService().Search(storePath, nil).Do(queryParams...) } - return c.fhirService().SearchType(storePath, resourceType, searchRequest).Do(queryParams...) + return c.fhirService().SearchType(storePath, resourceType, nil).Do(queryParams...) } func (c *fhirStoreClientImpl) deidentify(srcStorePath, dstStorePath string, deidConfig *healthcare.DeidentifyConfig) (operationResults, error) { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow.go b/sdks/go/pkg/beam/runners/dataflow/dataflow.go index 101441dbcb56..ecbfe53939ec 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow.go @@ -52,32 +52,34 @@ import ( // TODO(herohde) 5/16/2017: the Dataflow flags should match the other SDKs. var ( - endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") - stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") - workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") - image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") - labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") - serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") - numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") - workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") - maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") - diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") - diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") - autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") - zone = flag.String("zone", "", "GCP zone (optional)") - kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") - network = flag.String("network", "", "GCP network (optional)") - subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") - noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") - usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") - tempLocation = flag.String("temp_location", "", "Temp location (optional)") - workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") - machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") - minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") - workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") - workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") - dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") - flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") + endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") + stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") + workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") + image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") + labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") + serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") + numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") + workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") + maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") + diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") + diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") + diskProvisionedIops = flag.Int64("disk_provisioned_iops", 0, "Provisioned IOPS for the worker disk (optional).") + diskProvisionedThroughputMibps = flag.Int64("disk_provisioned_throughput_mibps", 0, "Provisioned throughput in MiB/s for the worker disk (optional).") + autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") + zone = flag.String("zone", "", "GCP zone (optional)") + kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") + network = flag.String("network", "", "GCP network (optional)") + subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") + noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") + usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") + tempLocation = flag.String("temp_location", "", "Temp location (optional)") + workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") + machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") + minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") + workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") + workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") + dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") + flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") // TODO(https://github.com/apache/beam/issues/21604) Turn this on once TO_STRING is implemented // enableHotKeyLogging = flag.Bool("enable_hot_key_logging", false, "Specifies that when a hot key is detected in the pipeline, the literal, human-readable key is printed in the user's Cloud Logging project (optional).") @@ -105,33 +107,35 @@ func init() { // should be added to this map. // Don't filter temp_location since we need this included in PipelineOptions to correctly upload heap dumps. var flagFilter = map[string]bool{ - "dataflow_endpoint": true, - "staging_location": true, - "worker_harness_container_image": true, - "sdk_container_image": true, - "labels": true, - "service_account_email": true, - "num_workers": true, - "max_num_workers": true, - "disk_size_gb": true, - "disk_type": true, - "autoscaling_algorithm": true, - "zone": true, - "network": true, - "subnetwork": true, - "no_use_public_ips": true, - "template_location": true, - "worker_machine_type": true, - "machine_type": true, - "min_cpu_platform": true, - "dataflow_worker_jar": true, - "worker_region": true, - "worker_zone": true, - "teardown_policy": true, - "cpu_profiling": true, - "session_recording": true, - "update": true, - "transform_name_mapping": true, + "dataflow_endpoint": true, + "staging_location": true, + "worker_harness_container_image": true, + "sdk_container_image": true, + "labels": true, + "service_account_email": true, + "num_workers": true, + "max_num_workers": true, + "disk_size_gb": true, + "disk_type": true, + "disk_provisioned_iops": true, + "disk_provisioned_throughput_mibps": true, + "autoscaling_algorithm": true, + "zone": true, + "network": true, + "subnetwork": true, + "no_use_public_ips": true, + "template_location": true, + "worker_machine_type": true, + "machine_type": true, + "min_cpu_platform": true, + "dataflow_worker_jar": true, + "worker_region": true, + "worker_zone": true, + "teardown_policy": true, + "cpu_profiling": true, + "session_recording": true, + "update": true, + "transform_name_mapping": true, // Job Options flags "endpoint": true, @@ -379,37 +383,39 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions beam.PipelineOptions.LoadOptionsFromFlags(flagFilter) opts := &dataflowlib.JobOptions{ - Name: jobopts.GetJobName(), - Streaming: streaming, - Experiments: experiments, - DataflowServiceOptions: dfServiceOptions, - Options: beam.PipelineOptions.Export(), - Project: project, - Region: region, - Zone: *zone, - KmsKey: *kmsKey, - Network: *network, - Subnetwork: *subnetwork, - NoUsePublicIPs: *noUsePublicIPs, - NumWorkers: *numWorkers, - MaxNumWorkers: *maxNumWorkers, - WorkerHarnessThreads: *workerHarnessThreads, - DiskSizeGb: *diskSizeGb, - DiskType: *diskType, - Algorithm: *autoscalingAlgorithm, - FlexRSGoal: *flexRSGoal, - MachineType: *firstNonEmpty(workerMachineType, machineType), - Labels: jobLabels, - ServiceAccountEmail: *serviceAccountEmail, - TempLocation: *tempLocation, - TemplateLocation: *templateLocation, - Worker: *jobopts.WorkerBinary, - WorkerRegion: *workerRegion, - WorkerZone: *workerZone, - TeardownPolicy: *teardownPolicy, - ContainerImage: getContainerImage(ctx), - Update: *update, - TransformNameMapping: updateTransformMapping, + Name: jobopts.GetJobName(), + Streaming: streaming, + Experiments: experiments, + DataflowServiceOptions: dfServiceOptions, + Options: beam.PipelineOptions.Export(), + Project: project, + Region: region, + Zone: *zone, + KmsKey: *kmsKey, + Network: *network, + Subnetwork: *subnetwork, + NoUsePublicIPs: *noUsePublicIPs, + NumWorkers: *numWorkers, + MaxNumWorkers: *maxNumWorkers, + WorkerHarnessThreads: *workerHarnessThreads, + DiskSizeGb: *diskSizeGb, + DiskType: *diskType, + DiskProvisionedIops: *diskProvisionedIops, + DiskProvisionedThroughputMibps: *diskProvisionedThroughputMibps, + Algorithm: *autoscalingAlgorithm, + FlexRSGoal: *flexRSGoal, + MachineType: *firstNonEmpty(workerMachineType, machineType), + Labels: jobLabels, + ServiceAccountEmail: *serviceAccountEmail, + TempLocation: *tempLocation, + TemplateLocation: *templateLocation, + Worker: *jobopts.WorkerBinary, + WorkerRegion: *workerRegion, + WorkerZone: *workerZone, + TeardownPolicy: *teardownPolicy, + ContainerImage: getContainerImage(ctx), + Update: *update, + TransformNameMapping: updateTransformMapping, } if opts.TempLocation == "" { opts.TempLocation = gcsx.Join(*stagingLocation, "tmp") diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go index fa418ebbe6a0..23dcd034120a 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go @@ -516,6 +516,26 @@ func getFieldFromOpt(fieldName string, opts *dataflowlib.JobOptions) string { return reflect.ValueOf(opts).Elem().FieldByName(fieldName).String() } +func TestGetJobOptions_DiskProvisionedOptions(t *testing.T) { + resetGlobals() + *stagingLocation = "gs://testStagingLocation" + *gcpopts.Project = "testProject" + *gcpopts.Region = "testRegion" + *diskProvisionedIops = 1000 + *diskProvisionedThroughputMibps = 100 + + opts, err := getJobOptions(context.Background(), false) + if err != nil { + t.Fatalf("getJobOptions() returned error %q, want %q", err, "nil") + } + if got, want := opts.DiskProvisionedIops, int64(1000); got != want { + t.Errorf("opts.DiskProvisionedIops = %d, want %d", got, want) + } + if got, want := opts.DiskProvisionedThroughputMibps, int64(100); got != want { + t.Errorf("opts.DiskProvisionedThroughputMibps = %d, want %d", got, want) + } +} + func resetGlobals() { *autoscalingAlgorithm = "" *dataflowServiceOptions = "" @@ -537,4 +557,6 @@ func resetGlobals() { *workerHarnessImage = "" *workerMachineType = "" *machineType = "" + *diskProvisionedIops = 0 + *diskProvisionedThroughputMibps = 0 } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go index db83992fbebc..2f8057b6d506 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go @@ -47,26 +47,28 @@ type JobOptions struct { // Pipeline options Options runtime.RawOptions - Streaming bool - Project string - Region string - Zone string - KmsKey string - Network string - Subnetwork string - NoUsePublicIPs bool - NumWorkers int64 - DiskSizeGb int64 - DiskType string - MachineType string - Labels map[string]string - ServiceAccountEmail string - WorkerRegion string - WorkerZone string - ContainerImage string - ArtifactURLs []string // Additional packages for workers. - FlexRSGoal string - EnableHotKeyLogging bool + Streaming bool + Project string + Region string + Zone string + KmsKey string + Network string + Subnetwork string + NoUsePublicIPs bool + NumWorkers int64 + DiskSizeGb int64 + DiskType string + DiskProvisionedIops int64 + DiskProvisionedThroughputMibps int64 + MachineType string + Labels map[string]string + ServiceAccountEmail string + WorkerRegion string + WorkerZone string + ContainerImage string + ArtifactURLs []string // Additional packages for workers. + FlexRSGoal string + EnableHotKeyLogging bool // Streaming update settings Update bool @@ -154,6 +156,9 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker return nil, err } + if opts.Options.Options == nil { + opts.Options.Options = make(map[string]string) + } opts.Options.Options["experiments"] = strings.Join(opts.Experiments, ",") job := &df.Job{ ProjectId: opts.Project, @@ -189,18 +194,20 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker AutoscalingSettings: &df.AutoscalingSettings{ MaxNumWorkers: opts.MaxNumWorkers, }, - DiskSizeGb: opts.DiskSizeGb, - DiskType: opts.DiskType, - IpConfiguration: ipConfiguration, - Kind: "harness", - Packages: packages, - WorkerHarnessContainerImage: opts.ContainerImage, - SdkHarnessContainerImages: dfImages, - NumWorkers: 1, - MachineType: opts.MachineType, - Network: opts.Network, - Subnetwork: opts.Subnetwork, - Zone: opts.Zone, + DiskSizeGb: opts.DiskSizeGb, + DiskType: opts.DiskType, + DiskProvisionedIops: opts.DiskProvisionedIops, + DiskProvisionedThroughputMibps: opts.DiskProvisionedThroughputMibps, + IpConfiguration: ipConfiguration, + Kind: "harness", + Packages: packages, + WorkerHarnessContainerImage: opts.ContainerImage, + SdkHarnessContainerImages: dfImages, + NumWorkers: 1, + MachineType: opts.MachineType, + Network: opts.Network, + Subnetwork: opts.Subnetwork, + Zone: opts.Zone, }}, WorkerRegion: opts.WorkerRegion, WorkerZone: opts.WorkerZone, @@ -350,10 +357,12 @@ func GetMetrics(ctx context.Context, client *df.Service, project, region, jobID // pipeline options that are communicated to cross-language SDK harnesses, so any pipeline options // needed for cross-language transforms in Dataflow must be declared here. type dataflowOptions struct { - Experiments []string `json:"experiments,omitempty"` - PipelineURL string `json:"pipelineUrl"` - Region string `json:"region"` - TempLocation string `json:"tempLocation"` + Experiments []string `json:"experiments,omitempty"` + PipelineURL string `json:"pipelineUrl"` + Region string `json:"region"` + TempLocation string `json:"tempLocation"` + DiskProvisionedIops int64 `json:"diskProvisionedIops"` + DiskProvisionedThroughputMibps int64 `json:"diskProvisionedThroughputMibps"` } func printOptions(opts *JobOptions, images []string) []*displayData { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go index fb44ff9c0133..303fcb776bff 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go @@ -280,3 +280,33 @@ func Test_containerImages(t *testing.T) { } } + +func TestTranslate(t *testing.T) { + ctx := context.Background() + p := &pipepb.Pipeline{} + opts := &JobOptions{ + Project: "test-project", + Name: "test-job", + DiskProvisionedIops: 4000, + DiskProvisionedThroughputMibps: 200, + } + workerURL := "gs://any-location/temp" + modelURL := "gs://any-location/temp" + + job, err := Translate(ctx, p, opts, workerURL, modelURL) + if err != nil { + t.Fatalf("Translate(...) error = %v, want nil", err) + } + + if len(job.Environment.WorkerPools) == 0 { + t.Fatal("Translate(...) returned job with no worker pools") + } + + wp := job.Environment.WorkerPools[0] + if wp.DiskProvisionedIops != 4000 { + t.Errorf("DiskProvisionedIops = %v, want 4000", wp.DiskProvisionedIops) + } + if wp.DiskProvisionedThroughputMibps != 200 { + t.Errorf("DiskProvisionedThroughputMibps = %v, want 200", wp.DiskProvisionedThroughputMibps) + } +} diff --git a/sdks/go/test/integration/expansions.go b/sdks/go/test/integration/expansions.go index 633f88d02930..2becce67333d 100644 --- a/sdks/go/test/integration/expansions.go +++ b/sdks/go/test/integration/expansions.go @@ -105,7 +105,7 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { } addr := "localhost:" + portStr - + // Use different wait strategies for test mode vs production if es.testMode { // In test mode, use simple wait time for compatibility with mock processes From 46b832d7eb9334f7982acf40fc9a6f0a1b728cc4 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 1 May 2026 17:49:46 +0400 Subject: [PATCH 052/490] Update go version --- .github/workflows/beam_Playground_CI_Nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_Playground_CI_Nightly.yml b/.github/workflows/beam_Playground_CI_Nightly.yml index 0241512f994d..2a0cba7bf2a3 100644 --- a/.github/workflows/beam_Playground_CI_Nightly.yml +++ b/.github/workflows/beam_Playground_CI_Nightly.yml @@ -66,7 +66,7 @@ jobs: uses: ./.github/actions/setup-environment-action with: python-version: default - go-version: '1.25' + go-version: '1.26' - name: Install requirements run: | cd $BEAM_ROOT_DIR/playground/infrastructure From 30af415d329a37fe7e83f8da6226090f5aa84c06 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 1 May 2026 18:20:02 +0400 Subject: [PATCH 053/490] Update go version base image --- playground/backend/containers/go/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/backend/containers/go/build.gradle b/playground/backend/containers/go/build.gradle index ad236e10d50f..6e75929b1444 100644 --- a/playground/backend/containers/go/build.gradle +++ b/playground/backend/containers/go/build.gradle @@ -88,7 +88,7 @@ docker { buildArgs( ['BASE_IMAGE' : project.rootProject.hasProperty(["base-image"]) ? project.rootProject["base-image"] : - "golang:1.25", + "golang:1.26", 'SDK_TAG' : project.rootProject.hasProperty(["sdk-tag"]) ? project.rootProject["sdk-tag"] : project.rootProject.sdk_version, 'SDK_TAG_LOCAL': project.rootProject.sdk_version, From 882361b9fe52f70f4da1d8541ad503adf9963fc9 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 1 May 2026 23:37:51 +0400 Subject: [PATCH 054/490] Install wget --- .github/workflows/beam_Playground_Precommit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index 8c40a08f7ac2..fff171782cc6 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -66,7 +66,7 @@ jobs: - name: Install sbt for running SCIO tests run: | sudo apt-get update --yes - sudo apt-get install apt-transport-https curl gnupg -yqq + sudo apt-get install wget apt-transport-https curl gnupg -yqq echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | sudo tee /etc/apt/sources.list.d/sbt_old.list curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo -H gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/scalasbt-release.gpg --import From 41c266cec93766ca66c3dfa1f8f3a924010f0286 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 1 May 2026 23:46:01 +0400 Subject: [PATCH 055/490] Install wget --- .github/workflows/beam_Playground_Precommit.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index fff171782cc6..80cc2015ae9d 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -62,11 +62,14 @@ jobs: - name: Add GOPATH/bin to PATH run: echo "PATH=$PATH:$(go env GOPATH)/bin" >> $GITHUB_ENV - + - name: Install Dependencies + run: | + sudo apt-get update --yes + sudo apt-get install -y wget - name: Install sbt for running SCIO tests run: | sudo apt-get update --yes - sudo apt-get install wget apt-transport-https curl gnupg -yqq + sudo apt-get install apt-transport-https curl gnupg -yqq echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | sudo tee /etc/apt/sources.list.d/sbt_old.list curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo -H gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/scalasbt-release.gpg --import From a51ca6ab91f4b0930ea654b0da6b508aef79d516 Mon Sep 17 00:00:00 2001 From: johnjcasey <95318300+johnjcasey@users.noreply.github.com> Date: Fri, 1 May 2026 16:01:00 -0400 Subject: [PATCH 056/490] Revert "Fix unhandled exception in KafkaIO SDF (#37449) (#37553)" (#38361) This reverts commit a4cb67621c6a24387c6699564638ee38effd5119. --- .../org/apache/beam/sdk/io/kafka/KafkaIO.java | 12 ++++-- .../kafka/WatchForKafkaTopicPartitions.java | 15 +++---- .../WatchForKafkaTopicPartitionsTest.java | 43 ------------------- 3 files changed, 13 insertions(+), 57 deletions(-) diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java index f9bfdd87aaac..518319a38e32 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIO.java @@ -2130,12 +2130,16 @@ public void processElement(OutputReceiver receiver) { } else { for (String topic : topics) { List partitionInfoList = consumer.partitionsFor(topic); - if (partitionInfoList == null || partitionInfoList.isEmpty()) { + if (logTopicVerification == null || !logTopicVerification) { + checkState( + partitionInfoList != null && !partitionInfoList.isEmpty(), + "Could not find any partitions info for topic %s. Please check Kafka configuration and make sure that provided topics exist.", + topic); + } else { LOG.warn( - "Could not find any partitions info for topic {}. Please check Kafka " - + "configuration and make sure that the provided topics exist.", + "Could not find any partitions info for topic {}. Please check Kafka configuration " + + "and make sure that the provided topics exist.", topic); - continue; } for (PartitionInfo p : partitionInfoList) { diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java index 3184b18267b2..490faafb22fa 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitions.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.kafka; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects.firstNonNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.util.ArrayList; import java.util.List; @@ -44,8 +45,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A {@link PTransform} for continuously querying Kafka for new partitions, and emitting those @@ -58,7 +57,6 @@ */ class WatchForKafkaTopicPartitions extends PTransform> { - private static final Logger LOG = LoggerFactory.getLogger(WatchForKafkaTopicPartitions.class); private static final Duration DEFAULT_CHECK_DURATION = Duration.standardHours(1); private static final String COUNTER_NAMESPACE = "watch_kafka_topic_partition"; @@ -193,13 +191,10 @@ static List getAllTopicPartitions( if (topics != null && !topics.isEmpty()) { for (String topic : topics) { List partitionInfoList = kafkaConsumer.partitionsFor(topic); - if (partitionInfoList == null || partitionInfoList.isEmpty()) { - LOG.warn( - "Could not find any partitions info for topic {}. Please check Kafka " - + "configuration and make sure that the provided topics exist.", - topic); - continue; - } + checkState( + partitionInfoList != null && !partitionInfoList.isEmpty(), + "Could not find any partitions info for topic %s. Please check Kafka configuration and make sure that provided topics exist.", + topic); for (PartitionInfo partition : partitionInfoList) { current.add(new TopicPartition(topic, partition.partition())); } diff --git a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java index 30ace6cd86d0..595d040bf403 100644 --- a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java +++ b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/WatchForKafkaTopicPartitionsTest.java @@ -18,12 +18,10 @@ package org.apache.beam.sdk.io.kafka; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Collections; import java.util.Set; import java.util.regex.Pattern; import org.apache.beam.sdk.io.kafka.KafkaMocks.PartitionGrowthMockConsumer; @@ -110,47 +108,6 @@ public void testGetAllTopicPartitionsWithGivenTopics() throws Exception { (input) -> mockConsumer, null, givenTopics, null)); } - @Test - public void testGetAllTopicPartitionsWithNullPartitionInfo() throws Exception { - Set givenTopics = ImmutableSet.of("topic1"); - - Consumer mockConsumer = Mockito.mock(Consumer.class); - when(mockConsumer.partitionsFor("topic1")).thenReturn(null); - assertTrue( - WatchForKafkaTopicPartitions.getAllTopicPartitions( - (input) -> mockConsumer, null, givenTopics, null) - .isEmpty()); - } - - @Test - public void testGetAllTopicPartitionsWithEmptyPartitionInfo() throws Exception { - Set givenTopics = ImmutableSet.of("topic1"); - - Consumer mockConsumer = Mockito.mock(Consumer.class); - when(mockConsumer.partitionsFor("topic1")).thenReturn(Collections.emptyList()); - assertTrue( - WatchForKafkaTopicPartitions.getAllTopicPartitions( - (input) -> mockConsumer, null, givenTopics, null) - .isEmpty()); - } - - @Test - public void testGetAllTopicPartitionsSkipsMissingTopics() throws Exception { - Set givenTopics = ImmutableSet.of("topic1", "topic2"); - - Consumer mockConsumer = Mockito.mock(Consumer.class); - when(mockConsumer.partitionsFor("topic1")).thenReturn(null); - when(mockConsumer.partitionsFor("topic2")) - .thenReturn( - ImmutableList.of( - new PartitionInfo("topic2", 0, null, null, null), - new PartitionInfo("topic2", 1, null, null, null))); - assertEquals( - ImmutableList.of(new TopicPartition("topic2", 0), new TopicPartition("topic2", 1)), - WatchForKafkaTopicPartitions.getAllTopicPartitions( - (input) -> mockConsumer, null, givenTopics, null)); - } - @Test public void testGetAllTopicPartitionsWithGivenPattern() throws Exception { Consumer mockConsumer = Mockito.mock(Consumer.class); From 588195a802857ab75991b3550cc8f399841c8dca Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 1 May 2026 17:52:35 -0400 Subject: [PATCH 057/490] Bump Java bytecode compatibility version to Java11 (#38267) --- ...PostCommit_Java_Examples_Dataflow_Java.yml | 2 +- ...tCommit_Java_Examples_Dataflow_V2_Java.yml | 2 +- .../beam_PostCommit_Java_Hadoop_Versions.yml | 12 -- ...ommit_Java_ValidatesRunner_Flink_Java8.yml | 102 -------------- ...ommit_Java_ValidatesRunner_Spark_Java8.yml | 102 -------------- .../workflows/beam_PreCommit_SQL_Java8.yml | 129 ------------------ CHANGES.md | 3 + .../beam/gradle/BeamModulePlugin.groovy | 9 +- gradle.properties | 2 +- ...reamingEngineComputationConfigFetcher.java | 2 +- .../beam/sdk/io/synthetic/SyntheticStep.java | 2 + sdks/java/testing/test-utils/build.gradle | 2 +- .../jvmverification/JvmVerification.java | 17 +-- .../content/en/documentation/sdks/java.md | 2 +- 14 files changed, 21 insertions(+), 367 deletions(-) delete mode 100644 .github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml delete mode 100644 .github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml delete mode 100644 .github/workflows/beam_PreCommit_SQL_Java8.yml diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml index 007ca1a600c0..f38bd7ae5c1b 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml @@ -60,7 +60,7 @@ jobs: matrix: job_name: [beam_PostCommit_Java_Examples_Dataflow_Java] job_phrase: [Run Java examples on Dataflow Java] - java_version: ['8','17','21', '25'] + java_version: ['17', '21', '25'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml index 373e49524a65..72d59d89f8f3 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml @@ -61,7 +61,7 @@ jobs: job_name: [beam_PostCommit_Java_Examples_Dataflow_V2_Java] job_phrase_1: [Run Java ] job_phrase_2: [Examples on Dataflow Runner V2] - java_version: ['8', '17', '21', '25'] + java_version: ['17', '21', '25'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml index 8c0d35e87f07..e0c26b293169 100644 --- a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml @@ -72,22 +72,10 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - with: - java-version: | - 8 - 11 - name: run javaHadoopVersionsTest script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :javaHadoopVersionsTest - # TODO(https://github.com/apache/beam/issues/32189) remove when embedded hive supports Java11 - - name: run java8HadoopVersionsTest script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :sdks:java:io:hcatalog:hadoopVersionsTest - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - name: Archive JUnit Test Results uses: actions/upload-artifact@v4 if: ${{ !success() }} diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml deleted file mode 100644 index ac4093a361c4..000000000000 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml +++ /dev/null @@ -1,102 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -name: PostCommit Java ValidatesRunner Flink Java8 - -on: - schedule: - - cron: '45 5/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.json'] - workflow_dispatch: - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Java_ValidatesRunner_Flink_Java8: - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 270 - strategy: - matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Flink_Java8] - job_phrase: [Run Flink ValidatesRunner Java 8] - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - startswith(github.event.comment.body, 'Run Flink ValidatesRunner Java 8') - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - with: - java-version: | - 8 - 11 - - name: run validatesRunner Java8 script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:flink:1.20:validatesRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - max-workers: 12 - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - large_files: true - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml deleted file mode 100644 index fab48ec60019..000000000000 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml +++ /dev/null @@ -1,102 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -name: PostCommit Java ValidatesRunner Spark Java8 - -on: - schedule: - - cron: '45 4/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.json'] - workflow_dispatch: - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Java_ValidatesRunner_Spark_Java8: - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 270 - strategy: - matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Spark_Java8] - job_phrase: [Run Spark ValidatesRunner Java 8] - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - startswith(github.event.comment.body, 'Run Spark ValidatesRunner Java 8') - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - with: - java-version: | - 8 - 11 - - name: run validatesRunner Java8 script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:spark:3:validatesRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - max-workers: 12 - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file diff --git a/.github/workflows/beam_PreCommit_SQL_Java8.yml b/.github/workflows/beam_PreCommit_SQL_Java8.yml deleted file mode 100644 index 1e69b3e3ca4c..000000000000 --- a/.github/workflows/beam_PreCommit_SQL_Java8.yml +++ /dev/null @@ -1,129 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: PreCommit SQL Java8 - -on: - push: - tags: ['v*'] - branches: ['master', 'release-*'] - paths: ['sdks/java/extensions/sql/**','.github/workflows/beam_PreCommit_SQL_Java8.yml'] - pull_request_target: - branches: ['master', 'release-*'] - paths: ['sdks/java/extensions/sql/**', 'release/trigger_all_tests.json', '.github/trigger_files/beam_PreCommit_SQL_Java8.json'] - issue_comment: - types: [created] - schedule: - - cron: '15 3/6 * * *' - workflow_dispatch: - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.event.pull_request.head.label || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -# Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -jobs: - beam_PreCommit_SQL_Java8: - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] - strategy: - matrix: - job_name: [beam_PreCommit_SQL_Java8] - job_phrase: [Run SQL_Java8 PreCommit] - timeout-minutes: 120 - if: | - github.event_name == 'push' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event_name == 'workflow_dispatch' || - github.event.comment.body == 'Run SQL_Java8 PreCommit' - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - with: - java-version: | - 8 - 11 - python-version: default - go-version: default - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.10.4' - channel: 'stable' - - name: Build and Test - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :sdks:java:extensions:sql:preCommit - arguments: | - -PdisableSpotlessCheck=true \ - -PdisableCheckStyle=true \ - -PtestJavaVersion=8 \ - -PskipCheckerFramework \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' - large_files: true - - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 - if: always() - with: - name: SpotBugs Results - path: '**/build/reports/spotbugs/*.html' - - name: Publish SpotBugs Results - uses: jwgmeligmeyling/spotbugs-github-action@v1.2 - if: always() - with: - name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 9f35964cf145..8574448d0898 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -77,6 +77,8 @@ ([#38139](https://github.com/apache/beam/issues/38139)). * (Python) Added type alias for with_exception_handling to be used for typehints. ([#38173](https://github.com/apache/beam/issues/38173)). * Added plugin mechanism to support different Lineage implementations (Java) ([#36790](https://github.com/apache/beam/issues/36790)). +* (Python) Supported Python user type in Beam SQL. For example, SQL statements like `SELECT some_field from PCOLLECTION` can now operate a PCollection of Beam Row containing pickable Python user type ([#20738](https://github.com/apache/beam/issues/20738)). +* (Python) Introduced `beam.coders.registry.register_row` as preferred API to register a named tuple or dataclass with a Beam Row. At pipelne runtime, the original type associated with the registered row are preserved across the serialization boundary ([#38108](https://github.com/apache/beam/issues/38108)). ## Breaking Changes @@ -85,6 +87,7 @@ ## Deprecations * X behavior is deprecated and will be removed in X versions ([#X](https://github.com/apache/beam/issues/X)). +* Dropped Java 8 support ([#31678](https://github.com/apache/beam/issues/31678)). * Removed Samza Runner support ([#35448](https://github.com/apache/beam/issues/35448)). ## Bugfixes diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 080c20678dfd..22655f696268 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -1135,6 +1135,8 @@ class BeamModulePlugin implements Plugin { project.javaVersion = '17' } else if (JavaVersion.VERSION_21.equals(configuration.requireJavaVersion)) { project.javaVersion = '21' + } else if (JavaVersion.VERSION_25.equals(configuration.requireJavaVersion)) { + project.javaVersion = '25' } else { throw new GradleException( "requireJavaVersion has to be supported LTS version greater than the default Java version. Actual: " + @@ -1157,6 +1159,9 @@ class BeamModulePlugin implements Plugin { } else if (requireJavaVersion.compareTo(JavaVersion.VERSION_21) <= 0 && project.hasProperty('java21Home')) { forkJavaVersion = '21' + } else if (requireJavaVersion.compareTo(JavaVersion.VERSION_25) <= 0 && + project.hasProperty('java25Home')) { + forkJavaVersion = '25' } else { logger.config("Module ${project.name} disabled. To enable, either " + "compile on newer Java version or pass java${project.javaVersion}Home project property") @@ -1580,7 +1585,7 @@ class BeamModulePlugin implements Plugin { options.encoding = "UTF-8" // If compiled on newer JDK, set byte code compatibility if (requireJavaVersion.compareTo(JavaVersion.current()) < 0) { - def compatVersion = project.javaVersion == '1.8' ? '8' : project.javaVersion + def compatVersion = project.javaVersion == '11' ? '11' : project.javaVersion options.compilerArgs += ['--release', compatVersion] // TODO(https://github.com/apache/beam/issues/23901): Fix // optimizerOuterThis breakage @@ -1614,7 +1619,7 @@ class BeamModulePlugin implements Plugin { } // if specified test java version, modify the compile and runtime versions accordingly - if (['8', '11', '17', '21', '25'].contains(project.findProperty('testJavaVersion'))) { + if (['11', '17', '21', '25'].contains(project.findProperty('testJavaVersion'))) { String ver = project.getProperty('testJavaVersion') def testJavaHome = project.getProperty("java${ver}Home") diff --git a/gradle.properties b/gradle.properties index 2cff0b656fa7..583eb24d8964 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,7 +33,7 @@ signing.gnupg.useLegacyGpg=true version=2.74.0-SNAPSHOT sdk_version=2.74.0.dev -javaVersion=1.8 +javaVersion=11 docker_image_default_repo_root=apache docker_image_default_repo_prefix=beam_ diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java index 6ade84403713..08263bd2243a 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java @@ -171,7 +171,7 @@ private StreamingGlobalConfig createPipelineConfig(StreamingConfigTask config) { ImmutableSet endpoints = StreamSupport.stream( Splitter.on(',').split(config.getWindmillServiceEndpoint()).spliterator(), - /* isParallel= */ false) + /* parallel= */ false) .map(endpoint -> HostAndPort.fromString(endpoint).withDefaultPort(windmillPort)) .collect(toImmutableSet()); diff --git a/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java b/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java index 78e0d4ff5f3a..1e0de64bc091 100644 --- a/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java +++ b/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java @@ -21,6 +21,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; import com.fasterxml.jackson.annotation.JsonProperty; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Random; import java.util.concurrent.TimeUnit; import org.apache.beam.sdk.io.synthetic.delay.SyntheticDelay; @@ -83,6 +84,7 @@ public SyntheticStep(Options options) { } @ProcessElement + @SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE") // intended public void processElement(ProcessContext c) throws Exception { byte[] key = c.element().getKey(); byte[] val = c.element().getValue(); diff --git a/sdks/java/testing/test-utils/build.gradle b/sdks/java/testing/test-utils/build.gradle index b5ab063c1007..348af2ccba65 100644 --- a/sdks/java/testing/test-utils/build.gradle +++ b/sdks/java/testing/test-utils/build.gradle @@ -46,7 +46,7 @@ dependencies { ['8', '11', '17', '21', '25'].each { String ver -> tasks.create(name: "verifyJavaVersion${ver}", type: Test) { filter { - includeTestsMatching "org.apache.beam.sdk.testutils.jvmverification.JvmVerification.verifyCodeIsCompiledWithJava8" + includeTestsMatching "org.apache.beam.sdk.testutils.jvmverification.JvmVerification.verifyCodeIsCompiledWithJava11" includeTestsMatching "org.apache.beam.sdk.testutils.jvmverification.JvmVerification.verifyTestCodeIsCompiledWithJava${ver}" includeTestsMatching "org.apache.beam.sdk.testutils.jvmverification.JvmVerification.verifyRunningJVMVersionIs${ver}" } diff --git a/sdks/java/testing/test-utils/src/test/java/org/apache/beam/sdk/testutils/jvmverification/JvmVerification.java b/sdks/java/testing/test-utils/src/test/java/org/apache/beam/sdk/testutils/jvmverification/JvmVerification.java index c90808d418ea..8ffad1c1f269 100644 --- a/sdks/java/testing/test-utils/src/test/java/org/apache/beam/sdk/testutils/jvmverification/JvmVerification.java +++ b/sdks/java/testing/test-utils/src/test/java/org/apache/beam/sdk/testutils/jvmverification/JvmVerification.java @@ -45,15 +45,10 @@ public class JvmVerification { versionMapping.put("0045", v25); } - // bytecode + // bytecode compatibility @Test - public void verifyCodeIsCompiledWithJava8() throws IOException { - assertEquals(v1_8, getByteCodeVersion(DoFn.class)); - } - - @Test - public void verifyTestCodeIsCompiledWithJava8() throws IOException { - assertEquals(v1_8, getByteCodeVersion(JvmVerification.class)); + public void verifyCodeIsCompiledWithJava11() throws IOException { + assertEquals(v11, getByteCodeVersion(DoFn.class)); } @Test @@ -77,12 +72,6 @@ public void verifyTestCodeIsCompiledWithJava25() throws IOException { } // jvm - @Test - public void verifyRunningJVMVersionIs8() { - final String version = getJavaSpecification(); - assertEquals(v1_8.name, version); - } - @Test public void verifyRunningJVMVersionIs11() { final String version = getJavaSpecification(); diff --git a/website/www/site/content/en/documentation/sdks/java.md b/website/www/site/content/en/documentation/sdks/java.md index 971d9951197f..659e1078ed89 100644 --- a/website/www/site/content/en/documentation/sdks/java.md +++ b/website/www/site/content/en/documentation/sdks/java.md @@ -78,6 +78,6 @@ Apache Beam lets you combine transforms written in any supported SDK language an 8 - 2.x + ≤ 2.73.0 \ No newline at end of file From 07844a47b5cde456a04bd2fbed2d7388dd83df38 Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Mon, 4 May 2026 05:53:58 -0700 Subject: [PATCH 058/490] Optimze away WindmillWatermarkHold::clear when the cached hold is empty (#38297) --- .../windmill/state/WindmillWatermarkHold.java | 6 +++- .../worker/StreamingDataflowWorkerTest.java | 12 ++----- .../state/WindmillStateInternalsTest.java | 34 +++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java index 613d87c127b7..b426b96cb5b9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java @@ -72,9 +72,13 @@ public class WindmillWatermarkHold extends WindmillState implements WatermarkHol @Override public void clear() { + localAdditions = null; + if (cachedValue != null && !cachedValue.isPresent()) { + // No need to clear the backend as it is known empty. + return; + } cleared = true; cachedValue = Optional.absent(); - localAdditions = null; } @Override diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java index 3a25a671ca92..cff5ba4a2fc4 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java @@ -2374,9 +2374,6 @@ public void testMergeSessionWindows_singleLateWindow() throws Exception { new Action( buildSessionInput( 1, 40, 0, Collections.singletonList(1L), Collections.EMPTY_LIST)) - .withHolds( - buildHold("/gAAAAAAAAAsK/+uhold", -1, true), - buildHold("/gAAAAAAAAAsK/+uextra", -1, true)) .withTimers(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 3600010)))); } @@ -2404,10 +2401,7 @@ public void testMergeSessionWindows() throws Exception { 0, Collections.EMPTY_LIST, Collections.singletonList(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 10)))) - .withTimers(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 3600010)) - .withHolds( - buildHold("/gAAAAAAAAAsK/+uhold", -1, true), - buildHold("/gAAAAAAAAAsK/+uextra", -1, true)), + .withTimers(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 3600010)), new Action( buildSessionInput( 3, 30, 0, Collections.singletonList(8L), Collections.EMPTY_LIST)) @@ -2436,8 +2430,8 @@ public void testMergeSessionWindows() throws Exception { .withHolds( buildHold("/gAAAAAAAACkK/+uhold", -1, true), buildHold("/gAAAAAAAACkK/+uextra", -1, true), - buildHold("/gAAAAAAAAAsK/+uhold", 40, true), - buildHold("/gAAAAAAAAAsK/+uextra", 3600040, true)), + buildHold("/gAAAAAAAAAsK/+uhold", 40, false), + buildHold("/gAAAAAAAAAsK/+uextra", 3600040, false)), new Action( buildSessionInput( 6, diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java index de036214b958..5fbae493581b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java @@ -3228,6 +3228,40 @@ public void testNewWatermarkNoFetch() throws Exception { Mockito.verifyNoInteractions(mockReader); } + @Test + public void testWatermarkClearNoOp() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + WatermarkHoldState hold = underTestNewKey.state(NAMESPACE, addr); + + hold.clear(); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTestNewKey.persist(commitBuilder); + + assertEquals(0, commitBuilder.getWatermarkHoldsCount()); + assertBuildable(commitBuilder); + } + + @Test + public void testWatermarkClearWithLocalAdditionsNoop() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + WatermarkHoldState hold = underTestNewKey.state(NAMESPACE, addr); + + hold.add(new Instant(500)); + + hold.clear(); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTestNewKey.persist(commitBuilder); + + assertEquals(0, commitBuilder.getWatermarkHoldsCount()); + assertBuildable(commitBuilder); + } + @Test public void testValueSetBeforeRead() throws Exception { StateTag> addr = StateTags.value("value", StringUtf8Coder.of()); From fb32ba5826672b47c3882c9b9f76ac864e62387a Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Mon, 4 May 2026 16:04:41 +0200 Subject: [PATCH 059/490] [runners-spark] Use robust constructor resolution in EncoderFactory (#38271) * [runners-spark] Use robust constructor resolution in EncoderFactory Replace `(Constructor) X.class.getConstructors()[0]` for StaticInvoke, Invoke, and NewInstance with a `primaryConstructor()` helper that picks the constructor with the most parameters. Class.getConstructors() returns constructors in JVM-defined order that is not guaranteed stable, so resolving the widest constructor explicitly makes the lookup robust to future Spark releases that add overloaded constructors. Today this is a no-op: StaticInvoke / Invoke / NewInstance only have one public constructor each in Spark 3.1.x through 3.5.x, so getConstructors()[0] and the widest constructor resolve to the same one. The change is purely defensive. Same fix has already landed in the Spark 4 override in PR #38255 (commit 9c071c5e, flagged by Gemini Code Assist round 1). Porting it to the shared base keeps both code paths consistent. * Address Gemini review: guard primaryConstructor against empty ctors If Class.getConstructors() returns empty (e.g. all public constructors removed in a future Spark version), throw an IllegalStateException with the class name instead of letting ctors[0] surface as a cryptic ArrayIndexOutOfBoundsException during static initialization. --- .../translation/helpers/EncoderFactory.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java index ceafc1642baa..793f1cb3e1f6 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java @@ -31,15 +31,32 @@ import scala.reflect.ClassTag; public class EncoderFactory { - // default constructor to reflectively create static invoke expressions + // Resolve the Scala case-class primary constructor (the one with the most parameters). + // Constructor ordering returned by Class.getConstructors() is JVM-defined and not stable + // across Spark versions, so we pick the widest constructor explicitly and then dispatch on + // parameter count below to pick the right argument shape per Spark version. private static final Constructor STATIC_INVOKE_CONSTRUCTOR = - (Constructor) StaticInvoke.class.getConstructors()[0]; + primaryConstructor(StaticInvoke.class); - private static final Constructor INVOKE_CONSTRUCTOR = - (Constructor) Invoke.class.getConstructors()[0]; + private static final Constructor INVOKE_CONSTRUCTOR = primaryConstructor(Invoke.class); private static final Constructor NEW_INSTANCE_CONSTRUCTOR = - (Constructor) NewInstance.class.getConstructors()[0]; + primaryConstructor(NewInstance.class); + + @SuppressWarnings("unchecked") + private static Constructor primaryConstructor(Class cls) { + Constructor[] ctors = cls.getConstructors(); + if (ctors.length == 0) { + throw new IllegalStateException("No public constructors found for " + cls.getName()); + } + Constructor widest = ctors[0]; + for (int i = 1; i < ctors.length; i++) { + if (ctors[i].getParameterCount() > widest.getParameterCount()) { + widest = ctors[i]; + } + } + return (Constructor) widest; + } static ExpressionEncoder create( Expression serializer, Expression deserializer, Class clazz) { From 0e724d650db1a1997a594e96a212e2db719008da Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Mon, 4 May 2026 10:07:03 -0400 Subject: [PATCH 060/490] add reshuffle as a first class yaml transform and a test (#38046) --- .../apache_beam/yaml/tests/reshuffle.yaml | 48 +++++++++++++++++++ sdks/python/apache_beam/yaml/yaml_provider.py | 17 +++++++ 2 files changed, 65 insertions(+) create mode 100644 sdks/python/apache_beam/yaml/tests/reshuffle.yaml diff --git a/sdks/python/apache_beam/yaml/tests/reshuffle.yaml b/sdks/python/apache_beam/yaml/tests/reshuffle.yaml new file mode 100644 index 000000000000..0683b9ce9d59 --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/reshuffle.yaml @@ -0,0 +1,48 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +pipelines: + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - {id: 1, value: "a"} + - {id: 2, value: "b"} + - {id: 3, value: "c"} + - {id: 4, value: "d"} + - {id: 5, value: "e"} + - {id: 6, value: "f"} + - {id: 7, value: "g"} + - {id: 8, value: "h"} + - {id: 9, value: "i"} + - {id: 10, value: "j"} + - type: Reshuffle + - type: AssertEqual + config: + elements: + - {id: 1, value: "a"} + - {id: 2, value: "b"} + - {id: 3, value: "c"} + - {id: 4, value: "d"} + - {id: 5, value: "e"} + - {id: 6, value: "f"} + - {id: 7, value: "g"} + - {id: 8, value: "h"} + - {id: 9, value: "i"} + - {id: 10, value: "j"} diff --git a/sdks/python/apache_beam/yaml/yaml_provider.py b/sdks/python/apache_beam/yaml/yaml_provider.py index 093e68cb9c7d..ce5a6e320589 100755 --- a/sdks/python/apache_beam/yaml/yaml_provider.py +++ b/sdks/python/apache_beam/yaml/yaml_provider.py @@ -1207,6 +1207,22 @@ def log_and_return(x): return pcoll | "LogForTesting" >> beam.Map(log_and_return) + class Reshuffle(beam.PTransform): + """Reshuffles the elements of a PCollection. + + Redistributes the elements of a PCollection to prevent fusion or balance + load. + + Args: + num_buckets: (optional) Specifies the maximum random keys that would be + generated. If not set, a default value is used. + """ + def __init__(self, num_buckets: Optional[int] = None): + self.num_buckets = num_buckets + + def expand(self, pcoll): + return pcoll | beam.Reshuffle(num_buckets=self.num_buckets) + @staticmethod def create_builtin_provider(): return InlineProvider({ @@ -1216,6 +1232,7 @@ def create_builtin_provider(): 'PyTransform': YamlProviders.fully_qualified_named_transform, 'Flatten': YamlProviders.Flatten, 'WindowInto': YamlProviders.WindowInto, + 'Reshuffle': YamlProviders.Reshuffle, }, no_input_transforms=('Create', )) From ab9f0f57b99f5835deeb85f0f9527ccc0b3cf0c6 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Sat, 2 May 2026 12:17:33 +0400 Subject: [PATCH 061/490] Run on ubuntu-24.04 --- .github/workflows/beam_PostCommit_Python_Arm.yml | 2 +- .github/workflows/beam_Publish_Beam_SDK_Snapshots.yml | 2 +- .github/workflows/republish_released_docker_containers.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/beam_PostCommit_Python_Arm.yml b/.github/workflows/beam_PostCommit_Python_Arm.yml index 43b33e840fc0..852b21d53bc6 100644 --- a/.github/workflows/beam_PostCommit_Python_Arm.yml +++ b/.github/workflows/beam_PostCommit_Python_Arm.yml @@ -53,7 +53,7 @@ env: jobs: beam_PostCommit_Python_Arm: name: ${{ matrix.job_name }} ${{ matrix.python_version }} - runs-on: ubuntu-22.04 + runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 480 strategy: fail-fast: false diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index 3a7f57cba08b..b91428f5ea96 100644 --- a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml +++ b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml @@ -52,7 +52,7 @@ jobs: if: | github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') - runs-on: ubuntu-22.04 + runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 300 name: ${{ matrix.job_name }} (${{ matrix.container_task }}) strategy: diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index afba70e3d164..b1b4618f2f74 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -38,7 +38,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: [self-hosted, ubuntu-24.04, main] strategy: fail-fast: false matrix: From d9ca5760785ada708354188388101683bc99db10 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 4 May 2026 12:33:09 -0400 Subject: [PATCH 062/490] Improve logging in boot.go to facilitate future triaging (#38342) --- sdks/python/container/boot.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/sdks/python/container/boot.go b/sdks/python/container/boot.go index 7c0f22675daf..a2655903a4b1 100644 --- a/sdks/python/container/boot.go +++ b/sdks/python/container/boot.go @@ -185,10 +185,14 @@ func launchSDKProcess() error { } experiments := getExperiments(options) + logger.Printf(ctx, "Experiments=%v", experiments) + pipNoBuildIsolation = false if slices.Contains(experiments, "pip_no_build_isolation") { pipNoBuildIsolation = true - logger.Printf(ctx, "Disabled build isolation when installing packages with pip") + logger.Printf(ctx, "Build isolation disabled when installing packages with pip") + } else { + logger.Printf(ctx, "Build isolation enabled when installing packages with pip") } // (2) Retrieve and install the staged packages. @@ -403,10 +407,14 @@ func setupVenv(ctx context.Context, logger *tools.Logger, baseDir, workerId stri return dir, nil } -// installSetupPackages installs Beam SDK and user dependencies. +// installSetupPackages installs user dependencies. func installSetupPackages(ctx context.Context, logger *tools.Logger, files []string, workDir string, requirementsFiles []string) error { bufLogger := tools.NewBufferedLogger(logger) - bufLogger.Printf(ctx, "Installing setup packages ...") + bufLogger.Printf(ctx, "Installing user dependencies ...") + + if err := logRuntimeDependencies(ctx, bufLogger, "initial runtime environment"); err != nil { + bufLogger.Printf(ctx, "Failed to fetch the runtime python dependencies: %v", err) + } // Install the Dataflow Python SDK if one was staged. In released // container images, SDK is already installed, but can be overriden @@ -432,11 +440,11 @@ func installSetupPackages(ctx context.Context, logger *tools.Logger, files []str if err := pipInstallPackage(ctx, logger, files, workDir, workflowFile, false, true, nil); err != nil { return fmt.Errorf("failed to install workflow: %v", err) } - if err := logRuntimeDependencies(ctx, bufLogger); err != nil { - bufLogger.Printf(ctx, "couldn't fetch the runtime python dependencies: %v", err) + if err := logRuntimeDependencies(ctx, bufLogger, "final runtime environment"); err != nil { + bufLogger.Printf(ctx, "Failed to fetch the runtime python dependencies: %v", err) } if err := logSubmissionEnvDependencies(ctx, bufLogger, workDir); err != nil { - bufLogger.Printf(ctx, "couldn't fetch the submission environment dependencies: %v", err) + bufLogger.Printf(ctx, "Failed to fetch the submission environment dependencies: %v", err) } return nil @@ -485,20 +493,20 @@ func processArtifactsInSetupOnlyMode() { // logRuntimeDependencies logs the python dependencies // installed in the runtime environment. -func logRuntimeDependencies(ctx context.Context, bufLogger *tools.BufferedLogger) error { +func logRuntimeDependencies(ctx context.Context, bufLogger *tools.BufferedLogger, phase string) error { pythonVersion, err := expansionx.GetPythonVersion() if err != nil { return err } - bufLogger.Printf(ctx, "Using Python version:") + bufLogger.Printf(ctx, "Python version in %s:", phase) args := []string{"--version"} if err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...); err != nil { bufLogger.FlushAtError(ctx) } else { bufLogger.FlushAtDebug(ctx) } - bufLogger.Printf(ctx, "Logging runtime dependencies:") - args = []string{"-m", "pip", "freeze"} + bufLogger.Printf(ctx, "Dependencies in %s:", phase) + args = []string{"-m", "pip", "freeze", "--all"} if err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...); err != nil { bufLogger.FlushAtError(ctx) } else { @@ -510,7 +518,7 @@ func logRuntimeDependencies(ctx context.Context, bufLogger *tools.BufferedLogger // logSubmissionEnvDependencies logs the python dependencies // installed in the submission environment. func logSubmissionEnvDependencies(ctx context.Context, bufLogger *tools.BufferedLogger, dir string) error { - bufLogger.Printf(ctx, "Logging submission environment dependencies:") + bufLogger.Printf(ctx, "Dependencies in submission environment:") // path for submission environment dependencies should match with the // one defined in apache_beam/runners/portability/stager.py. filename := filepath.Join(dir, "submission_environment_dependencies.txt") From 94e3804c3a5d3da0fa01b16fb231c11e96d9720d Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Mon, 4 May 2026 13:48:51 -0400 Subject: [PATCH 063/490] Upgrade github action versions (#38202) * rebase conflict * address merge conflicts * add wget install * fix conflict * remove accident rebased file * remove other accidental rebased files --- .../IO_Iceberg_Integration_Tests.yml | 2 +- .../IO_Iceberg_Integration_Tests_Dataflow.yml | 2 +- ...erg_Managed_Integration_Tests_Dataflow.yml | 2 +- .../IO_Iceberg_Performance_Tests.yml | 2 +- .github/workflows/IO_Iceberg_Unit_Tests.yml | 6 ++--- .github/workflows/assign_milestone.yml | 2 +- .../beam_CancelStaleDataflowJobs.yml | 2 +- .../beam_CleanUpDataprocResources.yml | 2 +- .../workflows/beam_CleanUpGCPResources.yml | 2 +- .../beam_CleanUpPrebuiltSDKImages.yml | 2 +- .../beam_CloudML_Benchmarks_Dataflow.yml | 2 +- .../beam_IODatastoresCredentialsRotation.yml | 2 +- ...m_Inference_Python_Benchmarks_Dataflow.yml | 2 +- .../beam_Infrastructure_PolicyEnforcer.yml | 2 +- .../beam_Infrastructure_SecurityLogging.yml | 2 +- ...beam_Infrastructure_ServiceAccountKeys.yml | 2 +- .../beam_Infrastructure_UsersPermissions.yml | 2 +- .github/workflows/beam_Java_JMH.yml | 2 +- .../beam_Java_LoadTests_Combine_Smoke.yml | 2 +- ...beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_CoGBK_Flink_batch.yml | 2 +- ...am_LoadTests_Go_Combine_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_Combine_Flink_Batch.yml | 2 +- .../beam_LoadTests_Go_GBK_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_GBK_Flink_Batch.yml | 2 +- ...beam_LoadTests_Go_ParDo_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_ParDo_Flink_Batch.yml | 2 +- ..._LoadTests_Go_SideInput_Dataflow_Batch.yml | 2 +- ...eam_LoadTests_Go_SideInput_Flink_Batch.yml | 2 +- ...am_LoadTests_Java_CoGBK_Dataflow_Batch.yml | 2 +- ...oadTests_Java_CoGBK_Dataflow_Streaming.yml | 4 +-- ...a_CoGBK_Dataflow_V2_Batch_JavaVersions.yml | 2 +- ...GBK_Dataflow_V2_Streaming_JavaVersions.yml | 2 +- ...a_CoGBK_SparkStructuredStreaming_Batch.yml | 2 +- ..._LoadTests_Java_Combine_Dataflow_Batch.yml | 2 +- ...dTests_Java_Combine_Dataflow_Streaming.yml | 2 +- ...Combine_SparkStructuredStreaming_Batch.yml | 2 +- ...beam_LoadTests_Java_GBK_Dataflow_Batch.yml | 2 +- ..._LoadTests_Java_GBK_Dataflow_Streaming.yml | 2 +- ...m_LoadTests_Java_GBK_Dataflow_V2_Batch.yml | 2 +- ...ests_Java_GBK_Dataflow_V2_Batch_Java17.yml | 2 +- ...adTests_Java_GBK_Dataflow_V2_Streaming.yml | 2 +- ..._Java_GBK_Dataflow_V2_Streaming_Java17.yml | 2 +- .../beam_LoadTests_Java_GBK_Smoke.yml | 2 +- ...ava_GBK_SparkStructuredStreaming_Batch.yml | 2 +- ...am_LoadTests_Java_ParDo_Dataflow_Batch.yml | 2 +- ...oadTests_Java_ParDo_Dataflow_Streaming.yml | 2 +- ...a_ParDo_Dataflow_V2_Batch_JavaVersions.yml | 2 +- ...rDo_Dataflow_V2_Streaming_JavaVersions.yml | 2 +- ...a_ParDo_SparkStructuredStreaming_Batch.yml | 2 +- .../beam_LoadTests_Java_PubsubIO.yml | 2 +- ..._LoadTests_Python_CoGBK_Dataflow_Batch.yml | 2 +- ...dTests_Python_CoGBK_Dataflow_Streaming.yml | 2 +- ...eam_LoadTests_Python_CoGBK_Flink_Batch.yml | 2 +- ...oadTests_Python_Combine_Dataflow_Batch.yml | 2 +- ...ests_Python_Combine_Dataflow_Streaming.yml | 2 +- ...m_LoadTests_Python_Combine_Flink_Batch.yml | 2 +- ...adTests_Python_Combine_Flink_Streaming.yml | 2 +- ...ests_Python_FnApiRunner_Microbenchmark.yml | 2 +- ...am_LoadTests_Python_GBK_Dataflow_Batch.yml | 2 +- ...oadTests_Python_GBK_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Python_GBK_Flink_Batch.yml | 2 +- ...ts_Python_GBK_reiterate_Dataflow_Batch.yml | 2 +- ...ython_GBK_reiterate_Dataflow_Streaming.yml | 2 +- ..._LoadTests_Python_ParDo_Dataflow_Batch.yml | 2 +- ...dTests_Python_ParDo_Dataflow_Streaming.yml | 2 +- ...eam_LoadTests_Python_ParDo_Flink_Batch.yml | 2 +- ...LoadTests_Python_ParDo_Flink_Streaming.yml | 2 +- ...dTests_Python_SideInput_Dataflow_Batch.yml | 2 +- .../workflows/beam_LoadTests_Python_Smoke.yml | 2 +- .../beam_MetricsCredentialsRotation.yml | 2 +- .github/workflows/beam_Metrics_Report.yml | 4 +-- .../beam_PerformanceTests_AvroIOIT.yml | 2 +- .../beam_PerformanceTests_AvroIOIT_HDFS.yml | 2 +- ...rmanceTests_BigQueryIO_Batch_Java_Avro.yml | 4 +-- ...rmanceTests_BigQueryIO_Batch_Java_Json.yml | 4 +-- ...ormanceTests_BigQueryIO_Streaming_Java.yml | 4 +-- ...erformanceTests_BiqQueryIO_Read_Python.yml | 2 +- ...nceTests_BiqQueryIO_Write_Python_Batch.yml | 2 +- .../workflows/beam_PerformanceTests_Cdap.yml | 2 +- ...m_PerformanceTests_Compressed_TextIOIT.yml | 2 +- ...formanceTests_Compressed_TextIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_HadoopFormat.yml | 2 +- .../workflows/beam_PerformanceTests_JDBC.yml | 2 +- .../beam_PerformanceTests_Kafka_IO.yml | 2 +- ...am_PerformanceTests_ManyFiles_TextIOIT.yml | 2 +- ...rformanceTests_ManyFiles_TextIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_MongoDBIO_IT.yml | 2 +- .../beam_PerformanceTests_ParquetIOIT.yml | 2 +- ...beam_PerformanceTests_ParquetIOIT_HDFS.yml | 2 +- ...manceTests_PubsubIOIT_Python_Streaming.yml | 2 +- ...formanceTests_SQLBigQueryIO_Batch_Java.yml | 4 +-- .../beam_PerformanceTests_SingleStoreIO.yml | 2 +- ...ormanceTests_SpannerIO_Read_2GB_Python.yml | 2 +- ...Tests_SpannerIO_Write_2GB_Python_Batch.yml | 2 +- ...beam_PerformanceTests_SparkReceiver_IO.yml | 2 +- .../beam_PerformanceTests_TFRecordIOIT.yml | 2 +- ...eam_PerformanceTests_TFRecordIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_TextIOIT.yml | 2 +- .../beam_PerformanceTests_TextIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_TextIOIT_Python.yml | 2 +- ...rmanceTests_WordCountIT_PythonVersions.yml | 4 +-- .../beam_PerformanceTests_XmlIOIT.yml | 2 +- .../beam_PerformanceTests_XmlIOIT_HDFS.yml | 2 +- ..._PerformanceTests_xlang_KafkaIO_Python.yml | 2 +- .../workflows/beam_Playground_CI_Nightly.yml | 2 +- .../workflows/beam_Playground_Precommit.yml | 2 +- .github/workflows/beam_PostCommit_Go.yml | 2 +- .../beam_PostCommit_Go_Dataflow_ARM.yml | 2 +- .../workflows/beam_PostCommit_Go_VR_Flink.yml | 2 +- .../workflows/beam_PostCommit_Go_VR_Spark.yml | 2 +- .github/workflows/beam_PostCommit_Java.yml | 4 +-- .../beam_PostCommit_Java_Avro_Versions.yml | 4 +-- ...m_PostCommit_Java_BigQueryEarlyRollout.yml | 4 +-- .../beam_PostCommit_Java_DataflowV1.yml | 4 +-- .../beam_PostCommit_Java_DataflowV2.yml | 4 +-- ...beam_PostCommit_Java_Examples_Dataflow.yml | 4 +-- ..._PostCommit_Java_Examples_Dataflow_ARM.yml | 4 +-- ...PostCommit_Java_Examples_Dataflow_Java.yml | 4 +-- ...m_PostCommit_Java_Examples_Dataflow_V2.yml | 4 +-- ...tCommit_Java_Examples_Dataflow_V2_Java.yml | 4 +-- .../beam_PostCommit_Java_Examples_Direct.yml | 4 +-- .../beam_PostCommit_Java_Examples_Flink.yml | 4 +-- .../beam_PostCommit_Java_Examples_Spark.yml | 4 +-- .../beam_PostCommit_Java_Hadoop_Versions.yml | 4 +-- ...m_PostCommit_Java_IO_Performance_Tests.yml | 6 ++--- .../beam_PostCommit_Java_InfluxDbIO_IT.yml | 2 +- .../beam_PostCommit_Java_Jpms_Dataflow.yml | 4 +-- ...PostCommit_Java_Jpms_Dataflow_Versions.yml | 4 +-- .../beam_PostCommit_Java_Jpms_Direct.yml | 4 +-- ...m_PostCommit_Java_Jpms_Direct_Versions.yml | 4 +-- ...beam_PostCommit_Java_Jpms_Flink_Java11.yml | 4 +-- ...beam_PostCommit_Java_Jpms_Spark_Java11.yml | 4 +-- .../beam_PostCommit_Java_Nexmark_Dataflow.yml | 2 +- ...am_PostCommit_Java_Nexmark_Dataflow_V2.yml | 2 +- ...stCommit_Java_Nexmark_Dataflow_V2_Java.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Direct.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Flink.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Spark.yml | 2 +- .../beam_PostCommit_Java_PVR_Flink_Batch.yml | 4 +-- ...am_PostCommit_Java_PVR_Flink_Streaming.yml | 4 +-- ...m_PostCommit_Java_PVR_Spark3_Streaming.yml | 4 +-- .../beam_PostCommit_Java_PVR_Spark_Batch.yml | 6 ++--- .../beam_PostCommit_Java_SingleStoreIO_IT.yml | 2 +- .../beam_PostCommit_Java_Tpcds_Dataflow.yml | 2 +- .../beam_PostCommit_Java_Tpcds_Flink.yml | 2 +- .../beam_PostCommit_Java_Tpcds_Spark.yml | 2 +- ...stCommit_Java_ValidatesRunner_Dataflow.yml | 4 +-- ..._ValidatesRunner_Dataflow_JavaVersions.yml | 4 +-- ...ava_ValidatesRunner_Dataflow_Streaming.yml | 4 +-- ...idatesRunner_Dataflow_Streaming_Engine.yml | 4 +-- ...unner_Dataflow_Streaming_TagEncodingV2.yml | 4 +-- ...ommit_Java_ValidatesRunner_Dataflow_V2.yml | 4 +-- ..._ValidatesRunner_Dataflow_V2_Streaming.yml | 4 +-- ...PostCommit_Java_ValidatesRunner_Direct.yml | 4 +-- ...va_ValidatesRunner_Direct_JavaVersions.yml | 4 +-- ..._PostCommit_Java_ValidatesRunner_Flink.yml | 4 +-- ..._PostCommit_Java_ValidatesRunner_Spark.yml | 4 +-- ...lidatesRunner_SparkStructuredStreaming.yml | 4 +-- ...stCommit_Java_ValidatesRunner_Twister2.yml | 4 +-- ...am_PostCommit_Java_ValidatesRunner_ULR.yml | 4 +-- .github/workflows/beam_PostCommit_Javadoc.yml | 4 +-- .../beam_PostCommit_PortableJar_Flink.yml | 4 +-- .../beam_PostCommit_PortableJar_Spark.yml | 4 +-- .github/workflows/beam_PostCommit_Python.yml | 4 +-- .../workflows/beam_PostCommit_Python_Arm.yml | 4 +-- .../beam_PostCommit_Python_Dependency.yml | 4 +-- ...am_PostCommit_Python_Examples_Dataflow.yml | 4 +-- ...beam_PostCommit_Python_Examples_Direct.yml | 4 +-- .../beam_PostCommit_Python_Examples_Flink.yml | 4 +-- .../beam_PostCommit_Python_Examples_Spark.yml | 4 +-- .../beam_PostCommit_Python_MongoDBIO_IT.yml | 4 +-- .../beam_PostCommit_Python_Nexmark_Direct.yml | 2 +- .../beam_PostCommit_Python_Portable_Flink.yml | 4 +-- ...mit_Python_ValidatesContainer_Dataflow.yml | 4 +-- ...on_ValidatesContainer_Dataflow_With_RC.yml | 4 +-- ...Commit_Python_ValidatesRunner_Dataflow.yml | 4 +-- ...ostCommit_Python_ValidatesRunner_Flink.yml | 4 +-- ...ostCommit_Python_ValidatesRunner_Spark.yml | 4 +-- ...m_PostCommit_Python_Xlang_Gcp_Dataflow.yml | 4 +-- ...eam_PostCommit_Python_Xlang_Gcp_Direct.yml | 4 +-- ...am_PostCommit_Python_Xlang_IO_Dataflow.yml | 4 +-- ...beam_PostCommit_Python_Xlang_IO_Direct.yml | 4 +-- .github/workflows/beam_PostCommit_SQL.yml | 4 +-- ...eam_PostCommit_TransformService_Direct.yml | 4 +-- .../beam_PostCommit_Website_Test.yml | 2 +- .../workflows/beam_PostCommit_XVR_Direct.yml | 4 +-- .../workflows/beam_PostCommit_XVR_Flink.yml | 4 +-- ...am_PostCommit_XVR_GoUsingJava_Dataflow.yml | 4 +-- ...ostCommit_XVR_JavaUsingPython_Dataflow.yml | 4 +-- ...Commit_XVR_PythonUsingJavaSQL_Dataflow.yml | 4 +-- ...ostCommit_XVR_PythonUsingJava_Dataflow.yml | 4 +-- .../workflows/beam_PostCommit_XVR_Spark3.yml | 4 +-- .../beam_PostCommit_Yaml_Xlang_Direct.yml | 4 +-- .../beam_PostRelease_NightlySnapshot.yml | 2 +- .../beam_PreCommit_CommunityMetrics.yml | 2 +- .../beam_PreCommit_Flink_Container.yml | 2 +- .github/workflows/beam_PreCommit_GHA.yml | 2 +- .github/workflows/beam_PreCommit_Go.yml | 2 +- .../workflows/beam_PreCommit_GoPortable.yml | 2 +- .github/workflows/beam_PreCommit_GoPrism.yml | 2 +- .../workflows/beam_PreCommit_ItFramework.yml | 4 +-- .github/workflows/beam_PreCommit_Java.yml | 8 +++--- ...it_Java_Amazon-Web-Services2_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Amqp_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Azure_IO_Direct.yml | 6 ++--- ...eam_PreCommit_Java_Cassandra_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Cdap_IO_Direct.yml | 6 ++--- ...am_PreCommit_Java_Clickhouse_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Csv_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Datadog_IO_Direct.yml | 6 ++--- ...beam_PreCommit_Java_Debezium_IO_Direct.yml | 6 ++--- ...PreCommit_Java_ElasticSearch_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Examples_Dataflow.yml | 4 +-- ...reCommit_Java_Examples_Dataflow_Java21.yml | 6 ++--- ...t_Java_File-schema-transform_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Flink_Versions.yml | 4 +-- .../beam_PreCommit_Java_GCP_IO_Direct.yml | 8 +++--- ...am_PreCommit_Java_Google-ads_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_HBase_IO_Direct.yml | 6 ++--- ...beam_PreCommit_Java_HCatalog_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Hadoop_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_IOs_Direct.yml | 6 ++--- ...beam_PreCommit_Java_InfluxDb_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_JDBC_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Jms_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Kafka_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Kudu_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_MongoDb_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Mqtt_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Neo4j_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_PVR_Flink_Batch.yml | 6 ++--- .../beam_PreCommit_Java_PVR_Flink_Docker.yml | 4 +-- ...beam_PreCommit_Java_PVR_Prism_Loopback.yml | 6 ++--- .../beam_PreCommit_Java_Parquet_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Pulsar_IO_Direct.yml | 6 ++--- ...beam_PreCommit_Java_RabbitMq_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Redis_IO_Direct.yml | 6 ++--- ...eCommit_Java_RequestResponse_IO_Direct.yml | 6 ++--- ...m_PreCommit_Java_SingleStore_IO_Direct.yml | 6 ++--- ...eam_PreCommit_Java_Snowflake_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Solace_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Solr_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Spark3_Versions.yml | 4 +-- .../beam_PreCommit_Java_Splunk_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Thrift_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Java_Tika_IO_Direct.yml | 6 ++--- .../beam_PreCommit_Kotlin_Examples.yml | 2 +- .../beam_PreCommit_Portable_Python.yml | 2 +- .../workflows/beam_PreCommit_Prism_Python.yml | 2 +- .github/workflows/beam_PreCommit_Python.yml | 4 +-- .../workflows/beam_PreCommit_PythonDocker.yml | 2 +- .../workflows/beam_PreCommit_PythonDocs.yml | 2 +- .../beam_PreCommit_PythonFormatter.yml | 2 +- .../workflows/beam_PreCommit_PythonLint.yml | 2 +- .../beam_PreCommit_Python_Coverage.yml | 4 +-- .../beam_PreCommit_Python_Dataframes.yml | 4 +-- .../workflows/beam_PreCommit_Python_Dill.yml | 4 +-- .../beam_PreCommit_Python_Examples.yml | 4 +-- .../beam_PreCommit_Python_Integration.yml | 4 +-- .../workflows/beam_PreCommit_Python_ML.yml | 4 +-- .../beam_PreCommit_Python_PVR_Flink.yml | 4 +-- .../beam_PreCommit_Python_Runners.yml | 4 +-- .../beam_PreCommit_Python_Transforms.yml | 4 +-- .github/workflows/beam_PreCommit_RAT.yml | 2 +- .github/workflows/beam_PreCommit_SQL.yml | 8 +++--- .../workflows/beam_PreCommit_SQL_Java17.yml | 6 ++--- .github/workflows/beam_PreCommit_Spotless.yml | 4 +-- .../workflows/beam_PreCommit_Typescript.yml | 2 +- .github/workflows/beam_PreCommit_Website.yml | 2 +- .../beam_PreCommit_Website_Stage_GCS.yml | 2 +- .../workflows/beam_PreCommit_Whitespace.yml | 2 +- ...m_PreCommit_Xlang_Generated_Transforms.yml | 2 +- .../beam_PreCommit_Yaml_Xlang_Direct.yml | 4 +-- .../beam_Prober_CommunityMetrics.yml | 2 +- .../workflows/beam_Publish_BeamMetrics.yml | 2 +- .../beam_Publish_Beam_SDK_Snapshots.yml | 2 +- .../beam_Publish_Docker_Snapshots.yml | 2 +- .github/workflows/beam_Publish_Website.yml | 4 +-- .../beam_Python_CostBenchmarks_Dataflow.yml | 2 +- ...Python_ValidatesContainer_Dataflow_ARM.yml | 4 +-- .../beam_Release_NightlySnapshot.yml | 2 +- .../beam_Release_Python_NightlySnapshot.yml | 2 +- .../beam_StressTests_Java_BigQueryIO.yml | 2 +- .../beam_StressTests_Java_BigTableIO.yml | 2 +- .../beam_StressTests_Java_KafkaIO.yml | 2 +- .../beam_StressTests_Java_PubSubIO.yml | 2 +- .../beam_StressTests_Java_SpannerIO.yml | 2 +- .github/workflows/build_release_candidate.yml | 14 +++++----- .github/workflows/build_runner_image.yml | 2 +- .github/workflows/build_wheels.yml | 26 +++++++++---------- .github/workflows/cancel.yml | 2 +- .../code_completion_plugin_tests.yml | 6 ++--- .github/workflows/cut_release_branch.yml | 4 +-- .github/workflows/dask_runner_tests.yml | 8 +++--- .github/workflows/finalize_release.yml | 6 ++--- .github/workflows/flaky_test_detection.yml | 2 +- .../workflows/git_tag_released_version.yml | 2 +- .github/workflows/go_tests.yml | 2 +- .github/workflows/issue-tagger.yml | 2 +- .github/workflows/java_tests.yml | 12 ++++----- .github/workflows/local_env_tests.yml | 4 +-- .../workflows/playground_frontend_test.yml | 2 +- .github/workflows/pr-bot-new-prs.yml | 2 +- .github/workflows/pr-bot-pr-updates.yml | 2 +- .../pr-bot-prs-needing-attention.yml | 2 +- .../publish_github_release_notes.yml | 4 +-- .github/workflows/python_dependency_tests.yml | 2 +- .github/workflows/python_tests.yml | 12 ++++----- .github/workflows/refresh_looker_metrics.yml | 2 +- .github/workflows/reportGenerator.yml | 2 +- .../republish_released_docker_containers.yml | 2 +- .github/workflows/run_perf_alert_tool.yml | 2 +- .../run_rc_validation_go_wordcount.yml | 2 +- .../run_rc_validation_java_mobile_gaming.yml | 4 +-- .../run_rc_validation_java_quickstart.yml | 2 +- ...run_rc_validation_python_mobile_gaming.yml | 2 +- .../run_rc_validation_python_yaml.yml | 2 +- .github/workflows/tour_of_beam_backend.yml | 2 +- .../tour_of_beam_backend_integration.yml | 2 +- .../workflows/tour_of_beam_frontend_test.yml | 2 +- .github/workflows/typescript_tests.yml | 8 +++--- .../workflows/update_python_dependencies.yml | 4 +-- 323 files changed, 555 insertions(+), 555 deletions(-) diff --git a/.github/workflows/IO_Iceberg_Integration_Tests.yml b/.github/workflows/IO_Iceberg_Integration_Tests.yml index 5de07f97eb0b..6341c51124c5 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests"] job_phrase: ["Run IcebergIO Integration Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml index 8f9a7770bfdc..38a3a2514855 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests_Dataflow"] job_phrase: ["Run IcebergIO Integration Tests on Dataflow"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml index 138d9b499310..ba72a85204a1 100644 --- a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Managed_Integration_Tests_Dataflow"] job_phrase: ["Run IcebergIO Managed Integration Tests on Dataflow"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Performance_Tests.yml b/.github/workflows/IO_Iceberg_Performance_Tests.yml index 1fa9ab39eac8..e3ec8b569012 100644 --- a/.github/workflows/IO_Iceberg_Performance_Tests.yml +++ b/.github/workflows/IO_Iceberg_Performance_Tests.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Performance_Tests"] job_phrase: ["Run IcebergIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Unit_Tests.yml b/.github/workflows/IO_Iceberg_Unit_Tests.yml index 21c993d7406f..dd8069c74af0 100644 --- a/.github/workflows/IO_Iceberg_Unit_Tests.yml +++ b/.github/workflows/IO_Iceberg_Unit_Tests.yml @@ -82,7 +82,7 @@ jobs: github.event.comment.body == 'Run IcebergIO Unit Tests' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableCheckStyle=true \ --info - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/assign_milestone.yml b/.github/workflows/assign_milestone.yml index 1f4ce3073ec2..300b990bf99c 100644 --- a/.github/workflows/assign_milestone.yml +++ b/.github/workflows/assign_milestone.yml @@ -31,7 +31,7 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 2 diff --git a/.github/workflows/beam_CancelStaleDataflowJobs.yml b/.github/workflows/beam_CancelStaleDataflowJobs.yml index 59f55af17db5..1328cf080c8c 100644 --- a/.github/workflows/beam_CancelStaleDataflowJobs.yml +++ b/.github/workflows/beam_CancelStaleDataflowJobs.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Cancel Stale Dataflow Jobs' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CleanUpDataprocResources.yml b/.github/workflows/beam_CleanUpDataprocResources.yml index c508dcdd8517..bf1314b29519 100644 --- a/.github/workflows/beam_CleanUpDataprocResources.yml +++ b/.github/workflows/beam_CleanUpDataprocResources.yml @@ -55,7 +55,7 @@ jobs: timeout-minutes: 100 name: "beam_CleanUpDataprocResources" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Delete leaked resources for all the jobs that generates flink clusters run: | cd ${{ github.workspace }}/.test-infra/dataproc; ./cleanup.sh -xe \ No newline at end of file diff --git a/.github/workflows/beam_CleanUpGCPResources.yml b/.github/workflows/beam_CleanUpGCPResources.yml index e3527a970839..d148b1e00b8f 100644 --- a/.github/workflows/beam_CleanUpGCPResources.yml +++ b/.github/workflows/beam_CleanUpGCPResources.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean GCP Resources' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml index c0723ed6dfec..4378e98fe389 100644 --- a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml +++ b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean Prebuilt Images' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml index 700470ab9793..69ec18d225fc 100644 --- a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_CloudML_Benchmarks_Dataflow"] job_phrase: ["Run TFT Criteo Benchmarks"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_IODatastoresCredentialsRotation.yml b/.github/workflows/beam_IODatastoresCredentialsRotation.yml index 4ba622055c9a..33327970a764 100644 --- a/.github/workflows/beam_IODatastoresCredentialsRotation.yml +++ b/.github/workflows/beam_IODatastoresCredentialsRotation.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_IODatastoresCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml index bc60b8bde351..61de2b54e730 100644 --- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Inference_Python_Benchmarks_Dataflow"] job_phrase: ["Run Inference Benchmarks"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml index baadd47751bf..75e5c00cb767 100644 --- a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml +++ b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml @@ -42,7 +42,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v4 diff --git a/.github/workflows/beam_Infrastructure_SecurityLogging.yml b/.github/workflows/beam_Infrastructure_SecurityLogging.yml index c59eb7173eac..ea0a33a6d108 100644 --- a/.github/workflows/beam_Infrastructure_SecurityLogging.yml +++ b/.github/workflows/beam_Infrastructure_SecurityLogging.yml @@ -44,7 +44,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v4 diff --git a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml index fc3293a49a51..b133a3543dfb 100644 --- a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml +++ b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml @@ -48,7 +48,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db diff --git a/.github/workflows/beam_Infrastructure_UsersPermissions.yml b/.github/workflows/beam_Infrastructure_UsersPermissions.yml index 8d25db729064..52678f3bd4e5 100644 --- a/.github/workflows/beam_Infrastructure_UsersPermissions.yml +++ b/.github/workflows/beam_Infrastructure_UsersPermissions.yml @@ -45,7 +45,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merged == true && github.base_ref || github.event.pull_request.head.sha }} - name: Setup gcloud diff --git a/.github/workflows/beam_Java_JMH.yml b/.github/workflows/beam_Java_JMH.yml index e879392d211b..68fcf0e91789 100644 --- a/.github/workflows/beam_Java_JMH.yml +++ b/.github/workflows/beam_Java_JMH.yml @@ -62,7 +62,7 @@ jobs: timeout-minutes: 900 name: "beam_Java_JMH" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action - name: run the Java JMH micro-benchmark harness suite diff --git a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml index 5a749ba32d83..56b36dfdcb64 100644 --- a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml +++ b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Java_LoadTests_Combine_Smoke"] job_phrase: ["Run Java Load Tests Combine Smoke"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml index b9886a5d08ac..c682a0d359b9 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Go CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml index 61831c4c140f..eb5d44521229 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_CoGBK_Flink_Batch"] job_phrase: ["Run Load Tests Go CoGBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml index 211d045e7163..64d970024b71 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Go Combine Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml index 2fb2eb65ae10..4978cd778275 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_Combine_Flink_Batch"] job_phrase: ["Run Load Tests Go Combine Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml index b13248da10f0..aaddaaee7e0c 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Go GBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml index 17e8f050581e..74181f1708da 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_GBK_Flink_Batch"] job_phrase: ["Run Load Tests Go GBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml index fbef4c9a4f50..96b2af740c38 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Go ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml index 065f0258a849..1055b18b119a 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_ParDo_Flink_Batch"] job_phrase: ["Run Load Tests Go ParDo Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml index b910d0374b98..44a509615620 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_SideInput_Dataflow_Batch"] job_phrase: ["Run Load Tests Go SideInput Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml index 31e4cf668e8c..1c63c632ee19 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_SideInput_Flink_Batch"] job_phrase: ["Run Load Tests Go SideInput Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml index d1f5fdcbcea7..461920600b98 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Java CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml index 77e5e981f346..76cb31ae5ada 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java CoGBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,7 +115,7 @@ jobs: -Prunner=:runners:google-cloud-dataflow-java \ '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_Streaming_test_arguments_4 }}' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml index e935d3e590ec..9e23fb7b42e7 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml index 2cb60eb34289..daa1559cfb9d 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml index 8273aa2bb8d4..7501c70b7ab6 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java CoGBK SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml index 0c2ab42471bd..0de3f585e703 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Java Combine Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml index 515151328a0e..3651895a76d6 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java Combine Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml index b3a4128d3974..4840de2bb84c 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java Combine SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml index 8108d3a6d5b5..bd662ac7f42e 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Java GBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml index e99c5e25137d..433d12a7a35a 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java GBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml index 2ca560ab9951..84b5e18f06e0 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Batch"] job_phrase: ["Run Load Tests GBK Dataflow V2 Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml index 67542fdf3f96..1899a5602d58 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17"] job_phrase: ["Run Load Tests Java 17 GBK Dataflow V2 Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml index 06a8d326a3ac..c44747a3db7b 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Streaming"] job_phrase: ["Run Load Tests GBK Dataflow V2 Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml index 1180b1889e44..5789be196d27 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17"] job_phrase: ["Run Load Tests Java 17 GBK Dataflow V2 Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml index f13ee1d2c6c9..9f3ab971c88e 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Smoke"] job_phrase: ["Run Java Load Tests GBK Smoke"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml index 01b81108f1eb..9c1eb98b366f 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java GBK SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml index f28a5a63b68a..47046f000a0f 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Java ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml index d38728ef4f86..37bcb095c126 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java ParDo Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml index 7ecc3af73b16..813efedd7984 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml index e10990b553d0..e2ec6e8cbb4a 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml index b80adb5750b6..d2a5a0748a00 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java ParDo SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml index 0f8cca3fc095..6267bf959fde 100644 --- a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml +++ b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_PubsubIO"] job_phrase: ["Run Load Tests Java PubsubIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml index fe7d6dd3f01c..16b53af2f698 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Python CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml index 66e4a9e3368d..85364a2964da 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python CoGBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml index ea9fab2b6d41..429eed69f182 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Flink_Batch"] job_phrase: ["Run Load Tests Python CoGBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml index 32049892fe17..60437c8439ed 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Python Combine Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml index 510b000312ba..e275b14b9911 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python Combine Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml index c8e05c2e1738..17ac98ffd804 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Flink_Batch"] job_phrase: ["Run Load Tests Python Combine Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml index 61b4e239b125..0ea3876371b2 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Flink_Streaming"] job_phrase: ["Run Load Tests Python Combine Flink Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml index ea007e8ddb1e..ec18d36cf583 100644 --- a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml +++ b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_FnApiRunner_Microbenchmark"] job_phrase: ["Run Python Load Tests FnApiRunner Microbenchmark"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml index 8d3fcde2c2be..6597e32ff808 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Python GBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml index 87abf4bf2f11..d71c96c8b8a8 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python GBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml index 794ea092c766..7dfc9c2a5403 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Flink_Batch"] job_phrase: ["Run Load Tests Python GBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml index 674286178b3a..235c9bd48b60 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch"] job_phrase: ["Run Load Tests Python GBK reiterate Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml index 81cf0e0d7f9c..38a11a0ec329 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python GBK reiterate Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml index f5e47b1fbe54..508d1acd8786 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Python ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml index 2512ee32fdb5..8efde4fa36d6 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Dataflow_Streaming"] job_phrase: ["Run Python Load Tests ParDo Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml index 88ff32847b6d..06c3f8479f60 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Flink_Batch"] job_phrase: ["Run Load Tests Python ParDo Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml index bf034f7c2398..b63dc9596e70 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Flink_Streaming"] job_phrase: ["Run Load Tests Python ParDo Flink Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml index 95b38bce1502..2dc81f470e67 100644 --- a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_SideInput_Dataflow_Batch"] job_phrase: ["Run Load Tests Python SideInput Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Smoke.yml b/.github/workflows/beam_LoadTests_Python_Smoke.yml index 511e7ac3227a..d8f22e4b7b5d 100644 --- a/.github/workflows/beam_LoadTests_Python_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Python_Smoke.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_LoadTests_Python_Smoke"] job_phrase: ["Run Python Load Tests Smoke"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_MetricsCredentialsRotation.yml b/.github/workflows/beam_MetricsCredentialsRotation.yml index d5eceea47a3b..3a8c8d424709 100644 --- a/.github/workflows/beam_MetricsCredentialsRotation.yml +++ b/.github/workflows/beam_MetricsCredentialsRotation.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_MetricsCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Metrics_Report.yml b/.github/workflows/beam_Metrics_Report.yml index d4b9c914a0d5..db07b31da7e3 100644 --- a/.github/workflows/beam_Metrics_Report.yml +++ b/.github/workflows/beam_Metrics_Report.yml @@ -61,7 +61,7 @@ jobs: ) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -75,7 +75,7 @@ jobs: INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - name: Archive Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Metrics Report path: "${{ github.workspace }}/.test-infra/jenkins/metrics_report/beam-metrics_report.html" diff --git a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml index 00417593e194..6ad99f853555 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT"] job_phrase: ["Run Java AvroIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml index 7988d99569e4..f44129034a89 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT_HDFS"] job_phrase: ["Run Java AvroIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml index 26ee6315b25d..55f341472b0e 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Batch_Java_Avro"] job_phrase: ["Run BigQueryIO Batch Performance Test Java Avro"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -DintegrationTestRunner=dataflow \ -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_BigQueryIO_Batch_Java_Avro_test_arguments_1 }}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml index a9cc6387e597..b7b933e5bb6f 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Batch_Java_Json"] job_phrase: ["Run BigQueryIO Batch Performance Test Java Json"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -DintegrationTestRunner=dataflow \ -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_BigQueryIO_Batch_Java_Json_test_arguments_1 }}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml index 1539c641db9d..5c414c4a94fd 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Streaming_Java"] job_phrase: ["Run BigQueryIO Streaming Performance Test Java"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -DintegrationTestRunner=dataflow \ -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_BigQueryIO_Streaming_Java_test_arguments_1 }}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml index 5dcb712abf11..58de7f6dfeac 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BiqQueryIO_Read_Python"] job_phrase: ["Run BigQueryIO Read Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml index 391c24ee8668..d7cb776ae265 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BiqQueryIO_Write_Python_Batch"] job_phrase: ["Run BigQueryIO Write Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Cdap.yml b/.github/workflows/beam_PerformanceTests_Cdap.yml index f5d3758e4997..94a43679470c 100644 --- a/.github/workflows/beam_PerformanceTests_Cdap.yml +++ b/.github/workflows/beam_PerformanceTests_Cdap.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Cdap"] job_phrase: ["Run Java CdapIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml index 0504ed52a5b5..017cb90d86e1 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT"] job_phrase: ["Run Java CompressedTextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml index 0b10c2e70e68..3803d275b8a3 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT_HDFS"] job_phrase: ["Run Java CompressedTextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml index 9375d3336df8..dc6d5b6914dc 100644 --- a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml +++ b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_HadoopFormat"] job_phrase: ["Run Java HadoopFormatIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_JDBC.yml b/.github/workflows/beam_PerformanceTests_JDBC.yml index eb3d1e9893e4..97283963dee5 100644 --- a/.github/workflows/beam_PerformanceTests_JDBC.yml +++ b/.github/workflows/beam_PerformanceTests_JDBC.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_JDBC"] job_phrase: ["Run Java JdbcIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml index d2bd6fbe4cb1..fa13bceb6d76 100644 --- a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml +++ b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml @@ -64,7 +64,7 @@ jobs: env: KAFKA_SERVICE_PORT: 32400 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml index 4996a1eaa849..feb909c1d338 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT"] job_phrase: ["Run Java ManyFilesTextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml index 5afbb928beeb..3699d1cbace4 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT_HDFS"] job_phrase: ["Run Java ManyFilesTextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml index da4d043e2985..db2d786d09f5 100644 --- a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_MongoDBIO_IT"] job_phrase: ["Run Java MongoDBIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml index 46c038d09e84..b9e7d9caa7e5 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT"] job_phrase: ["Run Java ParquetIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml index 36346d389d08..af5b6680d6ea 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT_HDFS"] job_phrase: ["Run Java ParquetIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml index 9a23cd1f7283..8ff05fb05f5d 100644 --- a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml +++ b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_PubsubIOIT_Python_Streaming"] job_phrase: ["Run PubsubIO Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml index 493ee1577e36..8fb2c8710e09 100644 --- a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml +++ b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SQLBigQueryIO_Batch_Java"] job_phrase: ["Run SQLBigQueryIO Batch Performance Test Java"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: -DintegrationTestRunner=dataflow \ '-DintegrationTestPipelineOptions=[${{env.beam_PerformanceTests_SQLBigQueryIO_Batch_Java_test_arguments_1}}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml index ae1e13134f4b..8c12533e9cc5 100644 --- a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml +++ b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml @@ -65,7 +65,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java SingleStoreIO Performance Test' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml index 32e3ca8634de..52c296652df3 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SpannerIO_Read_2GB_Python"] job_phrase: ["Run SpannerIO Read 2GB Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml index 932fa8c5fc25..3996acedf195 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch"] job_phrase: ["Run SpannerIO Write 2GB Performance Test Python Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml index 779404783487..f170f3ca4a3a 100644 --- a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml +++ b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SparkReceiver_IO"] job_phrase: ["Run Java SparkReceiverIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml index 3a51dfd208af..0490ca78cbee 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT"] job_phrase: ["Run Java TFRecordIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml index 9fc0de240f88..a2da2ca848e3 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT_HDFS"] job_phrase: ["Run Java TFRecordIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_TextIOIT.yml index d832032e086e..538866170fa6 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT"] job_phrase: ["Run Java TextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml index 9b1c934e46fc..e4e4a607580a 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_HDFS"] job_phrase: ["Run Java TextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml index 56c062e20637..e396812b187c 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_Python"] job_phrase: ["Run Python TextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml index 82d2e060195c..ccbe286fc2f1 100644 --- a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml +++ b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: [WordCountIT Performance Test] python_version: ['3.10'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,7 +104,7 @@ jobs: -Ptest=apache_beam/examples/wordcount_it_test.py::WordCountIT::test_wordcount_it \ "-Ptest-pipeline-options=${{ env.beam_PerformanceTests_WordCountIT_PythonVersions_test_arguments_1 }}" - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml index 5ce099f5b004..6c3e3cdbdc99 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT"] job_phrase: ["Run Java XmlIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml index 8a8f944c254a..7a1f86f687fd 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT_HDFS"] job_phrase: ["Run Java XmlIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml index 17e1ac271268..7ed3fce19a0e 100644 --- a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml +++ b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PerfTests_xlang_KafkaIO_Python"] job_phrase: ["Run Python xlang KafkaIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Playground_CI_Nightly.yml b/.github/workflows/beam_Playground_CI_Nightly.yml index 2a0cba7bf2a3..fc79c6fda9bc 100644 --- a/.github/workflows/beam_Playground_CI_Nightly.yml +++ b/.github/workflows/beam_Playground_CI_Nightly.yml @@ -61,7 +61,7 @@ jobs: sdk: ["python", "java", "go"] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index 80cc2015ae9d..379184fbd8a8 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -47,7 +47,7 @@ jobs: PYTHON_VERSION: '3.10' JAVA_VERSION: '11' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go.yml b/.github/workflows/beam_PostCommit_Go.yml index 1f5a1569a20c..5b42821f3cd5 100644 --- a/.github/workflows/beam_PostCommit_Go.yml +++ b/.github/workflows/beam_PostCommit_Go.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go"] job_phrase: ["Run Go PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml index 5ec840e200ba..d71ca934cdff 100644 --- a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml @@ -65,7 +65,7 @@ jobs: job_name: ["beam_PostCommit_Go_Dataflow_ARM"] job_phrase: ["Run Go PostCommit Dataflow ARM"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml index abbd2b83c4ed..6ea7ee837716 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Flink"] job_phrase: ["Run Go Flink ValidatesRunner"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml index 7eb5f66650a2..abbded6c3bcf 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Spark"] job_phrase: ["Run Go Spark ValidatesRunner"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java.yml b/.github/workflows/beam_PostCommit_Java.yml index 876fdf0b505a..98c7a6bfae06 100644 --- a/.github/workflows/beam_PostCommit_Java.yml +++ b/.github/workflows/beam_PostCommit_Java.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :javaPostCommit - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml index 891854d013fc..4ca15d53aa20 100644 --- a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Avro Versions PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :javaAvroVersionsTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml index 8ed0345ec7c5..61481b1cd8ea 100644 --- a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml +++ b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java BigQueryEarlyRollout PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: # body: | # PostCommit Java BigQueryEarlyRollout failed on ${{ env.date }}. This test monitors BigQuery rollouts impacting Beam and should be escalated immediately if a real issue is encountered to pause further rollouts. For further details refer to the following links:\n * Failing job: https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml \n * Job configuration: https://github.com/apache/beam/blob/master/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml index 8aa8851921c8..76f57afa2489 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_Dataflow' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:postCommit - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml index cb1581a404b4..d87685a63c25 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_DataflowV2' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:postCommitRunnerV2 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml index 77561b23a4de..5e6e090a9794 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples on Dataflow' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:examples:javaPostCommit max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml index 29bfe65ff271..ea1302c825d8 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'pull_request_target' || startswith(github.event.comment.body, 'Run Java_Examples_Dataflow_ARM PostCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -107,7 +107,7 @@ jobs: -PdisableCheckStyle=true \ -PskipCheckerFramework \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml index f38bd7ae5c1b..c45725652dc6 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Java examples on Dataflow Java') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:examples:java${{ matrix.java_version }}PostCommit max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml index 03484e90df52..274e579c19ed 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples on Dataflow Runner V2' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,7 +103,7 @@ jobs: -Pjava17Home=$JAVA_HOME_17_X64 \ -PdockerTag=${{ env.DOCKER_TAG }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml index 72d59d89f8f3..70e8a40023c3 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml @@ -69,7 +69,7 @@ jobs: (contains(github.event.comment.body, 'Run Java') && contains(github.event.comment.body, 'Examples on Dataflow Runner V2')) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} \ -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml index 4606803f3aec..ef6f7f3b77a0 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Direct' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: :runners:direct:examplesIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml index d82b89fe44b2..9f60e1529d15 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Flink' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: with: gradle-command: :runners:flink:1.20:examplesIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml index 06840e305fbf..380167cc99a2 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Spark' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: :runners:spark:3:examplesIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml index e0c26b293169..4ae2b3c6dd74 100644 --- a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_Hadoop_Versions' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -77,7 +77,7 @@ jobs: with: gradle-command: :javaHadoopVersionsTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml index 71ac34483c47..8820e32812ea 100644 --- a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml +++ b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml @@ -66,7 +66,7 @@ jobs: job_phrase: ["Run Java PostCommit IO Performance Tests"] test_case: ["GCSPerformanceTest", "BigTablePerformanceTest", "BigQueryStorageApiStreamingPerformanceTest"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: echo "BEAM_VERSION=${BEAM_VERSION}" >> $GITHUB_ENV - name: Checkout release branch if: github.event_name == 'schedule' #This has scheduled runs run against the latest release - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ env.BEAM_VERSION }} repository: apache/beam @@ -105,7 +105,7 @@ jobs: exportDataset: performance_tests exportTable: io_performance_metrics_test - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml index 3e6242b5fb81..2a12e9652c28 100644 --- a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml @@ -65,7 +65,7 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java InfluxDbIO_IT' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml index 2ab3f7dc306e..42d80bbbe6a6 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Dataflow"] job_phrase: ["Run Jpms Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :sdks:java:testing:jpms-tests:dataflowRunnerIntegrationTest arguments: -Dorg.gradle.java.home=$JAVA_HOME_11_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml index 0fd15f1ec004..b5ac893f60b8 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run Jpms Dataflow Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml index 904382039b06..6c1f97f37855 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Direct"] job_phrase: ["Run Jpms Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :sdks:java:testing:jpms-tests:directRunnerIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml index ea58b3591a33..f9bbdb93e0c4 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run Jpms Direct Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml index ed62695a1382..e6dd44033f4a 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Flink_Java11"] job_phrase: ["Run Jpms Flink Java 11 PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :sdks:java:testing:jpms-tests:flinkRunnerIntegrationTest arguments: -Dorg.gradle.java.home=$JAVA_HOME_11_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml index 9d4a76f1fb74..307d55c16d96 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Spark_Java11"] job_phrase: ["Run Jpms Spark Java 11 PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :sdks:java:testing:jpms-tests:sparkRunnerIntegrationTest arguments: -Dorg.gradle.java.home=$JAVA_HOME_11_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml index d0218f6f093e..d648a8c4ba08 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml @@ -93,7 +93,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml index 60d5e4c7c385..4c91daf38bec 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml @@ -93,7 +93,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Runner V2 Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml index 972aab081dba..82f862a837ed 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml @@ -95,7 +95,7 @@ jobs: (contains(github.event.comment.body, 'Run Dataflow Runner V2 Java') && contains(github.event.comment.body, 'Nexmark Tests')) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml index e23337440e42..db8eff3e748c 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml @@ -88,7 +88,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Direct Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml index 1c908fceefdc..9d7ca2043d74 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml @@ -87,7 +87,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Flink Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml index d36fb5bdd26b..f397c7ae18a5 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml @@ -87,7 +87,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml index 35f6baa42bde..476793bfa8b6 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml @@ -79,7 +79,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Flink_Batch PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,7 +103,7 @@ jobs: env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH }} - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml index ffa2dc24bad5..d5f7edeb11d0 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Flink PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: runners:flink:${{ matrix.flink_version }}:job-server:validatesPortableRunnerStreaming - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml index eb3ae9edd3f0..3001ce0debd3 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Spark v3 PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:spark:3:job-server:validatesPortableRunnerStreaming - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml index 81ed0f588825..e2ba7ec824ee 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml @@ -64,7 +64,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: :runners:spark:3:job-server:validatesPortableRunnerBatch \ :runners:spark:3:job-server:validatesPortableRunnerDocker \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -96,7 +96,7 @@ jobs: comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/build/test-results/**/*.xml' - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: SpotBugs Results path: "**/build/reports/spotbugs/*.html" diff --git a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml index ad309a25a42f..d16069b9e915 100644 --- a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml @@ -67,7 +67,7 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java SingleStoreIO_IT' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml index fc8abb083626..25890c3545e3 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml @@ -90,7 +90,7 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Dataflow"] job_phrase: ["Run Dataflow Runner Tpcds Tests"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml index 7cbcc5c314e7..ae3dcb7d6ad2 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml @@ -87,7 +87,7 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Flink"] job_phrase: ["Run Flink Runner Tpcds Tests"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml index 7a34da19b9c7..557db1c89794 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml @@ -86,7 +86,7 @@ jobs: job_phrase: ["Run Spark Runner Tpcds Tests"] runner: [SparkRunner, SparkStructuredStreamingRunner] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml index dddae544717e..ef07fac8f807 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunner max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml index 879996baac4a..e5d4a3d26f83 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Dataflow ValidatesRunner Java') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -89,7 +89,7 @@ jobs: -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml index 34ed632b0ee2..f70cbf0db32f 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Streaming ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerStreaming max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml index e4c138065719..422eb989d09c 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml @@ -64,7 +64,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Streaming Engine ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerStreamingEngine max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml index 3e8f121bcb9e..12340233310a 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow Streaming TagEncodingV2 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerStreamingTagEncodingV2 max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml index b3ef916ba4ed..39cd812d4776 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow V2 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerV2 max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml index 9c565182d83c..1d64520d2cc7 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow V2 ValidatesRunner Streaming' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -89,7 +89,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerV2Streaming max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml index 179c46d4046f..29d42d8e6edc 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Direct ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: - name: run validatesRunner script run: ./gradlew :runners:direct-java:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml index cc7affe0b84d..48fd46342d22 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Direct ValidatesRunner Java') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,7 +88,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} \ -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml index 1cc77a5da43b..febec1afa592 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Flink ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: with: gradle-command: :runners:flink:${{ matrix.flink_version }}:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml index c40527bad150..48f328789042 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:spark:3:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml index 637e96d8e623..5e000bfa1666 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark StructuredStreaming ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:spark:3:validatesStructuredStreamingRunnerBatch - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml index 1fc0b4c375bd..9a10cf8c2f9c 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Twister2 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:twister2:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml index dfd70fb0fc70..74400d0934ad 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run ULR Loopback ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -78,7 +78,7 @@ jobs: - name: run ulrLoopbackValidatesRunner script run: ./gradlew :runners:portability:java:ulrLoopbackValidatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Javadoc.yml b/.github/workflows/beam_PostCommit_Javadoc.yml index 142a2b830da8..538648c46e7d 100644 --- a/.github/workflows/beam_PostCommit_Javadoc.yml +++ b/.github/workflows/beam_PostCommit_Javadoc.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Javadoc PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,7 +87,7 @@ jobs: path-to-root: sdks/java/javadoc/build/docs/javadoc base-url-path: https://beam.apache.org/releases/javadoc/current/ - name: Upload Javadoc Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Javadoc Results path: '**/sdks/java/javadoc/build/docs/javadoc/**' \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml index d4ac112e1abd..9dd6efb968d7 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Flink"] job_phrase: ["Run PortableJar_Flink PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: arguments: | -PpythonVersion=3.10 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml index 4919ba941911..da9826febd53 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Spark"] job_phrase: ["Run PortableJar_Spark PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: arguments: | -PpythonVersion=3.10 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python.yml b/.github/workflows/beam_PostCommit_Python.yml index 1fe437fc0edb..41f72fa50e92 100644 --- a/.github/workflows/beam_PostCommit_Python.yml +++ b/.github/workflows/beam_PostCommit_Python.yml @@ -69,7 +69,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Python PostCommit 3.') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -105,7 +105,7 @@ jobs: env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results (${{ join(matrix.os, ', ') }}) diff --git a/.github/workflows/beam_PostCommit_Python_Arm.yml b/.github/workflows/beam_PostCommit_Python_Arm.yml index 852b21d53bc6..32dd751d79e7 100644 --- a/.github/workflows/beam_PostCommit_Python_Arm.yml +++ b/.github/workflows/beam_PostCommit_Python_Arm.yml @@ -66,7 +66,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -119,7 +119,7 @@ jobs: MULTIARCH_TAG: ${{ steps.set_tag.outputs.TAG }} USER: github-actions - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Dependency.yml b/.github/workflows/beam_PostCommit_Python_Dependency.yml index 0652671d732b..18349ab173ec 100644 --- a/.github/workflows/beam_PostCommit_Python_Dependency.yml +++ b/.github/workflows/beam_PostCommit_Python_Dependency.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Python PostCommit Dependency') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml index 14dbfc5be501..eb80bf1e7e79 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Examples_Dataflow"] job_phrase: ["Run Python Examples_Dataflow"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml index 4645e191efaf..368dba46c1c2 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Direct"] python_version: ['3.10','3.11','3.12', '3.13','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml index 466d275e0aaf..037dbca6a0a8 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Flink"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml index 308f8442203b..643ce2dcde28 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Spark"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml index f44c1c117385..305960e130e2 100644 --- a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_MongoDBIO_IT"] job_phrase: ["Run Python MongoDBIO_IT"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml index f942a30b5261..71adf5c37e91 100644 --- a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml @@ -108,7 +108,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Python Direct Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml index 40c881a3f220..78af1ff48e9a 100644 --- a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml @@ -67,7 +67,7 @@ jobs: # Run modes not covered by PreCommit_Python_PVR_Flink (i.e. other than 'LOOPBACK') environment_type: ['DOCKER'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,7 +88,7 @@ jobs: arguments: | -PpythonVersion=3.10 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results ${{ matrix.environment_type }} diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml index cac0f9c0a50b..2c36e665dc63 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml @@ -67,7 +67,7 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesContainer"] python_version: ['3.10','3.11','3.12','3.13','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,7 +117,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results ${{ matrix.python_version }} diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml index aca5894e123b..fddf74577243 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python RC Dataflow ValidatesContainer"] python_version: ['3.10','3.11','3.12','3.13','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,7 +113,7 @@ jobs: -PtestRCDependencies=true \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml index 3e840f579614..1b3aa2890783 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml index 88037f344a0d..1b769788fd51 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Flink ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -92,7 +92,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml index 7d70e893a281..fc68dc021db5 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Spark ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml index f4be27554dad..01997b22515d 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Gcp_Dataflow"] job_phrase: ["Run Python_Xlang_Gcp_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,7 +85,7 @@ jobs: -PpythonVersion=3.14 \ -PuseWheelDistribution \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml index bfbb8e04889b..e4a71ee8edd3 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Gcp_Direct"] job_phrase: ["Run Python_Xlang_Gcp_Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: with: gradle-command: :sdks:python:test-suites:direct:gcpCrossLanguagePostCommit - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml index 130db04ca203..832db23a73fc 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_IO_Dataflow"] job_phrase: ["Run Python_Xlang_IO_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,7 +85,7 @@ jobs: -PuseWheelDistribution \ -PkafkaBootstrapServer=10.128.0.40:9094,10.128.0.28:9094,10.128.0.165:9094 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml index 7335346a2fb9..bb3198f83d2c 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_IO_Direct"] job_phrase: ["Run Python_Xlang_IO_Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :sdks:python:test-suites:direct:ioCrossLanguagePostCommit arguments: -PuseWheelDistribution - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_SQL.yml b/.github/workflows/beam_PostCommit_SQL.yml index d082981d626a..54fde69e28a0 100644 --- a/.github/workflows/beam_PostCommit_SQL.yml +++ b/.github/workflows/beam_PostCommit_SQL.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run SQL PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :sqlPostCommit - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_TransformService_Direct.yml b/.github/workflows/beam_PostCommit_TransformService_Direct.yml index e6badfb9d31a..a127cb5b6046 100644 --- a/.github/workflows/beam_PostCommit_TransformService_Direct.yml +++ b/.github/workflows/beam_PostCommit_TransformService_Direct.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run TransformService_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,7 +87,7 @@ jobs: -Pjava11Home=$JAVA_HOME_11_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PostCommit_Website_Test.yml b/.github/workflows/beam_PostCommit_Website_Test.yml index b6fe904a6ae7..c58a2727fbe8 100644 --- a/.github/workflows/beam_PostCommit_Website_Test.yml +++ b/.github/workflows/beam_PostCommit_Website_Test.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Website_Test"] job_phrase: ["Run Full Website Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_Direct.yml b/.github/workflows/beam_PostCommit_XVR_Direct.yml index 014a7bf9176c..f2174aa3ec98 100644 --- a/.github/workflows/beam_PostCommit_XVR_Direct.yml +++ b/.github/workflows/beam_PostCommit_XVR_Direct.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,7 +85,7 @@ jobs: -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml b/.github/workflows/beam_PostCommit_XVR_Flink.yml index bb20b06efc41..44cb264ad9dd 100644 --- a/.github/workflows/beam_PostCommit_XVR_Flink.yml +++ b/.github/workflows/beam_PostCommit_XVR_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run XVR_Flink PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml index b4edf563fbbb..3961517c2707 100644 --- a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml @@ -66,7 +66,7 @@ jobs: job_name: ["beam_PostCommit_XVR_GoUsingJava_Dataflow"] job_phrase: ["Run XVR_GoUsingJava_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,7 +88,7 @@ jobs: arguments: | -PuseDockerBuildx - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml index e9b03a5bcaac..c282f6c47069 100644 --- a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_JavaUsingPython_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,7 +84,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml index 1967f8438e19..95496621f9ce 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow"] job_phrase: ["Run XVR_PythonUsingJavaSQL_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml index 5a9e6a5574fb..0cd9790e6793 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_PythonUsingJava_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,7 +84,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_Spark3.yml b/.github/workflows/beam_PostCommit_XVR_Spark3.yml index adfce4346d4f..2c7104df6115 100644 --- a/.github/workflows/beam_PostCommit_XVR_Spark3.yml +++ b/.github/workflows/beam_PostCommit_XVR_Spark3.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_Spark3 PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,7 +85,7 @@ jobs: -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index 1d9eb3eb78a2..ea1c255f7cc9 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -63,7 +63,7 @@ jobs: job_phrase: ["Run Yaml_Xlang_Direct PostCommit"] test_set: ["data", "databases", "messaging"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: with: gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostRelease_NightlySnapshot.yml b/.github/workflows/beam_PostRelease_NightlySnapshot.yml index 11cd459c5a5d..5e811fa47581 100644 --- a/.github/workflows/beam_PostRelease_NightlySnapshot.yml +++ b/.github/workflows/beam_PostRelease_NightlySnapshot.yml @@ -54,7 +54,7 @@ jobs: name: beam_PostRelease_NightlySnapshot runs-on: [self-hosted, ubuntu-24.04, highmem] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_PreCommit_CommunityMetrics.yml b/.github/workflows/beam_PreCommit_CommunityMetrics.yml index 0901351be1c8..6ea7a5019711 100644 --- a/.github/workflows/beam_PreCommit_CommunityMetrics.yml +++ b/.github/workflows/beam_PreCommit_CommunityMetrics.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run CommunityMetrics PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Flink_Container.yml b/.github/workflows/beam_PreCommit_Flink_Container.yml index 0d23cf14a8ea..0febb8981176 100644 --- a/.github/workflows/beam_PreCommit_Flink_Container.yml +++ b/.github/workflows/beam_PreCommit_Flink_Container.yml @@ -98,7 +98,7 @@ jobs: job_name: ["beam_PreCommit_Flink_Container"] job_phrase: ["Run Flink Container PreCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GHA.yml b/.github/workflows/beam_PreCommit_GHA.yml index 89afbca79172..76c5b1cba0fd 100644 --- a/.github/workflows/beam_PreCommit_GHA.yml +++ b/.github/workflows/beam_PreCommit_GHA.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GHA PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Go.yml b/.github/workflows/beam_PreCommit_Go.yml index 18dff488ae3d..c382815bdbd1 100644 --- a/.github/workflows/beam_PreCommit_Go.yml +++ b/.github/workflows/beam_PreCommit_Go.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Go PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GoPortable.yml b/.github/workflows/beam_PreCommit_GoPortable.yml index 8dbff2750603..dad631236112 100644 --- a/.github/workflows/beam_PreCommit_GoPortable.yml +++ b/.github/workflows/beam_PreCommit_GoPortable.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPortable PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GoPrism.yml b/.github/workflows/beam_PreCommit_GoPrism.yml index b833f5cf078e..ce9679142a10 100644 --- a/.github/workflows/beam_PreCommit_GoPrism.yml +++ b/.github/workflows/beam_PreCommit_GoPrism.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPrism PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_ItFramework.yml b/.github/workflows/beam_PreCommit_ItFramework.yml index 57a5b53d64bb..5f30ac844703 100644 --- a/.github/workflows/beam_PreCommit_ItFramework.yml +++ b/.github/workflows/beam_PreCommit_ItFramework.yml @@ -76,7 +76,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run It_Framework PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: - name: run ItFrameworkPrecommit script run: ./gradlew -p it build - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java.yml b/.github/workflows/beam_PreCommit_Java.yml index a11538c38a82..caa737abe624 100644 --- a/.github/workflows/beam_PreCommit_Java.yml +++ b/.github/workflows/beam_PreCommit_Java.yml @@ -170,7 +170,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -191,7 +191,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -205,7 +205,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -217,7 +217,7 @@ jobs: name: Publish SpotBugs path: '**/build/reports/spotbugs/*.html' - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml index 49fda8ae9023..ff27933a1004 100644 --- a/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_Amazon-Web-Services2_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -118,7 +118,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -132,7 +132,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml index d68a58c4e0f0..0a477eaec9b7 100644 --- a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Amqp_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -106,7 +106,7 @@ jobs: comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/build/test-results/**/*.xml' - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml index ae48e88c24dc..384a8fe5f83a 100644 --- a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_Azure_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,7 +111,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -125,7 +125,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml index afb2fb67f3f5..5aeddb5107c6 100644 --- a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Cassandra_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml index 5e7c60821646..2617601216ea 100644 --- a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml @@ -80,7 +80,7 @@ jobs: github.event.comment.body == 'Run Java_Cdap_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -97,7 +97,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -111,7 +111,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml index 54cc2cacf80a..50368fdbd7e9 100644 --- a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Clickhouse_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml index e7ca1a5cde05..1dcaeb168e45 100644 --- a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Csv_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml index d9aad3f96d68..7501b342959f 100644 --- a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Datadog_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml index 05dcefa1df70..dbd3dfef3e42 100644 --- a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Debezium_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -110,7 +110,7 @@ jobs: -PtestJavaVersion=17 \ -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -124,7 +124,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml index f31a0f883f57..5bc2d491de40 100644 --- a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_ElasticSearch_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -106,7 +106,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -120,7 +120,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml index 1dafd6a29878..2ff6f4b08764 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml @@ -87,7 +87,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Examples_Dataflow PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -107,7 +107,7 @@ jobs: -PdisableCheckStyle=true \ -Pdocker-pull-licenses \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml index 0259dc4dfadd..455b12b73b39 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml @@ -86,7 +86,7 @@ jobs: github.event.comment.body == 'Run Java_Examples_Dataflow_Java21 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -122,7 +122,7 @@ jobs: -Pdocker-pull-licenses \ max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -136,7 +136,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: SpotBugs Results path: '**/build/reports/spotbugs/*.html' \ No newline at end of file diff --git a/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml index dd435fcf21eb..03b8beecfdc6 100644 --- a/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_File-schema-transform_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,7 +94,7 @@ jobs: -PdisableCheckStyle=true \ -Dfile.encoding=UTF-8 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -108,7 +108,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml index 809ed57324f1..541dc08146c9 100644 --- a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml @@ -75,7 +75,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java_Flink_Versions PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: arguments: | -PdisableSpotlessCheck=true -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml index 161bc77649f3..2cf942f5d488 100644 --- a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_GCP_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,7 +115,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -129,7 +129,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -145,7 +145,7 @@ jobs: run: | echo "path=sdks/java/io/google-cloud-platform/build/reports/jacoco/test/jacocoTestReport.xml" >> $GITHUB_OUTPUT - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml index debcbba36daf..292d4bafb288 100644 --- a/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Google-ads_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -105,7 +105,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml index 65e36210f7a5..4a7553ddcd2c 100644 --- a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_HBase_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,7 +95,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -109,7 +109,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml index 62e37b4d1718..0a9de29e968b 100644 --- a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_HCatalog_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,7 +95,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -109,7 +109,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml index e0eb08a8b7b0..b8a085f8447f 100644 --- a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml @@ -102,7 +102,7 @@ jobs: github.event.comment.body == 'Run Java_Hadoop_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -133,7 +133,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -147,7 +147,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml index 67eba0170590..349d6d1dd06c 100644 --- a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml @@ -79,7 +79,7 @@ jobs: github.event.comment.body == 'Run Java_IOs_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -97,7 +97,7 @@ jobs: -PdisableCheckStyle=true \ -Dfile.encoding=UTF-8 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -111,7 +111,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml index 7f20a70c0628..dabac47afa4f 100644 --- a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_InfluxDb_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml index c7658f75d6ed..fd595d867a39 100644 --- a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_JDBC_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml index 7d34ae42c140..d032b22d3527 100644 --- a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Jms_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml index 71945c923c73..94ac5a6fec8e 100644 --- a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml @@ -84,7 +84,7 @@ jobs: github.event.comment.body == 'Run Java_Kafka_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,7 +102,7 @@ jobs: -PdisableCheckStyle=true \ max-workers: 4 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -116,7 +116,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml index 98220eabdf54..d60c67ab4e76 100644 --- a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Kudu_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml index a665ecad4b82..91bf02f0c0f3 100644 --- a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_MongoDb_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml index 5ffd65a1c106..a202a12d57a6 100644 --- a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Mqtt_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml index fdec1c5bf53d..c17c69c9a5a6 100644 --- a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_Neo4j_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,7 +102,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -116,7 +116,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml index 98eb7c2befff..9e04f103a779 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml @@ -82,7 +82,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Flink_Batch PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,13 +98,13 @@ jobs: env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH }} - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results path: "**/build/reports/tests/" - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: java-code-coverage-report path: "**/build/test-results/**/*.xml" diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml index f7b0fceae0f3..3ad28549e59e 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml @@ -87,7 +87,7 @@ jobs: github.event.comment.body == 'Run Java_PVR_Flink_Docker PreCommit' timeout-minutes: 240 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,7 +103,7 @@ jobs: env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml index 42ac08bb1848..c66b4a133bcf 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml @@ -88,7 +88,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Prism_Loopback PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,13 +102,13 @@ jobs: with: gradle-command: :runners:prism:java:prismLoopbackValidatesRunnerTests - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results path: "**/build/reports/tests/" - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: java-code-coverage-report path: "**/build/test-results/**/*.xml" diff --git a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml index 26f0f5816ebf..2e6e4c4548ed 100644 --- a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Parquet_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml index f8557ef4c10e..59c5aeb249f8 100644 --- a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Pulsar_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml index 12363d0dc535..296b36fbe228 100644 --- a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_RabbitMq_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml index 50a0df0ef664..de5d80c9cb32 100644 --- a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Redis_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml index faadc9dc91f6..78b290fb45e4 100644 --- a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_RequestResponse_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -105,7 +105,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml index 24cbe7fcb619..b5816df45c2b 100644 --- a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_SingleStore_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,7 +95,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -109,7 +109,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml index 88d531df6730..ce2d64387944 100644 --- a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml @@ -80,7 +80,7 @@ jobs: github.event.comment.body == 'Run Java_Snowflake_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,7 +104,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -118,7 +118,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml index 9bd8ba4dff56..42d8009c6b79 100644 --- a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Solace_IO_Direct PreCommit' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml index 2ca3dd3e9d8b..bcd6ea621282 100644 --- a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Solr_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml b/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml index 9a35688f4807..dd0bb69b32fe 100644 --- a/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml @@ -78,7 +78,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Spark3_Versions PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,7 +94,7 @@ jobs: arguments: | -PdisableSpotlessCheck=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml index 5fd66a536085..797390c81683 100644 --- a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Splunk_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml index 5ec0f6651873..a85e8cdaf60b 100644 --- a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Thrift_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml index 2954bd4fb548..c4cb837fb3a9 100644 --- a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Tika_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml index cd567d84686c..b05fff74e51c 100644 --- a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml +++ b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml @@ -88,7 +88,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Kotlin_Examples PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Portable_Python.yml b/.github/workflows/beam_PreCommit_Portable_Python.yml index fb3cf5446b63..fedb485bac8c 100644 --- a/.github/workflows/beam_PreCommit_Portable_Python.yml +++ b/.github/workflows/beam_PreCommit_Portable_Python.yml @@ -90,7 +90,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Portable_Python PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Prism_Python.yml b/.github/workflows/beam_PreCommit_Prism_Python.yml index 0e894453de97..ca2e6a08a23c 100644 --- a/.github/workflows/beam_PreCommit_Prism_Python.yml +++ b/.github/workflows/beam_PreCommit_Prism_Python.yml @@ -84,7 +84,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Prism_Python PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python.yml b/.github/workflows/beam_PreCommit_Python.yml index 6bb6cfb068a3..4ba7a4758592 100644 --- a/.github/workflows/beam_PreCommit_Python.yml +++ b/.github/workflows/beam_PreCommit_Python.yml @@ -91,7 +91,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -137,7 +137,7 @@ jobs: -Pposargs="--ignore=apache_beam/dataframe/ --ignore=apache_beam/ml/ --ignore=apache_beam/examples/ --ignore=apache_beam/runners/ --ignore=apache_beam/transforms/" \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_PythonDocker.yml b/.github/workflows/beam_PreCommit_PythonDocker.yml index 040b4dd71fc3..9499499affe3 100644 --- a/.github/workflows/beam_PreCommit_PythonDocker.yml +++ b/.github/workflows/beam_PreCommit_PythonDocker.yml @@ -69,7 +69,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonDocs.yml b/.github/workflows/beam_PreCommit_PythonDocs.yml index b1628a90667e..b4fca4070277 100644 --- a/.github/workflows/beam_PreCommit_PythonDocs.yml +++ b/.github/workflows/beam_PreCommit_PythonDocs.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonDocs PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonFormatter.yml b/.github/workflows/beam_PreCommit_PythonFormatter.yml index b9804d7f3114..5d06cdb8d6cc 100644 --- a/.github/workflows/beam_PreCommit_PythonFormatter.yml +++ b/.github/workflows/beam_PreCommit_PythonFormatter.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonFormatter PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonLint.yml b/.github/workflows/beam_PreCommit_PythonLint.yml index 43a0ae4a88e2..2ed76a8098d1 100644 --- a/.github/workflows/beam_PreCommit_PythonLint.yml +++ b/.github/workflows/beam_PreCommit_PythonLint.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonLint PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Coverage.yml b/.github/workflows/beam_PreCommit_Python_Coverage.yml index 794a28b77374..ad27dfc5de01 100644 --- a/.github/workflows/beam_PreCommit_Python_Coverage.yml +++ b/.github/workflows/beam_PreCommit_Python_Coverage.yml @@ -74,7 +74,7 @@ jobs: github.event_name == 'workflow_dispatch' || startswith(github.event.comment.body, 'Run Python_Coverage PreCommit 3.') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,7 +101,7 @@ jobs: flags: python directory: sdks/python - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results (${{ join(matrix.os, ', ') }}) diff --git a/.github/workflows/beam_PreCommit_Python_Dataframes.yml b/.github/workflows/beam_PreCommit_Python_Dataframes.yml index 0442bd2306bc..3266ea1b5a52 100644 --- a/.github/workflows/beam_PreCommit_Python_Dataframes.yml +++ b/.github/workflows/beam_PreCommit_Python_Dataframes.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dataframes PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -Pposargs=apache_beam/dataframe/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Dill.yml b/.github/workflows/beam_PreCommit_Python_Dill.yml index 63ef67c3db34..3ee44408d1a0 100644 --- a/.github/workflows/beam_PreCommit_Python_Dill.yml +++ b/.github/workflows/beam_PreCommit_Python_Dill.yml @@ -81,7 +81,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dill PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,7 +111,7 @@ jobs: }}" \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Examples.yml b/.github/workflows/beam_PreCommit_Python_Examples.yml index b79b7a49e202..0fedaa6437ae 100644 --- a/.github/workflows/beam_PreCommit_Python_Examples.yml +++ b/.github/workflows/beam_PreCommit_Python_Examples.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Examples PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +99,7 @@ jobs: -Pposargs=apache_beam/examples/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Integration.yml b/.github/workflows/beam_PreCommit_Python_Integration.yml index a8cac23844b4..4228c3c05c9f 100644 --- a/.github/workflows/beam_PreCommit_Python_Integration.yml +++ b/.github/workflows/beam_PreCommit_Python_Integration.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Integration PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -105,7 +105,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_ML.yml b/.github/workflows/beam_PreCommit_Python_ML.yml index 34aacce81b73..94a953f82d40 100644 --- a/.github/workflows/beam_PreCommit_Python_ML.yml +++ b/.github/workflows/beam_PreCommit_Python_ML.yml @@ -87,7 +87,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_ML PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Free Disk Space (Ubuntu) if: contains(matrix.os, 'ubuntu-latest') uses: jlumbroso/free-disk-space@v1.3.1 @@ -129,7 +129,7 @@ jobs: }}" \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results ${{ matrix.os }} diff --git a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml index 2e79a2806b30..5fce132513ce 100644 --- a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml +++ b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml @@ -90,7 +90,7 @@ jobs: github.event.comment.body == 'Run Python_PVR_Flink PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,7 +111,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Runners.yml b/.github/workflows/beam_PreCommit_Python_Runners.yml index ce9f1ef99752..82e106fb778e 100644 --- a/.github/workflows/beam_PreCommit_Python_Runners.yml +++ b/.github/workflows/beam_PreCommit_Python_Runners.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Runners PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -Pposargs=apache_beam/runners/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Transforms.yml b/.github/workflows/beam_PreCommit_Python_Transforms.yml index ef26df6b8e30..df13c2e41ee1 100644 --- a/.github/workflows/beam_PreCommit_Python_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Python_Transforms.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Transforms PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +99,7 @@ jobs: -Pposargs=apache_beam/transforms/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_RAT.yml b/.github/workflows/beam_PreCommit_RAT.yml index 3faa442ca0dc..46a648180844 100644 --- a/.github/workflows/beam_PreCommit_RAT.yml +++ b/.github/workflows/beam_PreCommit_RAT.yml @@ -69,7 +69,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run RAT PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_SQL.yml b/.github/workflows/beam_PreCommit_SQL.yml index a9248b08b902..1176976b0332 100644 --- a/.github/workflows/beam_PreCommit_SQL.yml +++ b/.github/workflows/beam_PreCommit_SQL.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run SQL PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -105,7 +105,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -121,7 +121,7 @@ jobs: run: | echo "path=sdks/java/extensions/sql/build/reports/jacoco/test/jacocoTestReport.xml" >> $GITHUB_OUTPUT - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_SQL_Java17.yml b/.github/workflows/beam_PreCommit_SQL_Java17.yml index ba7a1996d8ab..4bb5a6a5d69f 100644 --- a/.github/workflows/beam_PreCommit_SQL_Java17.yml +++ b/.github/workflows/beam_PreCommit_SQL_Java17.yml @@ -71,7 +71,7 @@ jobs: github.event.comment.body == 'Run SQL_Java17 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -PskipCheckerFramework \ -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -112,7 +112,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Spotless.yml b/.github/workflows/beam_PreCommit_Spotless.yml index c9a58c900806..145e31c4448a 100644 --- a/.github/workflows/beam_PreCommit_Spotless.yml +++ b/.github/workflows/beam_PreCommit_Spotless.yml @@ -76,7 +76,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Spotless PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: gradle-command: spotlessCheck checkStyleMain checkStyleTest :buildSrc:spotlessCheck arguments: -PdisableSpotlessApply - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: java-code-coverage-report path: "**/build/reports/checkstyle/*.xml" diff --git a/.github/workflows/beam_PreCommit_Typescript.yml b/.github/workflows/beam_PreCommit_Typescript.yml index 5cfd7a0b076f..1fe8898d6d8e 100644 --- a/.github/workflows/beam_PreCommit_Typescript.yml +++ b/.github/workflows/beam_PreCommit_Typescript.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Typescript PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Website.yml b/.github/workflows/beam_PreCommit_Website.yml index bd50ca8cbb04..103735e0b151 100644 --- a/.github/workflows/beam_PreCommit_Website.yml +++ b/.github/workflows/beam_PreCommit_Website.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml index 09054e6d0e90..150cfe06bad3 100644 --- a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml +++ b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website_Stage_GCS PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Whitespace.yml b/.github/workflows/beam_PreCommit_Whitespace.yml index 866686500c47..7c79a465d48a 100644 --- a/.github/workflows/beam_PreCommit_Whitespace.yml +++ b/.github/workflows/beam_PreCommit_Whitespace.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Whitespace PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml index 40c0933f97f6..959f36234d70 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -92,7 +92,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Xlang_Generated_Transforms PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 779b984cd02c..7d17fd2140c9 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -77,7 +77,7 @@ jobs: job_name: ["beam_PreCommit_Yaml_Xlang_Direct"] job_phrase: ["Run Yaml_Xlang_Direct PreCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: with: gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_Prober_CommunityMetrics.yml b/.github/workflows/beam_Prober_CommunityMetrics.yml index 0513caeed2e6..7a37a93fdfc2 100644 --- a/.github/workflows/beam_Prober_CommunityMetrics.yml +++ b/.github/workflows/beam_Prober_CommunityMetrics.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Prober_CommunityMetrics"] job_phrase: ["Run Community Metrics Prober"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_BeamMetrics.yml b/.github/workflows/beam_Publish_BeamMetrics.yml index 60a9298b17ef..db2e791c7dbd 100644 --- a/.github/workflows/beam_Publish_BeamMetrics.yml +++ b/.github/workflows/beam_Publish_BeamMetrics.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PostCommit_BeamMetrics_Publish"] job_phrase: ["Run Beam Metrics Deployment"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index b91428f5ea96..96648c6b79bd 100644 --- a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml +++ b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml @@ -79,7 +79,7 @@ jobs: - "python:container:ml:py313:docker" - "java:expansion-service:container:docker" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml b/.github/workflows/beam_Publish_Docker_Snapshots.yml index fe113121890e..d4aa75286116 100644 --- a/.github/workflows/beam_Publish_Docker_Snapshots.yml +++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml @@ -61,7 +61,7 @@ jobs: job_name: ["beam_Publish_Docker_Snapshots"] job_phrase: ["Publish Docker Snapshots"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_Website.yml b/.github/workflows/beam_Publish_Website.yml index a2966319c985..51bed6a3eff3 100644 --- a/.github/workflows/beam_Publish_Website.yml +++ b/.github/workflows/beam_Publish_Website.yml @@ -53,7 +53,7 @@ jobs: timeout-minutes: 30 name: beam_Publish_Website steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -69,4 +69,4 @@ jobs: with: gradle-command: :website:clean :website:publishWebsite arguments: -PgitPublishRemote="https://github.com/apache/beam.git" -PgcpCredsFile="${{ steps.auth-gcp.outputs.credentials_file_path }}" - - uses: actions/checkout@v4 # Extra checkout to make sure we're on master for post steps. + - uses: actions/checkout@v6 # Extra checkout to make sure we're on master for post steps. diff --git a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml index 0d70538d175d..93f147626b7a 100644 --- a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml +++ b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml @@ -61,7 +61,7 @@ jobs: job_name: ["beam_Python_CostBenchmark_Dataflow"] job_phrase: ["Run Python Dataflow Cost Benchmarks"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml index 1837fbc3e565..9ce1dec8ef7b 100644 --- a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml +++ b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -113,7 +113,7 @@ jobs: MULTIARCH_TAG: ${{ steps.set_tag.outputs.TAG }} USER: github-actions - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_Release_NightlySnapshot.yml b/.github/workflows/beam_Release_NightlySnapshot.yml index 6c011675b9ef..38c2f635d2c5 100644 --- a/.github/workflows/beam_Release_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_NightlySnapshot.yml @@ -51,7 +51,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Release_Python_NightlySnapshot.yml b/.github/workflows/beam_Release_Python_NightlySnapshot.yml index 7cb8bf27951a..46c64b30f22f 100644 --- a/.github/workflows/beam_Release_Python_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_Python_NightlySnapshot.yml @@ -53,7 +53,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml index e0cdfbcdd5be..690c4bdc8900 100644 --- a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_BigQueryIO"] job_phrase: ["Run Stress Tests Java BigQueryIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_BigTableIO.yml b/.github/workflows/beam_StressTests_Java_BigTableIO.yml index e6025a91b639..f50f4499e3e4 100644 --- a/.github/workflows/beam_StressTests_Java_BigTableIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigTableIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_BigTableIO"] job_phrase: ["Run Stress Tests Java BigTableIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_KafkaIO.yml b/.github/workflows/beam_StressTests_Java_KafkaIO.yml index f59665e7e8ae..d4dc1a20e918 100644 --- a/.github/workflows/beam_StressTests_Java_KafkaIO.yml +++ b/.github/workflows/beam_StressTests_Java_KafkaIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_KafkaIO"] job_phrase: ["Run Stress Tests Java KafkaIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_PubSubIO.yml b/.github/workflows/beam_StressTests_Java_PubSubIO.yml index 2c6415bbf0ca..f6b25638bd00 100644 --- a/.github/workflows/beam_StressTests_Java_PubSubIO.yml +++ b/.github/workflows/beam_StressTests_Java_PubSubIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_PubSubIO"] job_phrase: ["Run Stress Tests Java PubSubIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_SpannerIO.yml b/.github/workflows/beam_StressTests_Java_SpannerIO.yml index 605a4c84eec0..cb343cb70877 100644 --- a/.github/workflows/beam_StressTests_Java_SpannerIO.yml +++ b/.github/workflows/beam_StressTests_Java_SpannerIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_SpannerIO"] job_phrase: ["Run Stress Tests Java SpannerIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index cabe2b4c8ced..410925229f41 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -61,7 +61,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -170,7 +170,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Mask Apache Password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -270,7 +270,7 @@ jobs: ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -315,13 +315,13 @@ jobs: with: docker-images: false - name: Checkout Beam Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam path: beam - name: Checkout Beam Site Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: apache/beam-site path: beam-site @@ -431,7 +431,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -559,7 +559,7 @@ jobs: MANAGED_IO_DOCS_PATH: website/www/site/content/en/documentation/io/managed-io.md steps: - name: Checkout Beam Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index 9e7092008b5a..34f816949b5b 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: GCloud Docker credential helper diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 93f1419e4aba..bdee24af7afa 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -55,7 +55,7 @@ jobs: py-versions-full: ${{ steps.set-py-versions.outputs.py-versions-full }} py-versions-test: ${{ steps.set-py-versions.outputs.py-versions-test }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -88,7 +88,7 @@ jobs: rc_num: ${{ steps.get_rc_version.outputs.RC_NUM }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install python uses: actions/setup-python@v5 with: @@ -127,12 +127,12 @@ jobs: # https://github.com/pypa/setuptools/issues/4300 changed naming. Match both old and new names. run: mv $(ls | grep "apache-beam-\|apache_beam-") apache-beam-source - name: Upload source as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source path: sdks/python/apache-beam-source - name: Upload compressed sources as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source_zip path: sdks/python/dist @@ -170,13 +170,13 @@ jobs: run: mv $(ls | grep "apache-beam-\|apache_beam-") apache-beam-source-rc - name: Upload RC source as artifact if: steps.is_rc.outputs.is_rc == 1 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source_rc${{ steps.get_rc_version.outputs.RC_NUM }} path: sdks/python/apache-beam-source-rc - name: Upload compressed RC sources as artifacts if: steps.is_rc.outputs.is_rc == 1 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source_zip_rc${{ steps.get_rc_version.outputs.RC_NUM }} path: sdks/python/dist @@ -202,7 +202,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' steps: - name: Download compressed sources from artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: source_zip path: source/ @@ -231,13 +231,13 @@ jobs: py_version: ["cp310-", "cp311-", "cp312-", "cp313-", "cp314-"] steps: - name: Download python source distribution from artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: source path: apache-beam-source - name: Download Python SDK RC source distribution from artifacts if: ${{ needs.build_source.outputs.is_rc == 1 }} - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: source_rc${{ needs.build_source.outputs.rc_num }} path: apache-beam-source-rc @@ -275,7 +275,7 @@ jobs: shell: bash - name: Upload wheels as artifacts if: ${{ contains(matrix.os_python.python, matrix.py_version) }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheelhouse-${{ matrix.py_version }}${{ matrix.os_python.os }}${{ (matrix.os_python.arch == 'aarch64' && '-aarch64') || '' }} path: apache-beam-source/wheelhouse/ @@ -300,7 +300,7 @@ jobs: shell: bash - name: Upload RC wheels as artifacts if: ${{ needs.build_source.outputs.is_rc == 1 }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheelhouse-rc${{ needs.build_source.outputs.rc_num }}-${{ matrix.py_version }}${{ matrix.os_python.os }}${{ (matrix.os_python.arch == 'aarch64' && '-aarch64') || '' }} path: apache-beam-source-rc/wheelhouse/ @@ -314,7 +314,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' && github.event_name != 'pull_request' steps: - name: Download wheels from artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: pattern: wheelhouse-* merge-multiple: true @@ -365,7 +365,7 @@ jobs: if: github.repository_owner == 'apache' && github.event_name == 'schedule' steps: - name: Checkout code on master branch - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml index f826b22e043b..ab4df9fb4fd7 100644 --- a/.github/workflows/cancel.yml +++ b/.github/workflows/cancel.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/code_completion_plugin_tests.yml b/.github/workflows/code_completion_plugin_tests.yml index b183385383be..78022196fb29 100644 --- a/.github/workflows/code_completion_plugin_tests.yml +++ b/.github/workflows/code_completion_plugin_tests.yml @@ -57,13 +57,13 @@ jobs: # Check out beam repository - name: Fetch beam Sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: main # Check out intellij community repository for tests - name: Fetch intellij-community Sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: JetBrains/intellij-community path: intellij @@ -106,7 +106,7 @@ jobs: # Collect Tests Result of failed tests - name: Collect Tests Result if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: tests-result path: ${{ github.workspace }}/build/reports/tests diff --git a/.github/workflows/cut_release_branch.yml b/.github/workflows/cut_release_branch.yml index bedbd91c14a9..c5da893ee5fd 100644 --- a/.github/workflows/cut_release_branch.yml +++ b/.github/workflows/cut_release_branch.yml @@ -61,7 +61,7 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -110,7 +110,7 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/dask_runner_tests.yml b/.github/workflows/dask_runner_tests.yml index bbecfb8e18c2..674b1ca866fb 100644 --- a/.github/workflows/dask_runner_tests.yml +++ b/.github/workflows/dask_runner_tests.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install python uses: actions/setup-python@v5 with: @@ -52,7 +52,7 @@ jobs: working-directory: ./sdks/python/dist run: mv $(ls | grep "apache-beam.*tar\.gz\|apache_beam.*tar\.gz") apache-beam-source.tar.gz - name: Upload compressed sources as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python_sdk_source path: sdks/python/dist/apache-beam-source.tar.gz @@ -69,7 +69,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install python uses: actions/setup-python@v5 with: @@ -88,7 +88,7 @@ jobs: working-directory: ./sdks/python run: tox -c tox.ini -e ${{ matrix.params.tox_env }}-win-dask - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: pytest-${{matrix.os}}-${{matrix.params.py_ver}} diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 476314d4f9b6..b6f1fde44567 100644 --- a/.github/workflows/finalize_release.yml +++ b/.github/workflows/finalize_release.yml @@ -91,7 +91,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Mask PyPi password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -132,7 +132,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -172,7 +172,7 @@ jobs: POST_RELEASE_BRANCH: "release-${{ github.event.inputs.RELEASE }}-postrelease" steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/flaky_test_detection.yml b/.github/workflows/flaky_test_detection.yml index 32e2bb32c69e..733a15b2037a 100644 --- a/.github/workflows/flaky_test_detection.yml +++ b/.github/workflows/flaky_test_detection.yml @@ -38,7 +38,7 @@ jobs: flaky-test-detection: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/git_tag_released_version.yml b/.github/workflows/git_tag_released_version.yml index 0c6782603856..3ca113de90de 100644 --- a/.github/workflows/git_tag_released_version.yml +++ b/.github/workflows/git_tag_released_version.yml @@ -37,7 +37,7 @@ jobs: VERSION_PATH: ${{ github.event.inputs.VERSION_TAG }} steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index e5289e096787..61c26be9cee3 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -40,7 +40,7 @@ jobs: name: Go Build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 - name: Setup environment diff --git a/.github/workflows/issue-tagger.yml b/.github/workflows/issue-tagger.yml index dbfe2e996d5e..831bae7325cd 100644 --- a/.github/workflows/issue-tagger.yml +++ b/.github/workflows/issue-tagger.yml @@ -24,7 +24,7 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: damccorm/tag-ur-it@6fa72bbf1a2ea157b533d7e7abeafdb5855dbea5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/java_tests.yml b/.github/workflows/java_tests.yml index 393016c1a883..eedb1b102980 100644 --- a/.github/workflows/java_tests.yml +++ b/.github/workflows/java_tests.yml @@ -48,7 +48,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest, windows-latest] steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive @@ -64,7 +64,7 @@ jobs: gradle-command: test arguments: -p sdks/java/core/ - name: Upload test logs for :sdks:java:core:test - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_unit_tests-sdks-java-core-test-${{ matrix.os }} @@ -77,7 +77,7 @@ jobs: arguments: -p sdks/java/harness/ if: always() - name: Upload test logs for :sdks:java:harness:test - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_unit_tests-sdks-java-harness-test-${{ matrix.os }} @@ -90,7 +90,7 @@ jobs: arguments: -p runners/core-java/ if: always() - name: Upload test logs for :runners:core-java:test - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_unit_tests-runners-core-java-test-${{ matrix.os }} @@ -105,7 +105,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest, windows-latest] steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive @@ -122,7 +122,7 @@ jobs: -DintegrationTestRunner=direct -DintegrationTestPipelineOptions=[\"--runner=DirectRunner\",\"--tempRoot=./tmp\"] - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_wordcount_direct_runner-${{matrix.os}} diff --git a/.github/workflows/local_env_tests.yml b/.github/workflows/local_env_tests.yml index 3983bfe7e7b9..fdee2f3492ea 100644 --- a/.github/workflows/local_env_tests.yml +++ b/.github/workflows/local_env_tests.yml @@ -45,7 +45,7 @@ jobs: name: "Ubuntu run local environment shell script" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -62,7 +62,7 @@ jobs: name: "Mac run local environment shell script" runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/playground_frontend_test.yml b/.github/workflows/playground_frontend_test.yml index 1a0dff44d2e8..3c2fa18e18d3 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -45,7 +45,7 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: 'Cache Flutter Dependencies' uses: actions/cache@v4 diff --git a/.github/workflows/pr-bot-new-prs.yml b/.github/workflows/pr-bot-new-prs.yml index 590824002012..14254c364746 100644 --- a/.github/workflows/pr-bot-new-prs.yml +++ b/.github/workflows/pr-bot-new-prs.yml @@ -33,7 +33,7 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/pr-bot-pr-updates.yml b/.github/workflows/pr-bot-pr-updates.yml index 86cc291e87bb..2038823b2f4d 100644 --- a/.github/workflows/pr-bot-pr-updates.yml +++ b/.github/workflows/pr-bot-pr-updates.yml @@ -36,7 +36,7 @@ jobs: steps: # Pin to master so users can't do anything malicious on their own branch and run it here. - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: 'master' - name: Setup Node diff --git a/.github/workflows/pr-bot-prs-needing-attention.yml b/.github/workflows/pr-bot-prs-needing-attention.yml index eb6adfcaa823..d8a2e56d2d3d 100644 --- a/.github/workflows/pr-bot-prs-needing-attention.yml +++ b/.github/workflows/pr-bot-prs-needing-attention.yml @@ -33,7 +33,7 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/publish_github_release_notes.yml b/.github/workflows/publish_github_release_notes.yml index 473e0deef83d..f1a1183789e6 100644 --- a/.github/workflows/publish_github_release_notes.yml +++ b/.github/workflows/publish_github_release_notes.yml @@ -36,7 +36,7 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -49,7 +49,7 @@ jobs: name: Publish Github Release Notes steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Publish github release notes run: | POST_PATH="website/www/site/content/en/blog/beam-${{env.RELEASE_VERSION}}.md" diff --git a/.github/workflows/python_dependency_tests.yml b/.github/workflows/python_dependency_tests.yml index ad5d5c4629a3..777e57f6d585 100644 --- a/.github/workflows/python_dependency_tests.yml +++ b/.github/workflows/python_dependency_tests.yml @@ -34,7 +34,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install libsnappy-dev run: sudo apt-get update && sudo apt-get install -y libsnappy-dev - name: Install python diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index a8a3716ce8af..6740b45c7956 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -45,7 +45,7 @@ jobs: outputs: gcp-variables-set: ${{ steps.check_gcp_variables.outputs.gcp-variables-set }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -68,7 +68,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -80,7 +80,7 @@ jobs: working-directory: ./sdks/python/dist run: mv $(ls | grep "apache-beam.*tar\.gz\|apache_beam.*tar\.gz") apache-beam-source.tar.gz - name: Upload compressed sources as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python_sdk_source path: sdks/python/dist/apache-beam-source.tar.gz @@ -101,7 +101,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -121,7 +121,7 @@ jobs: working-directory: ./sdks/python run: tox -c tox.ini run -e ${{ matrix.params.tox_env }}-win - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: pytest-${{matrix.os}}-${{matrix.params.py_ver}} @@ -137,7 +137,7 @@ jobs: python: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install python uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/refresh_looker_metrics.yml b/.github/workflows/refresh_looker_metrics.yml index 0d6f76c627a1..2c4d0bcdbe4e 100644 --- a/.github/workflows/refresh_looker_metrics.yml +++ b/.github/workflows/refresh_looker_metrics.yml @@ -31,7 +31,7 @@ jobs: refresh_looker_metrics: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/reportGenerator.yml b/.github/workflows/reportGenerator.yml index 7a4abdb66a08..142b6f9b86a0 100644 --- a/.github/workflows/reportGenerator.yml +++ b/.github/workflows/reportGenerator.yml @@ -26,7 +26,7 @@ jobs: name: Generate issue report runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index b1b4618f2f74..7519f61d1e5b 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -57,7 +57,7 @@ jobs: ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: "release-${{ env.release }}-postrelease" repository: apache/beam diff --git a/.github/workflows/run_perf_alert_tool.yml b/.github/workflows/run_perf_alert_tool.yml index 81031e4ea3b6..055880d54d21 100644 --- a/.github/workflows/run_perf_alert_tool.yml +++ b/.github/workflows/run_perf_alert_tool.yml @@ -35,7 +35,7 @@ jobs: issues: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/run_rc_validation_go_wordcount.yml b/.github/workflows/run_rc_validation_go_wordcount.yml index ca3964bf6421..5f60ae9dbe42 100644 --- a/.github/workflows/run_rc_validation_go_wordcount.yml +++ b/.github/workflows/run_rc_validation_go_wordcount.yml @@ -48,7 +48,7 @@ jobs: runs-on: self-hosted steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up environment uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/run_rc_validation_java_mobile_gaming.yml b/.github/workflows/run_rc_validation_java_mobile_gaming.yml index aecfd76db9b0..f8aabfd126b2 100644 --- a/.github/workflows/run_rc_validation_java_mobile_gaming.yml +++ b/.github/workflows/run_rc_validation_java_mobile_gaming.yml @@ -78,7 +78,7 @@ jobs: run: echo "GCS_BUCKET_NAME=$(echo ${{ github.event.inputs.GCS_BUCKET }} | sed 's/^gs:\/\///')" >> $GITHUB_ENV - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: v${{ github.event.inputs.RELEASE_VER }}-RC${{ github.event.inputs.RC_NUM }} @@ -129,7 +129,7 @@ jobs: # Reporting (Optional: Keep if test results are generated) - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() # Upload only on failure with: name: JUnit Test Results (Java MobileGaming RC) diff --git a/.github/workflows/run_rc_validation_java_quickstart.yml b/.github/workflows/run_rc_validation_java_quickstart.yml index 269ccaab8d57..dce9b7f3fedb 100644 --- a/.github/workflows/run_rc_validation_java_quickstart.yml +++ b/.github/workflows/run_rc_validation_java_quickstart.yml @@ -68,7 +68,7 @@ jobs: timeout-minutes: 60 # Adjust timeout as needed steps: - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/run_rc_validation_python_mobile_gaming.yml b/.github/workflows/run_rc_validation_python_mobile_gaming.yml index a8609e22b73e..7bffc0555e53 100644 --- a/.github/workflows/run_rc_validation_python_mobile_gaming.yml +++ b/.github/workflows/run_rc_validation_python_mobile_gaming.yml @@ -89,7 +89,7 @@ jobs: steps: - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/run_rc_validation_python_yaml.yml b/.github/workflows/run_rc_validation_python_yaml.yml index 7395d1601505..2a8b828d29d7 100644 --- a/.github/workflows/run_rc_validation_python_yaml.yml +++ b/.github/workflows/run_rc_validation_python_yaml.yml @@ -81,7 +81,7 @@ jobs: steps: - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml index fb7b61f6b05c..4271020ad403 100644 --- a/.github/workflows/tour_of_beam_backend.yml +++ b/.github/workflows/tour_of_beam_backend.yml @@ -41,7 +41,7 @@ jobs: run: working-directory: ./learning/tour-of-beam/backend steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: # pin to the biggest Go version supported by Cloud Functions runtime diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml index c18b51eb3176..e4b96793906c 100644 --- a/.github/workflows/tour_of_beam_backend_integration.yml +++ b/.github/workflows/tour_of_beam_backend_integration.yml @@ -76,7 +76,7 @@ jobs: run: working-directory: ./learning/tour-of-beam/backend steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/tour_of_beam_frontend_test.yml b/.github/workflows/tour_of_beam_frontend_test.yml index 1dc13c3fc758..f1afd9b377d8 100644 --- a/.github/workflows/tour_of_beam_frontend_test.yml +++ b/.github/workflows/tour_of_beam_frontend_test.yml @@ -47,7 +47,7 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: 'Cache Flutter Dependencies' uses: actions/cache@v4 diff --git a/.github/workflows/typescript_tests.yml b/.github/workflows/typescript_tests.yml index dc28334162c9..9bc352379913 100644 --- a/.github/workflows/typescript_tests.yml +++ b/.github/workflows/typescript_tests.yml @@ -54,7 +54,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest] steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive @@ -96,7 +96,7 @@ jobs: fail-fast: false steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive @@ -137,7 +137,7 @@ jobs: outputs: gcp-variables-set: ${{ steps.check_gcp_variables.outputs.gcp-variables-set }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -159,7 +159,7 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/update_python_dependencies.yml b/.github/workflows/update_python_dependencies.yml index 0da9698c3700..753fb6037fcc 100644 --- a/.github/workflows/update_python_dependencies.yml +++ b/.github/workflows/update_python_dependencies.yml @@ -41,7 +41,7 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -51,7 +51,7 @@ jobs: name: Update Python Dependencies steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: From d258037c3f6092bac3f6e42620558307e5c60334 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Mon, 4 May 2026 20:15:15 +0200 Subject: [PATCH 064/490] Fix SDF bundle finalization timeout in streaming test (#38287) * Fix SDF bundle finalization timeout in streaming test * trigger workflow * increased FINALIZATION_CALLBACK_TIMEOUT_SECS * trigger workflow * trigger workflow * Increased timeout * Fix SDF bundle-finalizer timeout constant in SplittableDoFnTest * Stabilize bundle finalization SDF validates runner tests --- ...va_ValidatesRunner_Dataflow_Streaming.json | 2 +- .../sdk/transforms/SplittableDoFnTest.java | 46 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json index 090751435f20..e623d3373a93 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 7, + "modification": 1, } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java index 8ce9330b9ab4..80d8728aa01b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java @@ -31,10 +31,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.coders.BigEndianIntegerCoder; @@ -958,16 +958,22 @@ public OffsetRange getInitialRestriction() { } /** - * While the finalization callback hasn't been invoked, this DoFn will keep requesting - * finalization, wait one second and then checkpoint upto MAX_ATTEMPTS amount of times. Once the - * callback has been invoked, the DoFn will output the element and stop. + * While the finalization callback hasn't been invoked, this DoFn repeatedly registers a + * finalization request, sleeps ~100ms, and checkpoints (via {@link ProcessContinuation#resume()}) + * up to {@link #MAX_ATTEMPTS} times. Once the callback runs and flips {@code WAS_FINALIZED}, the + * DoFn outputs the element and stops. + * + *

    Shared state must be thread-safe: {@link BundleFinalizer} callbacks can run on a different + * thread than {@code @ProcessElement}; use {@link ConcurrentHashMap} for {@code WAS_FINALIZED} so + * {@code computeIfAbsent} / updates are not racy (a plain {@link HashMap} can hang or corrupt + * under concurrent structural access). */ public static class BundleFinalizingSplittableDoFn extends DoFn { + /** Upper bound on {@link ProcessContinuation#resume()} iterations via restriction width. */ private static final long MAX_ATTEMPTS = 3000; - // We use the UUID to uniquely identify this DoFn in case this test is run with - // other tests in the same JVM. - private static final Map WAS_FINALIZED = new HashMap(); - private final UUID uuid = UUID.randomUUID(); + + private static final long FINALIZATION_CALLBACK_TIMEOUT_SECS = 300; + private static final Map WAS_FINALIZED = new ConcurrentHashMap<>(); @NewTracker public RestrictionTracker newTracker(@Restriction OffsetRange restriction) { @@ -987,23 +993,29 @@ public ProcessContinuation process( RestrictionTracker tracker, BundleFinalizer bundleFinalizer) throws InterruptedException { - if (WAS_FINALIZED.computeIfAbsent(uuid, (unused) -> new AtomicBoolean()).get()) { + AtomicBoolean wasFinalized = + WAS_FINALIZED.computeIfAbsent(element, (unused) -> new AtomicBoolean()); + if (wasFinalized.get()) { tracker.tryClaim(tracker.currentRestriction().getFrom() + 1); receiver.output(element); + WAS_FINALIZED.remove(element); // Claim beyond the end now that we know we have been finalized. tracker.tryClaim(Long.MAX_VALUE); return stop(); } if (tracker.tryClaim(tracker.currentRestriction().getFrom() + 1)) { bundleFinalizer.afterBundleCommit( - Instant.now().plus(Duration.standardSeconds(MAX_ATTEMPTS)), - () -> WAS_FINALIZED.computeIfAbsent(uuid, (unused) -> new AtomicBoolean()).set(true)); + Instant.now().plus(Duration.standardSeconds(FINALIZATION_CALLBACK_TIMEOUT_SECS)), + () -> wasFinalized.set(true)); // We sleep here instead of setting a resume time since the resume time doesn't need to // be honored. sleep(100L); return resume(); } - return stop(); + WAS_FINALIZED.remove(element); + throw new RuntimeException( + String.format( + "Bundle finalization callback was not observed after %d checkpoints.", MAX_ATTEMPTS)); } @GetInitialRestriction @@ -1017,9 +1029,10 @@ public OffsetRange getInitialRestriction() { public void testBundleFinalizationOccursOnBoundedSplittableDoFn() throws Exception { @BoundedPerElement class BoundedBundleFinalizingSplittableDoFn extends BundleFinalizingSplittableDoFn {} - PCollection foo = p.apply(Create.of("foo")); + String element = "foo-" + UUID.randomUUID(); + PCollection foo = p.apply(Create.of(element)); PCollection res = foo.apply(ParDo.of(new BoundedBundleFinalizingSplittableDoFn())); - PAssert.that(res).containsInAnyOrder("foo"); + PAssert.that(res).containsInAnyOrder(element); p.run(); } @@ -1028,9 +1041,10 @@ class BoundedBundleFinalizingSplittableDoFn extends BundleFinalizingSplittableDo public void testBundleFinalizationOccursOnUnboundedSplittableDoFn() throws Exception { @UnboundedPerElement class UnboundedBundleFinalizingSplittableDoFn extends BundleFinalizingSplittableDoFn {} - PCollection foo = p.apply(Create.of("foo")); + String element = "foo-" + UUID.randomUUID(); + PCollection foo = p.apply(Create.of(element)); PCollection res = foo.apply(ParDo.of(new UnboundedBundleFinalizingSplittableDoFn())); - PAssert.that(res).containsInAnyOrder("foo"); + PAssert.that(res).containsInAnyOrder(element); p.run(); } From be40d1064e2c9043954edeec1a80a981ea9c8a18 Mon Sep 17 00:00:00 2001 From: "RuiLong J." Date: Mon, 4 May 2026 11:38:52 -0700 Subject: [PATCH 065/490] Add option to use asyncio for AsyncWrapper (#38262) * Add option to use asyncio for AsyncWrapper * Update sdks/python/apache_beam/transforms/async_dofn.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update sdks/python/apache_beam/transforms/async_dofn.py Co-authored-by: Danny McCormick * Address comments * Minor fixes * lint --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Danny McCormick --- .../apache_beam/transforms/async_dofn.py | 98 +++++++++++++++++-- .../apache_beam/transforms/async_dofn_test.py | 49 ++++++---- 2 files changed, 122 insertions(+), 25 deletions(-) diff --git a/sdks/python/apache_beam/transforms/async_dofn.py b/sdks/python/apache_beam/transforms/async_dofn.py index 5e1c6d219f4b..28568bd893c5 100644 --- a/sdks/python/apache_beam/transforms/async_dofn.py +++ b/sdks/python/apache_beam/transforms/async_dofn.py @@ -17,15 +17,21 @@ from __future__ import absolute_import +import asyncio +import inspect import logging import random +import threading import uuid +from collections.abc import AsyncIterable +from collections.abc import Iterable from concurrent.futures import ThreadPoolExecutor from math import floor from threading import RLock from time import sleep from time import time from types import GeneratorType +from typing import Optional import apache_beam as beam from apache_beam import TimeDomain @@ -60,6 +66,9 @@ class AsyncWrapper(beam.DoFn): [coders.FastPrimitivesCoder(), coders.FastPrimitivesCoder()])) # The below items are one per dofn (not instance) so are maps of UUID to # value. + _event_loop: Optional[asyncio.AbstractEventLoop] = None + _event_loop_thread: Optional[threading.Thread] = None + _loop_started: Optional[threading.Event] = None _processing_elements = {} _items_in_buffer = {} _pool = {} @@ -78,6 +87,7 @@ def __init__( timeout=1, max_wait_time=0.5, id_fn=None, + use_asyncio=False, ): """Wraps the sync_fn to create an asynchronous version. @@ -104,6 +114,10 @@ def __init__( schedule an item. Used in testing to ensure timeouts are met. id_fn: A function that returns a hashable object from an element. This will be used to track items instead of the element's default hash. + use_asyncio: If true, use asyncio and coroutines to process items. If + false, use ThreadPoolExecutor. Use asyncio when the work being done + is not CPU intensive and heavily waits on network or IO which can + benefit from higher parallelism. """ self._sync_fn = sync_fn self._uuid = uuid.uuid4().hex @@ -112,6 +126,7 @@ def __init__( self._max_wait_time = max_wait_time self._timer_frequency = callback_frequency self._id_fn = id_fn or (lambda x: x) + self._use_asyncio = use_asyncio if max_items_to_buffer is None: self._max_items_to_buffer = max(parallelism * 2, 10) else: @@ -126,11 +141,33 @@ def __init__( def initialize_pool(parallelism): return lambda: ThreadPoolExecutor(max_workers=parallelism) + @staticmethod + def _run_event_loop(): + """Sets up and runs the asyncio event loop in a background thread.""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + AsyncWrapper._event_loop = loop + AsyncWrapper._loop_started.set() + loop.run_forever() + loop.close() + @staticmethod def reset_state(): - for pool in AsyncWrapper._pool.values(): - pool.acquire(AsyncWrapper.initialize_pool(1)).shutdown( - wait=True, cancel_futures=True) + with AsyncWrapper._lock: + if AsyncWrapper._event_loop: + AsyncWrapper._event_loop.call_soon_threadsafe( + AsyncWrapper._event_loop.stop) + if AsyncWrapper._event_loop_thread: + AsyncWrapper._event_loop_thread.join() + + AsyncWrapper._event_loop = None + AsyncWrapper._event_loop_thread = None + if AsyncWrapper._loop_started is not None: + AsyncWrapper._loop_started.clear() + + for pool in AsyncWrapper._pool.values(): + pool.acquire(AsyncWrapper.initialize_pool(1)).shutdown( + wait=True, cancel_futures=True) with AsyncWrapper._lock: AsyncWrapper._pool = {} AsyncWrapper._processing_elements = {} @@ -140,6 +177,13 @@ def setup(self): """Forwards to the wrapped dofn's setup method.""" self._sync_fn.setup() with AsyncWrapper._lock: + if self._use_asyncio and AsyncWrapper._event_loop_thread is None: + AsyncWrapper._loop_started = threading.Event() + AsyncWrapper._event_loop_thread = threading.Thread( + target=AsyncWrapper._run_event_loop, daemon=True) + AsyncWrapper._event_loop_thread.start() + AsyncWrapper._loop_started.wait() + if not self._uuid in AsyncWrapper._pool: AsyncWrapper._pool[self._uuid] = Shared() AsyncWrapper._processing_elements[self._uuid] = {} @@ -187,9 +231,41 @@ def sync_fn_process(self, element, *args, **kwargs): to_return.append(x) for x in bundle_result: to_return.append(x) - return to_return + async def async_fn_process(self, element, *args, **kwargs): + """Makes the call to the wrapped dofn's start_bundle, process + and finish_bundle methods for asynchronous DoFns. + + Args: + element: The element to process. + *args: Any additional arguments to pass to the wrapped dofn's process + method. + **kwargs: Any additional keyword arguments to pass to the wrapped dofn's + process method. + + Returns: + A list of elements produced by the input element. + """ + async def _collect(result): + if result is None: + return [] + if inspect.isawaitable(result): + result = await result + if isinstance(result, AsyncIterable): + return [item async for item in result] + if isinstance(result, + (GeneratorType, Iterable)) and not isinstance(result, + (str, bytes)): + return list(result) + return [result] + + self._sync_fn.start_bundle() + process_result = await _collect( + self._sync_fn.process(element, *args, **kwargs)) + bundle_result = await _collect(self._sync_fn.finish_bundle()) + return process_result + bundle_result + def decrement_items_in_buffer(self, future): with AsyncWrapper._lock: AsyncWrapper._items_in_buffer[self._uuid] -= 1 @@ -214,10 +290,16 @@ def schedule_if_room(self, element, ignore_buffer=False, *args, **kwargs): logging.info('item %s already in processing elements', element) return True if self.accepting_items() or ignore_buffer: - result = AsyncWrapper._pool[self._uuid].acquire( - AsyncWrapper.initialize_pool(self._parallelism)).submit( - lambda: self.sync_fn_process(element, *args, **kwargs), - ) + if self._use_asyncio: + result = asyncio.run_coroutine_threadsafe( + self.async_fn_process(element, *args, **kwargs), + AsyncWrapper._event_loop, + ) + else: + result = AsyncWrapper._pool[self._uuid].acquire( + AsyncWrapper.initialize_pool(self._parallelism)).submit( + lambda: self.sync_fn_process(element, *args, **kwargs), + ) result.add_done_callback(self.decrement_items_in_buffer) AsyncWrapper._processing_elements[self._uuid][element_id] = ( element, result) diff --git a/sdks/python/apache_beam/transforms/async_dofn_test.py b/sdks/python/apache_beam/transforms/async_dofn_test.py index fe75de05ccd5..81c7b8e163ff 100644 --- a/sdks/python/apache_beam/transforms/async_dofn_test.py +++ b/sdks/python/apache_beam/transforms/async_dofn_test.py @@ -22,6 +22,8 @@ from concurrent.futures import ThreadPoolExecutor from threading import Lock +from parameterized import parameterized_class + import apache_beam as beam import apache_beam.transforms.async_dofn as async_lib @@ -62,7 +64,7 @@ class FakeBagState: def __init__(self, items): self.items = items # Normally SE would have a lock on the BT row protecting this from multiple - # updates. Here without SE we must lock ourselvs. + # updates. Here without SE we must lock ourselves. self.lock = Lock() def add(self, item): @@ -86,6 +88,14 @@ def set(self, time): self.time = time +@parameterized_class([ + { + "use_asyncio": True + }, + { + "use_asyncio": False + }, +]) class AsyncTest(unittest.TestCase): def setUp(self): super().setUp() @@ -132,7 +142,8 @@ def __eq__(self, other): return self.element_id == other.element_id dofn = BasicDofn() - async_dofn = async_lib.AsyncWrapper(dofn, id_fn=lambda x: x.element_id) + async_dofn = async_lib.AsyncWrapper( + dofn, id_fn=lambda x: x.element_id, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state = FakeBagState([]) fake_timer = FakeTimer(0) @@ -156,7 +167,7 @@ def __eq__(self, other): def test_basic(self): # Setup an async dofn and send a message in to process. dofn = BasicDofn() - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state = FakeBagState([]) fake_timer = FakeTimer(0) @@ -181,9 +192,9 @@ def test_basic(self): self.assertEqual(fake_bag_state.items, []) def test_multi_key(self): - # Send in two messages with different keys.. + # Send in two messages with different keys. dofn = BasicDofn() - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state_key1 = FakeBagState([]) fake_bag_state_key2 = FakeBagState([]) @@ -211,7 +222,7 @@ def test_multi_key(self): def test_long_item(self): # Test that everything still works with a long running time for the dofn. dofn = BasicDofn(sleep_time=5) - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state = FakeBagState([]) fake_timer = FakeTimer(0) @@ -231,10 +242,10 @@ def test_long_item(self): self.assertEqual(fake_bag_state.items, []) def test_lost_item(self): - # Setup an element in the bag stat thats not in processing state. + # Setup an element in the bag state that's not in processing state. # The async dofn should reschedule this element. dofn = BasicDofn() - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_timer = FakeTimer(0) msg = ('key1', 1) @@ -250,9 +261,9 @@ def test_lost_item(self): def test_cancelled_item(self): # Test that an item gets removed for processing and does not get output when # it is not present in the bag state. Either this item moved or a commit - # failed making the local state and bag stat inconsistent. + # failed making the local state and bag state inconsistent. dofn = BasicDofn() - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() msg = ('key1', 1) msg2 = ('key1', 2) @@ -272,7 +283,7 @@ def test_multi_element_dofn(self): # Test that async works when a dofn produces multiple elements in process # and finish_bundle. dofn = MultiElementDoFn() - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state = FakeBagState([]) fake_timer = FakeTimer(0) @@ -289,7 +300,7 @@ def test_duplicates(self): # Test that async will produce a single output when a given input is sent # multiple times. dofn = BasicDofn(5) - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state = FakeBagState([]) fake_timer = FakeTimer(0) @@ -310,7 +321,7 @@ def test_slow_duplicates(self): # Test that async will produce a single output when a given input is sent # multiple times. dofn = BasicDofn(5) - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() fake_bag_state = FakeBagState([]) fake_timer = FakeTimer(0) @@ -335,7 +346,7 @@ def test_slow_duplicates(self): def test_buffer_count(self): # Test that the buffer count is correctly incremented when adding items. dofn = BasicDofn(5) - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() msg = ('key1', 1) fake_timer = FakeTimer(0) @@ -353,7 +364,10 @@ def test_buffer_stops_accepting_items(self): # Test that the buffer stops accepting items when it is full. dofn = BasicDofn(5) async_dofn = async_lib.AsyncWrapper( - dofn, parallelism=1, max_items_to_buffer=5) + dofn, + parallelism=1, + max_items_to_buffer=5, + use_asyncio=self.use_asyncio) async_dofn.setup() fake_timer = FakeTimer(0) fake_bag_state = FakeBagState([]) @@ -391,7 +405,7 @@ def add_item(i): def test_buffer_with_cancellation(self): dofn = BasicDofn(3) - async_dofn = async_lib.AsyncWrapper(dofn) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=self.use_asyncio) async_dofn.setup() msg = ('key1', 1) msg2 = ('key1', 2) @@ -423,7 +437,8 @@ def test_load_correctness(self): # Test AsyncDofn over heavy load. dofn = BasicDofn(1) max_sleep = 10 - async_dofn = async_lib.AsyncWrapper(dofn, max_wait_time=max_sleep) + async_dofn = async_lib.AsyncWrapper( + dofn, max_wait_time=max_sleep, use_asyncio=self.use_asyncio) async_dofn.setup() bag_states = {} timers = {} From 9be2aa3c9c9025871228e06b91abb9707a3deb36 Mon Sep 17 00:00:00 2001 From: Lalit Yadav Date: Mon, 4 May 2026 14:34:45 -0500 Subject: [PATCH 066/490] Add TableRowMatchers with strict type-aware equality for BigQuery (#37890) * Add TableRowMatchers with strict type-aware equality for BigQuery * Fix checkstyle NeedBraces violations * Remove public modifier from TableRowMatchers to fix GcpApiSurfaceTest --- .../BigQueryTableRowEqualityTest.java | 202 +++++++++++++++++ .../sdk/io/gcp/bigquery/TableRowMatchers.java | 204 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java create mode 100644 sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java new file mode 100644 index 000000000000..5b8a095a3d09 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import static org.apache.beam.sdk.io.gcp.bigquery.TableRowMatchers.isTableRowEqualTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; + +import com.google.api.services.bigquery.model.TableRow; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link TableRowMatchers} class. */ +@RunWith(JUnit4.class) +public class BigQueryTableRowEqualityTest { + + @Test + public void testIdenticalRows() { + TableRow row1 = new TableRow().set("count", 1).set("name", "Alice"); + TableRow row2 = new TableRow().set("count", 1).set("name", "Alice"); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testEmptyRows() { + TableRow row1 = new TableRow(); + TableRow row2 = new TableRow(); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testIntegerVsString() { + TableRow rowWithInteger = new TableRow().set("count", 1); + TableRow rowWithString = new TableRow().set("count", "1"); + assertThat(rowWithInteger, not(isTableRowEqualTo(rowWithString))); + } + + @Test + public void testDoubleVsInteger() { + TableRow rowWithDouble = new TableRow().set("value", 1.0); + TableRow rowWithInteger = new TableRow().set("value", 1); + assertThat(rowWithDouble, not(isTableRowEqualTo(rowWithInteger))); + } + + @Test + public void testBooleanVsString() { + TableRow rowWithBoolean = new TableRow().set("active", true); + TableRow rowWithString = new TableRow().set("active", "true"); + assertThat(rowWithBoolean, not(isTableRowEqualTo(rowWithString))); + } + + @Test + public void testBothFieldsNull() { + TableRow row1 = new TableRow().set("name", null); + TableRow row2 = new TableRow().set("name", null); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testNullVsNonNull() { + TableRow rowWithNull = new TableRow().set("name", null); + TableRow rowWithValue = new TableRow().set("name", "Alice"); + assertThat(rowWithNull, not(isTableRowEqualTo(rowWithValue))); + } + + @Test + public void testEmptyStringVsNull() { + TableRow rowWithEmptyString = new TableRow().set("name", ""); + TableRow rowWithNull = new TableRow().set("name", null); + assertThat(rowWithEmptyString, not(isTableRowEqualTo(rowWithNull))); + } + + @Test + public void testWhitespaceDifference() { + TableRow rowWithoutSpace = new TableRow().set("name", "Alice"); + TableRow rowWithLeadingSpace = new TableRow().set("name", " Alice"); + assertThat(rowWithoutSpace, not(isTableRowEqualTo(rowWithLeadingSpace))); + } + + @Test + public void testDifferentFieldCount() { + TableRow rowWithTwoFields = new TableRow().set("a", 1).set("b", 2); + TableRow rowWithOneField = new TableRow().set("a", 1); + assertThat(rowWithTwoFields, not(isTableRowEqualTo(rowWithOneField))); + } + + @Test + public void testMissingField() { + TableRow rowWithFieldB = new TableRow().set("a", 1).set("b", 2); + TableRow rowWithFieldC = new TableRow().set("a", 1).set("c", 2); + assertThat(rowWithFieldB, not(isTableRowEqualTo(rowWithFieldC))); + } + + @Test + public void testDifferentInsertionOrder() { + TableRow row1 = new TableRow().set("a", 1).set("b", 2); + TableRow row2 = new TableRow().set("b", 2).set("a", 1); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testIdenticalNestedRows() { + TableRow innerRow1 = new TableRow().set("id", 42); + TableRow innerRow2 = new TableRow().set("id", 42); + TableRow outerRow1 = new TableRow().set("nested", innerRow1); + TableRow outerRow2 = new TableRow().set("nested", innerRow2); + assertThat(outerRow1, isTableRowEqualTo(outerRow2)); + } + + @Test + public void testNestedRowsWithTypeMismatch() { + TableRow innerRowWithInteger = new TableRow().set("id", 42); + TableRow innerRowWithString = new TableRow().set("id", "42"); + TableRow outerRow1 = new TableRow().set("nested", innerRowWithInteger); + TableRow outerRow2 = new TableRow().set("nested", innerRowWithString); + assertThat(outerRow1, not(isTableRowEqualTo(outerRow2))); + } + + @Test + public void testDeeplyNestedRowsWithTypeMismatch() { + TableRow level3WithInteger = new TableRow().set("val", 1); + TableRow level3WithString = new TableRow().set("val", "1"); + TableRow level2Row1 = new TableRow().set("l2", level3WithInteger); + TableRow level2Row2 = new TableRow().set("l2", level3WithString); + TableRow level1Row1 = new TableRow().set("l1", level2Row1); + TableRow level1Row2 = new TableRow().set("l1", level2Row2); + assertThat(level1Row1, not(isTableRowEqualTo(level1Row2))); + } + + @Test + public void testIdenticalListFields() { + TableRow row1 = new TableRow().set("tags", Arrays.asList("a", "b")); + TableRow row2 = new TableRow().set("tags", Arrays.asList("a", "b")); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testListFieldsWithDifferentOrder() { + TableRow row1 = new TableRow().set("tags", Arrays.asList("a", "b")); + TableRow row2 = new TableRow().set("tags", Arrays.asList("b", "a")); + assertThat(row1, not(isTableRowEqualTo(row2))); + } + + @Test + public void testZeroIntegerVsZeroDouble() { + TableRow rowWithZeroInteger = new TableRow().set("value", 0); + TableRow rowWithZeroDouble = new TableRow().set("value", 0.0); + assertThat(rowWithZeroInteger, not(isTableRowEqualTo(rowWithZeroDouble))); + } + + @Test + public void testNegativeNumbers() { + TableRow row1 = new TableRow().set("temp", -10); + TableRow row2 = new TableRow().set("temp", -10); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testLongVsInteger() { + TableRow rowWithLong = new TableRow().set("count", 1L); + TableRow rowWithInteger = new TableRow().set("count", 1); + assertThat(rowWithLong, not(isTableRowEqualTo(rowWithInteger))); + } + + @Test + public void testTrueVsFalse() { + TableRow rowWithTrue = new TableRow().set("active", true); + TableRow rowWithFalse = new TableRow().set("active", false); + assertThat(rowWithTrue, not(isTableRowEqualTo(rowWithFalse))); + } + + @Test + public void testLargeIntegerVsLong() { + TableRow rowWithInteger = new TableRow().set("big", Integer.MAX_VALUE); + TableRow rowWithLong = new TableRow().set("big", (long) Integer.MAX_VALUE); + assertThat(rowWithInteger, not(isTableRowEqualTo(rowWithLong))); + } + + @Test + public void testMultipleFieldsWithOneTypeMismatch() { + TableRow row1 = new TableRow().set("id", 1).set("name", "Alice").set("score", 99); + TableRow row2 = new TableRow().set("id", 1).set("name", "Alice").set("score", "99"); + assertThat(row1, not(isTableRowEqualTo(row2))); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java new file mode 100644 index 000000000000..988b1bd0842c --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import com.google.api.services.bigquery.model.TableRow; +import java.util.stream.Collectors; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +class TableRowMatchers { + + static Matcher isTableRowEqualTo(TableRow expected) { + return new TypeSafeMatcher() { + + @Override + protected boolean matchesSafely(TableRow actual) { + return rowsMatch(expected, actual); + } + + @Override + public void describeTo(Description description) { + description.appendText("TableRow (strict) "); + description.appendText(formatValue(expected, 0)); + } + + @Override + protected void describeMismatchSafely(TableRow actual, Description mismatch) { + describeRowMismatch(expected, actual, mismatch); + } + }; + } + + private static boolean rowsMatch(TableRow expected, TableRow actual) { + if (expected == null && actual == null) { + return true; + } + + if (expected == null || actual == null) { + return false; + } + + if (actual.size() != expected.size()) { + return false; + } + + for (String key : expected.keySet()) { + if (!actual.containsKey(key)) { + return false; + } + + Object expectedVal = expected.get(key); + Object actualVal = actual.get(key); + + if (expectedVal == null && actualVal == null) { + continue; + } + if (expectedVal == null || actualVal == null) { + return false; + } + + // recursively compare nested TableRows + if (expectedVal instanceof TableRow && actualVal instanceof TableRow) { + if (!rowsMatch((TableRow) expectedVal, (TableRow) actualVal)) { + return false; + } + continue; + } + + if (!expectedVal.getClass().equals(actualVal.getClass())) { + return false; + } + + if (!expectedVal.equals(actualVal)) { + return false; + } + } + return true; + } + + private static void describeRowMismatch( + TableRow expected, TableRow actual, Description mismatch) { + + // size mismatch + if (actual.size() != expected.size()) { + mismatch.appendText( + String.format( + "had %d field(s) %s but expected %d field(s) %s", + actual.size(), actual.keySet(), expected.size(), expected.keySet())); + return; + } + + // missing field + for (String key : expected.keySet()) { + if (!actual.containsKey(key)) { + mismatch.appendText(String.format("missing field '%s'", key)); + return; + } + } + + // value/type mismatch + for (String key : expected.keySet()) { + Object expectedVal = expected.get(key); + Object actualVal = actual.get(key); + + if (expectedVal == null && actualVal == null) { + continue; + } + ; + + if (expectedVal == null) { + mismatch.appendText( + String.format( + "field '%s': expected null but was %s(%s)", + key, actualVal.getClass().getSimpleName(), formatValue(actualVal, 0))); + return; + } + + if (actualVal == null) { + mismatch.appendText( + String.format( + "field '%s': expected %s(%s) but was null", + key, expectedVal.getClass().getSimpleName(), formatValue(expectedVal, 0))); + return; + } + + // recurse into nested TableRows + if (expectedVal instanceof TableRow && actualVal instanceof TableRow) { + if (!rowsMatch((TableRow) expectedVal, (TableRow) actualVal)) { + mismatch.appendText(String.format("field '%s': ", key)); + describeRowMismatch((TableRow) expectedVal, (TableRow) actualVal, mismatch); + return; + } + continue; + } + + // type mismatch + if (!expectedVal.getClass().equals(actualVal.getClass())) { + mismatch.appendText( + String.format( + "field '%s': expected %s(%s) but was %s(%s)", + key, + expectedVal.getClass().getSimpleName(), + formatValue(expectedVal, 0), + actualVal.getClass().getSimpleName(), + formatValue(actualVal, 0))); + return; + } + + // value mismatch + if (!expectedVal.equals(actualVal)) { + mismatch.appendText( + String.format( + "field '%s': expected value (%s) but was (%s)", + key, formatValue(expectedVal, 0), formatValue(actualVal, 0))); + return; + } + } + } + + // recursive formatter + private static String formatValue(Object val, int depth) { + if (val == null) { + return "null"; + } + + // safety net against infinite recursion + if (depth > 10) { + return "..."; + } + if (val instanceof TableRow) { + TableRow row = (TableRow) val; + String fields = + row.keySet().stream() + .map( + k -> { + Object v = row.get(k); + String typeName = + v == null + ? "null" + : v instanceof TableRow ? "TableRow" : v.getClass().getSimpleName(); + return String.format("%s(%s)=%s", k, typeName, formatValue(v, depth + 1)); + }) + .collect(Collectors.joining(", ")); + return "TableRow{" + fields + "}"; + } + return String.valueOf(val); + } +} From 816f00b0c6a8399d8322cec50eadcc07c08647f7 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Mon, 4 May 2026 16:41:41 -0400 Subject: [PATCH 067/490] [ValueKind] Add to model (#38308) * add valuekind to model * add unspecified first value --- .../model/fn_execution/v1/beam_fn_api.proto | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto index 22b19ef03289..ecef3f2e7a94 100644 --- a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto +++ b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto @@ -757,6 +757,29 @@ message Elements { } } + // The type of change operation represented by a Change Data Capture (CDC) record + message ValueKind { + enum Enum { + // Unspecified ValueKind. Will be treated as an INSERT + VALUE_KIND_UNSPECIFIED = 0; + + // Indicates a new record was created in the source system. + INSERT = 1; + + // Indicates the state of a record immediately before an update occurred. + // This is typically used to identify the previous values of modified columns + // or to locate the record via its primary key. + UPDATE_BEFORE = 2; + + // Indicates the state of a record immediately after an update occurred. + // Represents the current, valid state of the record following the change. + UPDATE_AFTER = 3; + + // Indicates that an existing record was removed from the source system. + DELETE = 4; + } + } + // Element metadata passed as part of WindowedValue to make WindowedValue // extensible and backward compatible message ElementMetadata { @@ -770,6 +793,9 @@ message Elements { // across IOs - Kafka, PubSub, http. // Example value: congo=t61rcWkgMzE optional string tracestate = 3; + // (Optional) The kind of value for CDC metadata. + // If missing or unspecified, implies INSERT for backwards compatibility. + optional ValueKind.Enum value_kind = 4; } // Represent the encoded user timer for a given instruction, transform and From 0a340dd47debec2b8b8785f9013d4ba63b56f1bb Mon Sep 17 00:00:00 2001 From: Asish Kumar <87874775+officialasishkumar@users.noreply.github.com> Date: Tue, 5 May 2026 02:59:02 +0530 Subject: [PATCH 068/490] Allow Beam Python GCP extra to resolve with google-cloud-storage 3.x (#38135) --- sdks/python/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/setup.py b/sdks/python/setup.py index 237aa83c2bdb..b3fb98d8b0ef 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -530,7 +530,7 @@ def get_portability_package_data(): 'google-auth-httplib2>=0.1.0,<0.3.0', 'google-cloud-datastore>=2.0.0,<3', 'google-cloud-pubsub>=2.1.0,<3', - 'google-cloud-storage>=2.18.2,<3', + 'google-cloud-storage>=2.18.2,<4', # GCP packages required by tests 'google-cloud-bigquery>=2.0.0,<4', 'google-cloud-bigquery-storage>=2.6.3,<3', From 15df34db485e24b51681e1d1bc5337c5b4c239e4 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Mon, 4 May 2026 19:35:56 -0400 Subject: [PATCH 069/490] [IcebergIO] Support hash distribution mode when writing rows (#38061) * group rows by partition before writing * add documentation; fix tests; extend to YAML * add BeamRowWrapper test class * lint * spotless * spotless * fix lint * add BeamRowWrapper javadoc; add an autosharding toggle option; address other comments * cleanup * cleanup * volatile map * add to changes.md * format * format --- .../IO_Iceberg_Integration_Tests.json | 2 +- CHANGES.md | 3 +- .../AssignDestinationsAndPartitions.java | 140 +++++++ .../beam/sdk/io/iceberg/BeamRowWrapper.java | 180 ++++++++ .../apache/beam/sdk/io/iceberg/IcebergIO.java | 73 +++- .../IcebergWriteSchemaTransformProvider.java | 28 ++ .../beam/sdk/io/iceberg/RecordWriter.java | 3 +- .../iceberg/WritePartitionedRowsToFiles.java | 263 ++++++++++++ .../sdk/io/iceberg/WriteToPartitions.java | 118 ++++++ .../sdk/io/iceberg/BeamRowWrapperTest.java | 226 ++++++++++ .../sdk/io/iceberg/IcebergIOWriteTest.java | 390 +++++++++++++++++- ...ebergWriteSchemaTransformProviderTest.java | 50 ++- .../iceberg/catalog/IcebergCatalogBaseIT.java | 28 ++ sdks/python/apache_beam/yaml/yaml_io.py | 8 +- 14 files changed, 1483 insertions(+), 29 deletions(-) create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapper.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java create mode 100644 sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapperTest.java diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index 5d04b2c0a8c7..b73af5e61a43 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 5 + "modification": 1 } diff --git a/CHANGES.md b/CHANGES.md index 8574448d0898..f9b9f1d28483 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -66,6 +66,7 @@ * Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * IcebergIO: support declaring a table's sort order on dynamic table creation via the new `sort_fields` config ([#38269](https://github.com/apache/beam/issues/38269)). +* IcebergIO: support writing with hash distribution mode, and with autosharding ([#38061](https://github.com/apache/beam/issues/38061))). ## New Features / Improvements @@ -2434,4 +2435,4 @@ Schema Options, it will be removed in version `2.23.0`. ([BEAM-9704](https://iss ## Highlights -- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). \ No newline at end of file +- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java new file mode 100644 index 000000000000..475786d3a4f6 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.util.HashMap; +import java.util.Map; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.iceberg.PartitionKey; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; + +/** + * Assigns destination metadata for each input record. + * + *

    The output will have the format { {destination, partition}, data } + */ +class AssignDestinationsAndPartitions + extends PTransform, PCollection>> { + + private final DynamicDestinations dynamicDestinations; + private final IcebergCatalogConfig catalogConfig; + static final String DESTINATION = "destination"; + static final String PARTITION = "partition"; + static final org.apache.beam.sdk.schemas.Schema OUTPUT_SCHEMA = + org.apache.beam.sdk.schemas.Schema.builder() + .addStringField(DESTINATION) + .addStringField(PARTITION) + .build(); + + public AssignDestinationsAndPartitions( + DynamicDestinations dynamicDestinations, IcebergCatalogConfig catalogConfig) { + this.dynamicDestinations = dynamicDestinations; + this.catalogConfig = catalogConfig; + } + + @Override + public PCollection> expand(PCollection input) { + return input + .apply(ParDo.of(new AssignDoFn(dynamicDestinations, catalogConfig))) + .setCoder( + KvCoder.of( + RowCoder.of(OUTPUT_SCHEMA), RowCoder.of(dynamicDestinations.getDataSchema()))); + } + + static class AssignDoFn extends DoFn> { + private transient @MonotonicNonNull Map partitionKeys; + private transient @MonotonicNonNull Map wrappers; + private final DynamicDestinations dynamicDestinations; + private final IcebergCatalogConfig catalogConfig; + + AssignDoFn(DynamicDestinations dynamicDestinations, IcebergCatalogConfig catalogConfig) { + this.dynamicDestinations = dynamicDestinations; + this.catalogConfig = catalogConfig; + } + + @Setup + public void setup() { + this.wrappers = new HashMap<>(); + this.partitionKeys = new HashMap<>(); + } + + @ProcessElement + public void processElement( + @Element Row element, + BoundedWindow window, + PaneInfo paneInfo, + @Timestamp Instant timestamp, + OutputReceiver> out) { + String tableIdentifier = + dynamicDestinations.getTableStringIdentifier( + ValueInSingleWindow.of(element, timestamp, window, paneInfo)); + Row data = dynamicDestinations.getData(element); + + @Nullable PartitionKey partitionKey = checkStateNotNull(partitionKeys).get(tableIdentifier); + @Nullable BeamRowWrapper wrapper = checkStateNotNull(wrappers).get(tableIdentifier); + if (partitionKey == null || wrapper == null) { + PartitionSpec spec = PartitionSpec.unpartitioned(); + Schema schema = IcebergUtils.beamSchemaToIcebergSchema(data.getSchema()); + @Nullable + IcebergTableCreateConfig createConfig = + dynamicDestinations.instantiateDestination(tableIdentifier).getTableCreateConfig(); + if (createConfig != null && createConfig.getPartitionFields() != null) { + spec = + PartitionUtils.toPartitionSpec(createConfig.getPartitionFields(), data.getSchema()); + } else { + try { + // see if table already exists with a spec + // TODO(https://github.com/apache/beam/issues/38337): improve this by periodically + // refreshing the table to fetch updated specs + spec = catalogConfig.catalog().loadTable(TableIdentifier.parse(tableIdentifier)).spec(); + } catch (NoSuchTableException ignored) { + // no partition to apply + } + } + partitionKey = new PartitionKey(spec, schema); + wrapper = new BeamRowWrapper(data.getSchema(), schema.asStruct()); + checkStateNotNull(partitionKeys).put(tableIdentifier, partitionKey); + checkStateNotNull(wrappers).put(tableIdentifier, wrapper); + } + partitionKey.partition(wrapper.wrap(data)); + String partitionPath = partitionKey.toPath(); + + Row destAndPartition = + Row.withSchema(OUTPUT_SCHEMA).addValues(tableIdentifier, partitionPath).build(); + out.output(KV.of(destAndPartition, data)); + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapper.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapper.java new file mode 100644 index 000000000000..4ab2b5b931be --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapper.java @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.schemas.logicaltypes.Date; +import org.apache.beam.sdk.schemas.logicaltypes.DateTime; +import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; +import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.Time; +import org.apache.beam.sdk.values.Row; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.types.Type; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.DateTimeUtil; +import org.apache.iceberg.util.UUIDUtil; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A wrapper that adapts a Beam {@link Row} to Iceberg's {@link StructLike} interface. + * + *

    This class allows Beam rows to be processed by Iceberg internal components (like partition + * keys or writers) without requiring a full conversion into Iceberg's internal Record format. It + * handles the mapping between Beam's {@link Schema} and Iceberg's {@link Types.StructType}, + * including complex type conversions for timestamps, logical types, and UUIDs. + * + *

    Note: This implementation is read-only. Calls to {@link #set(int, Object)} will + * throw an {@link UnsupportedOperationException}. + */ +public class BeamRowWrapper implements StructLike { + + private final FieldType[] types; + private final @Nullable PositionalGetter[] getters; + private @Nullable Row row = null; + + /** Constructs a new wrapper and pre-computes the mapping between Beam and Iceberg fields. */ + public BeamRowWrapper(Schema schema, Types.StructType struct) { + int size = schema.getFieldCount(); + + types = (FieldType[]) Array.newInstance(FieldType.class, size); + getters = (PositionalGetter[]) Array.newInstance(PositionalGetter.class, size); + + for (int i = 0; i < size; i++) { + types[i] = schema.getField(i).getType(); + getters[i] = buildGetter(types[i], struct.fields().get(i).type()); + } + } + + /** + * Sets the current Beam {@link Row} to be wrapped. This method allows the wrapper to be reused + * across different rows to minimize object allocation. + */ + public BeamRowWrapper wrap(@Nullable Row row) { + this.row = row; + return this; + } + + @Override + public int size() { + return types.length; + } + + /** + * Retrieves a field value from the wrapped row, performing any necessary type conversion to match + * Iceberg's internal expectations (e.g., converting Timestamps to microseconds). + */ + @Override + public @Nullable T get(int pos, Class javaClass) { + if (row == null || row.getValue(pos) == null) { + return null; + } else if (getters[pos] != null) { + return javaClass.cast(getters[pos].get(checkStateNotNull(row), pos)); + } + + return javaClass.cast(checkStateNotNull(row).getValue(pos)); + } + + @Override + public void set(int pos, T value) { + throw new UnsupportedOperationException( + "Could not set a field in the BeamRowWrapper because rowData is read-only"); + } + + private interface PositionalGetter { + T get(Row data, int pos); + } + + /** + * Factory method to create a getter that handles type-specific conversion logic. + * + *

    Handles special cases: + * + *

      + *
    • UUID: Converts {@code byte[]} to Iceberg's UUID representation. + *
    • DateTime: Converts Beam {@code DateTime} or logical types to microsecond timestamps. + *
    • Nested Rows: Recursively wraps nested structures in a new {@code BeamRowWrapper}. + *
    + */ + private static @Nullable PositionalGetter buildGetter(FieldType beamType, Type icebergType) { + switch (beamType.getTypeName()) { + case BYTE: + return Row::getByte; + case INT16: + return Row::getInt16; + case STRING: + return Row::getString; + case BYTES: + return (row, pos) -> { + byte[] bytes = checkStateNotNull(row.getBytes(pos)); + if (Type.TypeID.UUID == icebergType.typeId()) { + return UUIDUtil.convert(bytes); + } else { + return ByteBuffer.wrap(bytes); + } + }; + case DECIMAL: + return Row::getDecimal; + case DATETIME: + return (row, pos) -> + TimeUnit.MILLISECONDS.toMicros(checkStateNotNull(row.getDateTime(pos)).getMillis()); + case ROW: + Schema beamSchema = checkStateNotNull(beamType.getRowSchema()); + Types.StructType structType = (Types.StructType) icebergType; + + BeamRowWrapper nestedWrapper = new BeamRowWrapper(beamSchema, structType); + return (row, pos) -> nestedWrapper.wrap(row.getRow(pos)); + case LOGICAL_TYPE: + if (beamType.isLogicalType(MicrosInstant.IDENTIFIER)) { + return (row, pos) -> { + Instant instant = checkStateNotNull(row.getLogicalTypeValue(pos, Instant.class)); + return TimeUnit.SECONDS.toMicros(instant.getEpochSecond()) + instant.getNano() / 1000; + }; + } else if (beamType.isLogicalType(DateTime.IDENTIFIER)) { + return (row, pos) -> + DateTimeUtil.microsFromTimestamp( + checkStateNotNull(row.getLogicalTypeValue(pos, LocalDateTime.class))); + } else if (beamType.isLogicalType(Date.IDENTIFIER)) { + return (row, pos) -> + DateTimeUtil.daysFromDate( + checkStateNotNull(row.getLogicalTypeValue(pos, LocalDate.class))); + } else if (beamType.isLogicalType(Time.IDENTIFIER)) { + return (row, pos) -> + DateTimeUtil.microsFromTime( + checkStateNotNull(row.getLogicalTypeValue(pos, LocalTime.class))); + } else if (beamType.isLogicalType(FixedPrecisionNumeric.IDENTIFIER)) { + return (row, pos) -> row.getLogicalTypeValue(pos, BigDecimal.class); + } else { + return null; + } + default: + return null; + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java index 1d71ad549094..a5a3beef8f51 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java @@ -31,6 +31,7 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; import org.apache.iceberg.catalog.TableIdentifier; @@ -381,7 +382,11 @@ public class IcebergIO { public static WriteRows writeRows(IcebergCatalogConfig catalog) { - return new AutoValue_IcebergIO_WriteRows.Builder().setCatalogConfig(catalog).build(); + return new AutoValue_IcebergIO_WriteRows.Builder() + .setCatalogConfig(catalog) + .setDistributionMode(DistributionMode.NONE) + .setAutoSharding(false) + .build(); } @AutoValue @@ -397,6 +402,10 @@ public abstract static class WriteRows extends PTransform, Iceb abstract @Nullable Integer getDirectWriteByteLimit(); + abstract DistributionMode getDistributionMode(); + + abstract boolean getAutoSharding(); + abstract Builder toBuilder(); @AutoValue.Builder @@ -411,6 +420,10 @@ abstract static class Builder { abstract Builder setDirectWriteByteLimit(Integer directWriteByteLimit); + abstract Builder setDistributionMode(DistributionMode mode); + + abstract Builder setAutoSharding(boolean autoSharding); + abstract WriteRows build(); } @@ -443,6 +456,24 @@ public WriteRows withDirectWriteByteLimit(Integer directWriteByteLimit) { return toBuilder().setDirectWriteByteLimit(directWriteByteLimit).build(); } + /** + * Defines distribution of write data. Supported distributions: + * + *
      + *
    1. {@link DistributionMode.NONE}: don't shuffle rows (default) + *
    2. {@link DistributionMode.HASH}: shuffle rows by partition key before writing data + *
    + * + * {@link DistributionMode.RANGE} is not supported yet + */ + public WriteRows withDistributionMode(DistributionMode mode) { + return toBuilder().setDistributionMode(mode).build(); + } + + public WriteRows withAutosharding() { + return toBuilder().setAutoSharding(true).build(); + } + @Override public IcebergWriteResult expand(PCollection input) { List allToArgs = Arrays.asList(getTableIdentifier(), getDynamicDestinations()); @@ -464,15 +495,37 @@ public IcebergWriteResult expand(PCollection input) { IcebergUtils.isUnbounded(input), "Must only provide direct write limit for unbounded pipelines."); } - return input - .apply("Assign Table Destinations", new AssignDestinations(destinations)) - .apply( - "Write Rows to Destinations", - new WriteToDestinations( - getCatalogConfig(), - destinations, - getTriggeringFrequency(), - getDirectWriteByteLimit())); + + switch (getDistributionMode()) { + case NONE: + Preconditions.checkArgument( + !getAutoSharding(), + "Autosharding option is only available with " + "'hash' distribution mode."); + return input + .apply("Assign Table Destinations", new AssignDestinations(destinations)) + .apply( + "Write Rows to Destinations", + new WriteToDestinations( + getCatalogConfig(), + destinations, + getTriggeringFrequency(), + getDirectWriteByteLimit())); + case HASH: + return input + .apply( + "AssignDestinationAndPartition", + new AssignDestinationsAndPartitions(destinations, getCatalogConfig())) + .apply( + "Write Rows to Partitions", + new WriteToPartitions( + getCatalogConfig(), + destinations, + getTriggeringFrequency(), + getAutoSharding())); + default: + throw new UnsupportedOperationException( + "Unsupported distribution mode: " + getDistributionMode()); + } } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java index b76ede93a16f..8db4fb77a8e8 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java @@ -42,6 +42,7 @@ import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.FileFormat; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; @@ -144,6 +145,19 @@ public static Builder builder() { + "For more information on sort orders, please visit https://iceberg.apache.org/spec/#sort-orders.") public abstract @Nullable List getSortFields(); + @SchemaFieldDescription( + "Defines distribution of write data. Supported distributions:" + + "\n- none: don't shuffle rows (default)" + + "\n- hash: shuffle rows by partition key before writing data") + public abstract @Nullable String getDistributionMode(); + + @SchemaFieldDescription( + "Enables dynamic sharding to automatically adjust the number of parallel writers " + + "based on data volume. It handles data skew " + + "by further sub-dividing partitions into multiple shards to prevent bottlenecks " + + "during high-throughput writes. Only available with 'hash' distribution mode.") + public abstract @Nullable Boolean getAutosharding(); + @AutoValue.Builder public abstract static class Builder { public abstract Builder setTable(String table); @@ -170,6 +184,10 @@ public abstract static class Builder { public abstract Builder setSortFields(List sortFields); + public abstract Builder setDistributionMode(String mode); + + public abstract Builder setAutosharding(Boolean autosharding); + public abstract Configuration build(); } @@ -251,6 +269,16 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { writeTransform = writeTransform.withDirectWriteByteLimit(directWriteByteLimit); } + @Nullable String mode = configuration.getDistributionMode(); + if (mode != null) { + writeTransform = writeTransform.withDistributionMode(DistributionMode.fromName(mode)); + } + + @Nullable Boolean autoSharding = configuration.getAutosharding(); + if (autoSharding != null && autoSharding) { + writeTransform = writeTransform.withAutosharding(); + } + // TODO: support dynamic destinations IcebergWriteResult result = rows.apply(writeTransform); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java index 82251c00e72e..fd3d5d63327c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java @@ -23,6 +23,7 @@ import org.apache.iceberg.DataFile; import org.apache.iceberg.FileFormat; import org.apache.iceberg.PartitionKey; +import org.apache.iceberg.StructLike; import org.apache.iceberg.Table; import org.apache.iceberg.avro.Avro; import org.apache.iceberg.catalog.Catalog; @@ -56,7 +57,7 @@ class RecordWriter { partitionKey); } - RecordWriter(Table table, FileFormat fileFormat, String filename, PartitionKey partitionKey) + RecordWriter(Table table, FileFormat fileFormat, String filename, StructLike partitionKey) throws IOException { this.table = table; this.fileFormat = fileFormat; diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java new file mode 100644 index 000000000000..54ad120f1aca --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.io.iceberg.AssignDestinationsAndPartitions.DESTINATION; +import static org.apache.beam.sdk.io.iceberg.AssignDestinationsAndPartitions.PARTITION; +import static org.apache.beam.sdk.io.iceberg.RecordWriterManager.getPartitionDataPath; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.coders.IterableCoder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.PartitionField; +import org.apache.iceberg.PartitionKey; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class WritePartitionedRowsToFiles + extends PTransform>>, PCollection> { + private static final Logger LOG = LoggerFactory.getLogger(WritePartitionedRowsToFiles.class); + private final DynamicDestinations dynamicDestinations; + private final IcebergCatalogConfig catalogConfig; + private final String filePrefix; + + WritePartitionedRowsToFiles( + IcebergCatalogConfig catalogConfig, + DynamicDestinations dynamicDestinations, + String filePrefix) { + this.catalogConfig = catalogConfig; + this.dynamicDestinations = dynamicDestinations; + this.filePrefix = filePrefix; + } + + @Override + public PCollection expand(PCollection>> input) { + Schema dataSchema = + ((RowCoder) + ((IterableCoder) + ((KvCoder>) input.getCoder()).getValueCoder()) + .getElemCoder()) + .getSchema(); + return input.apply( + ParDo.of(new WriteDoFn(catalogConfig, dynamicDestinations, filePrefix, dataSchema))); + } + + private static class WriteDoFn extends DoFn>, FileWriteResult> { + + private final DynamicDestinations dynamicDestinations; + private final IcebergCatalogConfig catalogConfig; + private final String filePrefix; + private final Schema dataSchema; + static final Cache LAST_REFRESHED_TABLE_CACHE = + CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(); + + WriteDoFn( + IcebergCatalogConfig catalogConfig, + DynamicDestinations dynamicDestinations, + String filePrefix, + Schema dataSchema) { + this.catalogConfig = catalogConfig; + this.dynamicDestinations = dynamicDestinations; + this.filePrefix = filePrefix; + this.dataSchema = dataSchema; + } + + @ProcessElement + public void processElement( + @Element KV> element, OutputReceiver out) + throws Exception { + String tableIdentifier = checkStateNotNull(element.getKey().getString(DESTINATION)); + String partitionPath = checkStateNotNull(element.getKey().getString(PARTITION)); + + IcebergDestination destination = dynamicDestinations.instantiateDestination(tableIdentifier); + LastRefreshedTable lastRefreshedTable = getOrCreateTable(destination, dataSchema); + Table table = lastRefreshedTable.table; + partitionPath = getPartitionDataPath(partitionPath, lastRefreshedTable.partitionFieldMap); + + StructLike partitionData = + table.spec().isPartitioned() + ? DataFiles.data(table.spec(), partitionPath) + : new PartitionKey(table.spec(), table.schema()); + + String fileName = + destination + .getFileFormat() + .addExtension(String.format("%s-%s", filePrefix, UUID.randomUUID())); + + RecordWriter writer = + new RecordWriter(table, destination.getFileFormat(), fileName, partitionData); + try { + for (Row row : element.getValue()) { + Record record = IcebergUtils.beamRowToIcebergRecord(table.schema(), row); + writer.write(record); + } + } finally { + writer.close(); + } + + SerializableDataFile sdf = SerializableDataFile.from(writer.getDataFile(), partitionPath); + out.output( + FileWriteResult.builder() + .setTableIdentifier(destination.getTableIdentifier()) + .setSerializableDataFile(sdf) + .build()); + } + + static final class LastRefreshedTable { + final Table table; + volatile Instant lastRefreshTime; + static final Duration STALENESS_THRESHOLD = Duration.ofMinutes(2); + private int specId; + volatile Map partitionFieldMap = Maps.newHashMap(); + + LastRefreshedTable(Table table, Instant lastRefreshTime) { + this.table = table; + this.specId = table.spec().specId(); + this.lastRefreshTime = lastRefreshTime; + for (PartitionField partitionField : table.spec().fields()) { + partitionFieldMap.put(partitionField.name(), partitionField); + } + } + + /** + * Refreshes the table metadata if it is considered stale (older than 2 minutes). + * + *

    This method first performs a non-synchronized check on the table's freshness. This + * provides a lock-free fast path that avoids synchronization overhead in the common case + * where the table does not need to be refreshed. If the table might be stale, it then enters + * a synchronized block to ensure that only one thread performs the refresh operation. + */ + void refreshIfStale() { + // Fast path: Avoid entering the synchronized block if the table is not stale. + if (lastRefreshTime.isAfter(Instant.now().minus(STALENESS_THRESHOLD))) { + return; + } + synchronized (this) { + if (lastRefreshTime.isBefore(Instant.now().minus(STALENESS_THRESHOLD))) { + table.refresh(); + lastRefreshTime = Instant.now(); + if (table.spec().specId() != this.specId) { + partitionFieldMap = Maps.newHashMap(); + for (PartitionField partitionField : table.spec().fields()) { + partitionFieldMap.put(partitionField.name(), partitionField); + } + this.specId = table.spec().specId(); + } + } + } + } + } + + LastRefreshedTable getOrCreateTable(IcebergDestination destination, Schema dataSchema) { + TableIdentifier identifier = destination.getTableIdentifier(); + @Nullable + LastRefreshedTable lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier); + if (lastRefreshedTable != null) { + lastRefreshedTable.refreshIfStale(); + return lastRefreshedTable; + } + + Namespace namespace = identifier.namespace(); + @Nullable IcebergTableCreateConfig createConfig = destination.getTableCreateConfig(); + PartitionSpec partitionSpec = + createConfig != null ? createConfig.getPartitionSpec() : PartitionSpec.unpartitioned(); + Map tableProperties = + createConfig != null && createConfig.getTableProperties() != null + ? createConfig.getTableProperties() + : Maps.newHashMap(); + + @Nullable Table table = null; + synchronized (LAST_REFRESHED_TABLE_CACHE) { + lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier); + if (lastRefreshedTable != null) { + lastRefreshedTable.refreshIfStale(); + return lastRefreshedTable; + } + + Catalog catalog = catalogConfig.catalog(); + // Create namespace if it does not exist yet + if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { + SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; + if (!supportsNamespaces.namespaceExists(namespace)) { + try { + supportsNamespaces.createNamespace(namespace); + LOG.info("Created new namespace '{}'.", namespace); + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this namespace + LOG.info("Namespace `{}` already exists.", namespace); + } + } + } + + // If table exists, just load it + // Note: the implementation of catalog.tableExists() will load the table to check its + // existence. We don't use it here to avoid double loadTable() calls. + try { + table = catalog.loadTable(identifier); + } catch (NoSuchTableException e) { // Otherwise, create the table + org.apache.iceberg.Schema tableSchema = + IcebergUtils.beamSchemaToIcebergSchema(dataSchema); + try { + table = catalog.createTable(identifier, tableSchema, partitionSpec, tableProperties); + LOG.info( + "Created Iceberg table '{}' with schema: {}\n" + + ", partition spec: {}, table properties: {}", + identifier, + tableSchema, + partitionSpec, + tableProperties); + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this table + table = catalog.loadTable(identifier); + } + } + } + lastRefreshedTable = new LastRefreshedTable(table, Instant.now()); + LAST_REFRESHED_TABLE_CACHE.put(identifier, lastRefreshedTable); + return lastRefreshedTable; + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java new file mode 100644 index 000000000000..310fa1bede41 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; +import static org.apache.beam.sdk.values.TypeDescriptors.iterables; +import static org.apache.beam.sdk.values.TypeDescriptors.kvs; +import static org.apache.beam.sdk.values.TypeDescriptors.rows; + +import java.util.UUID; +import org.apache.beam.sdk.coders.IterableCoder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.transforms.GroupIntoBatches; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.windowing.AfterProcessingTime; +import org.apache.beam.sdk.transforms.windowing.GlobalWindows; +import org.apache.beam.sdk.transforms.windowing.Repeatedly; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; + +class WriteToPartitions extends PTransform>, IcebergWriteResult> { + private static final long DEFAULT_BYTES_PER_FILE = (1L << 29); // 512mb + private final IcebergCatalogConfig catalogConfig; + private final DynamicDestinations dynamicDestinations; + private final @Nullable Duration triggeringFrequency; + private final String filePrefix; + private final boolean autoSharding; + + WriteToPartitions( + IcebergCatalogConfig catalogConfig, + DynamicDestinations dynamicDestinations, + @Nullable Duration triggeringFrequency, + boolean autoSharding) { + this.dynamicDestinations = dynamicDestinations; + this.catalogConfig = catalogConfig; + this.triggeringFrequency = triggeringFrequency; + // single unique prefix per write transform + this.filePrefix = UUID.randomUUID().toString(); + this.autoSharding = autoSharding; + } + + private PCollection>> groupByPartition(PCollection> input) { + RowCoder destinationCoder = RowCoder.of(AssignDestinationsAndPartitions.OUTPUT_SCHEMA); + RowCoder dataCoder = RowCoder.of(dynamicDestinations.getDataSchema()); + + GroupIntoBatches groupIntoPartitions = + GroupIntoBatches.ofByteSize(DEFAULT_BYTES_PER_FILE); + if (IcebergUtils.isUnbounded(input) && triggeringFrequency != null) { + groupIntoPartitions = groupIntoPartitions.withMaxBufferingDuration(triggeringFrequency); + } + + if (autoSharding) { + return input + .apply(groupIntoPartitions.withShardedKey()) + .setCoder( + KvCoder.of( + org.apache.beam.sdk.util.ShardedKey.Coder.of(destinationCoder), + IterableCoder.of(dataCoder))) + .apply( + "DropShardId", + MapElements.into(kvs(rows(), iterables(rows()))) + .via(kv -> KV.of(kv.getKey().getKey(), kv.getValue()))) + .setCoder(KvCoder.of(destinationCoder, IterableCoder.of(dataCoder))); + } else { + return input + .apply(groupIntoPartitions) + .setCoder(KvCoder.of(destinationCoder, IterableCoder.of(dataCoder))); + } + } + + @Override + public IcebergWriteResult expand(PCollection> input) { + PCollection>> groupedRows = groupByPartition(input); + + PCollection writtenFiles = + groupedRows.apply( + new WritePartitionedRowsToFiles(catalogConfig, dynamicDestinations, filePrefix)); + + if (IcebergUtils.isUnbounded(input) && triggeringFrequency != null) { + writtenFiles = + writtenFiles.apply( + "ApplyUserTrigger", + Window.into(new GlobalWindows()) + .triggering( + Repeatedly.forever( + AfterProcessingTime.pastFirstElementInPane() + .plusDelayOf(checkArgumentNotNull(triggeringFrequency)))) + .discardingFiredPanes()); + } + + // Commit files to tables + PCollection> snapshots = + writtenFiles.apply(new AppendFilesToTables(catalogConfig, filePrefix)); + + return new IcebergWriteResult(input.getPipeline(), snapshots); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapperTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapperTest.java new file mode 100644 index 000000000000..bd8cead72987 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapperTest.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.logicaltypes.Date; +import org.apache.beam.sdk.schemas.logicaltypes.DateTime; +import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; +import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.Time; +import org.apache.beam.sdk.values.Row; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.DateTimeUtil; +import org.apache.iceberg.util.UUIDUtil; +import org.junit.Test; + +public class BeamRowWrapperTest { + private static final Schema NESTED_BEAM_SCHEMA = + Schema.builder().addInt32Field("nested_int").build(); + private static final Schema BEAM_SCHEMA = + Schema.builder() + .addByteField("byte_field") + .addInt16Field("int16_field") + .addStringField("string_field") + .addByteArrayField("bytes_field") + .addByteArrayField("uuid_field") + .addDecimalField("decimal_field") + .addDateTimeField("datetime_field") + .addLogicalTypeField("micros_instant_field", new MicrosInstant()) + .addLogicalTypeField("date_time_field", new DateTime()) + .addLogicalTypeField("date_field", new Date()) + .addLogicalTypeField("time_field", new Time()) + .addLogicalTypeField("fixed_numeric_field", FixedPrecisionNumeric.of(10, 2)) + .addRowField("row_field", NESTED_BEAM_SCHEMA) + .addInt32Field("pass_through_field") + .build(); + private static final Types.StructType ICEBERG_STRUCT = + Types.StructType.of( + Types.NestedField.required(1, "byte_field", Types.IntegerType.get()), + Types.NestedField.required(2, "int16_field", Types.IntegerType.get()), + Types.NestedField.required(3, "string_field", Types.StringType.get()), + Types.NestedField.required(4, "bytes_field", Types.BinaryType.get()), + Types.NestedField.required(5, "uuid_field", Types.UUIDType.get()), + Types.NestedField.required(6, "decimal_field", Types.DecimalType.of(10, 2)), + Types.NestedField.required(7, "datetime_field", Types.TimestampType.withZone()), + Types.NestedField.required(8, "micros_instant_field", Types.TimestampType.withZone()), + Types.NestedField.required(9, "date_time_field", Types.TimestampType.withoutZone()), + Types.NestedField.required(10, "date_field", Types.DateType.get()), + Types.NestedField.required(11, "time_field", Types.TimeType.get()), + Types.NestedField.required(12, "fixed_numeric_field", Types.DecimalType.of(10, 2)), + Types.NestedField.required( + 13, + "row_field", + Types.StructType.of( + Types.NestedField.required(1, "nested_int", Types.IntegerType.get()))), + Types.NestedField.required(14, "pass_through_field", Types.IntegerType.get())); + private static final UUID TEST_UUID = UUID.randomUUID(); + private static final Row NESTED_ROW = Row.withSchema(NESTED_BEAM_SCHEMA).addValue(999).build(); + private static final Row ROW = + Row.withSchema(BEAM_SCHEMA) + .addValues( + (byte) 42, + (short) 123, + "testString", + new byte[] {0x01, 0x02, 0x03}, + ByteBuffer.allocate(16) + .putLong(TEST_UUID.getMostSignificantBits()) + .putLong(TEST_UUID.getLeastSignificantBits()) + .array(), + new BigDecimal("123.45"), + org.joda.time.Instant.now(), + Instant.now(), + LocalDateTime.now(ZoneId.systemDefault()), + LocalDate.now(ZoneId.systemDefault()), + LocalTime.now(ZoneId.systemDefault()), + new BigDecimal("567.89"), + NESTED_ROW, + 888) + .build(); + private static final BeamRowWrapper WRAPPER = + new BeamRowWrapper(BEAM_SCHEMA, ICEBERG_STRUCT).wrap(ROW); + + @Test + public void testSize() { + assertEquals("Size should match the schema field count", 14, WRAPPER.size()); + } + + @Test + public void testUnsupportedSetThrowsException() { + assertThrows(UnsupportedOperationException.class, () -> WRAPPER.set(0, "test")); + } + + @Test + public void testNullRowHandling() { + BeamRowWrapper emptyWrapper = new BeamRowWrapper(BEAM_SCHEMA, ICEBERG_STRUCT); + assertNull( + "Should return null if the underlying row is null", emptyWrapper.get(0, Object.class)); + } + + @Test + public void testNullFieldHandling() { + Schema nullableSchema = Schema.builder().addNullableStringField("nullable_str").build(); + Types.StructType nullableIcebergType = + Types.StructType.of(Types.NestedField.optional(1, "nullable_str", Types.StringType.get())); + + Row nullRow = Row.withSchema(nullableSchema).addValue(null).build(); + BeamRowWrapper nullableWrapper = + new BeamRowWrapper(nullableSchema, nullableIcebergType).wrap(nullRow); + + assertNull("Should return null for a null field value", nullableWrapper.get(0, String.class)); + } + + // --- Type Conversion Tests --- + + @Test + public void testByteConversion() { + assertEquals(ROW.getByte(0), WRAPPER.get(0, Byte.class)); + } + + @Test + public void testInt16Conversion() { + assertEquals(ROW.getInt16(1), WRAPPER.get(1, Short.class)); + } + + @Test + public void testStringConversion() { + assertEquals(ROW.getString(2), WRAPPER.get(2, String.class)); + } + + @Test + public void testBytesToByteBufferConversion() { + assertEquals(ByteBuffer.wrap(ROW.getBytes(3)), WRAPPER.get(3, ByteBuffer.class)); + } + + @Test + public void testBytesToUUIDConversion() { + assertEquals(UUIDUtil.convert(ROW.getBytes(4)), WRAPPER.get(4, UUID.class)); + } + + @Test + public void testDecimalConversion() { + assertEquals(ROW.getDecimal(5), WRAPPER.get(5, BigDecimal.class)); + } + + @Test + public void testDateTimeConversion() { + long expectedJodaMicros = TimeUnit.MILLISECONDS.toMicros(ROW.getDateTime(6).getMillis()); + assertEquals(expectedJodaMicros, (long) WRAPPER.get(6, Long.class)); + } + + @Test + public void testMicrosInstantLogicalTypeConversion() { + Instant javaInstant = ROW.getLogicalTypeValue(7, Instant.class); + long expectedMicrosInstant = + TimeUnit.SECONDS.toMicros(javaInstant.getEpochSecond()) + javaInstant.getNano() / 1000; + assertEquals(expectedMicrosInstant, (long) WRAPPER.get(7, Long.class)); + } + + @Test + public void testDateTimeLogicalTypeConversion() { + long expectedDateTime = + DateTimeUtil.microsFromTimestamp(ROW.getLogicalTypeValue(8, LocalDateTime.class)); + assertEquals(expectedDateTime, (long) WRAPPER.get(8, Long.class)); + } + + @Test + public void testDateLogicalTypeConversion() { + int expectedDate = DateTimeUtil.daysFromDate(ROW.getLogicalTypeValue(9, LocalDate.class)); + assertEquals(expectedDate, (int) WRAPPER.get(9, Integer.class)); + } + + @Test + public void testTimeLogicalTypeConversion() { + long expectedTime = DateTimeUtil.microsFromTime(ROW.getLogicalTypeValue(10, LocalTime.class)); + assertEquals(expectedTime, (long) WRAPPER.get(10, Long.class)); + } + + @Test + public void testFixedPrecisionNumericLogicalTypeConversion() { + assertEquals(ROW.getLogicalTypeValue(11, BigDecimal.class), WRAPPER.get(11, BigDecimal.class)); + } + + @Test + public void testNestedRowConversion() { + StructLike nestedWrapperResult = WRAPPER.get(12, StructLike.class); + assertTrue( + "Should return a nested BeamRowWrapper", nestedWrapperResult instanceof BeamRowWrapper); + assertEquals(999, (int) nestedWrapperResult.get(0, Integer.class)); + } + + @Test + public void testPassThroughFallbackConversion() { + // Tests the 'default' case in the switch statement + assertEquals(ROW.getInt32(13), WRAPPER.get(13, Integer.class)); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java index a7349bffdfa0..52d92911f4e4 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java @@ -17,24 +17,54 @@ */ package org.apache.beam.sdk.io.iceberg; +import static java.util.Arrays.asList; import static org.apache.beam.sdk.io.iceberg.IcebergUtils.beamRowToIcebergRecord; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.sdk.values.TypeDescriptors.integers; +import static org.apache.beam.sdk.values.TypeDescriptors.kvs; +import static org.apache.beam.sdk.values.TypeDescriptors.strings; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import java.io.Serializable; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import org.apache.beam.sdk.PipelineResult; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.VarLongCoder; +import org.apache.beam.sdk.io.GenerateSequence; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.MetricNameFilter; +import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.metrics.MetricsFilter; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaCoder; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.testing.TestStream; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.Flatten; +import org.apache.beam.sdk.transforms.GroupByKey; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.Redistribute; +import org.apache.beam.sdk.transforms.Values; +import org.apache.beam.sdk.transforms.WithKeys; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionList; import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptors; import org.apache.beam.sdk.values.ValueInSingleWindow; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; @@ -44,7 +74,9 @@ import org.apache.iceberg.AppendFiles; import org.apache.iceberg.CatalogUtil; import org.apache.iceberg.DataFile; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.FileFormat; +import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.SupportsNamespaces; @@ -64,14 +96,26 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class IcebergIOWriteTest implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(IcebergIOWriteTest.class); + private static final String NONE = "none"; + private static final String HASH = "hash"; + private static final String HASH_WITH_AUTOSHARDING = "hashWithAutoSharding"; + + @Parameterized.Parameters + public static Iterable data() { + return asList(new Object[][] {{NONE}, {HASH}, {HASH_WITH_AUTOSHARDING}}); + } + + @Parameterized.Parameter(0) + public String distributionMode; + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); @Rule @@ -79,6 +123,28 @@ public class IcebergIOWriteTest implements Serializable { @Rule public transient TestPipeline testPipeline = TestPipeline.create(); + private IcebergIO.WriteRows writeTransform( + IcebergCatalogConfig catalog, TableIdentifier tableId) { + IcebergIO.WriteRows write = IcebergIO.writeRows(catalog).to(tableId); + return applyDistribution(write); + } + + private IcebergIO.WriteRows writeTransform( + IcebergCatalogConfig catalog, DynamicDestinations dynamicDestinations) { + IcebergIO.WriteRows write = IcebergIO.writeRows(catalog).to(dynamicDestinations); + return applyDistribution(write); + } + + private IcebergIO.WriteRows applyDistribution(IcebergIO.WriteRows write) { + if (distributionMode.contains(HASH)) { + write = write.withDistributionMode(DistributionMode.HASH); + } + if (distributionMode.equals(HASH_WITH_AUTOSHARDING)) { + write = write.withAutosharding(); + } + return write; + } + @Test public void testSimpleAppend() throws Exception { TableIdentifier tableId = @@ -99,7 +165,7 @@ public void testSimpleAppend() throws Exception { testPipeline .apply("Records To Add", Create.of(TestFixtures.asRows(TestFixtures.FILE1SNAPSHOT1))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(tableId)); + .apply("Append To Table", writeTransform(catalog, tableId)); LOG.info("Executing pipeline"); testPipeline.run().waitUntilFinish(); @@ -129,7 +195,7 @@ public void testCreateNamespaceAndTable() { testPipeline .apply("Records To Add", Create.of(TestFixtures.asRows(TestFixtures.FILE1SNAPSHOT1))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(tableId)); + .apply("Append To Table", writeTransform(catalog, tableId)); assertFalse(((SupportsNamespaces) catalog.catalog()).namespaceExists(newNamespace)); LOG.info("Executing pipeline"); @@ -200,7 +266,7 @@ public IcebergDestination instantiateDestination(String dest) { TestFixtures.FILE1SNAPSHOT2, TestFixtures.FILE1SNAPSHOT3)))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(dynamicDestinations)); + .apply("Append To Table", writeTransform(catalog, dynamicDestinations)); LOG.info("Executing pipeline"); testPipeline.run().waitUntilFinish(); @@ -293,7 +359,7 @@ public IcebergDestination instantiateDestination(String dest) { testPipeline .apply("Records To Add", Create.of(TestFixtures.asRows(elements))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(dynamicDestinations)); + .apply("Append To Table", writeTransform(catalog, dynamicDestinations)); LOG.info("Executing pipeline"); testPipeline.run().waitUntilFinish(); @@ -386,8 +452,7 @@ public void testStreamingWrite() { .apply("Stream Records", stream) .apply( "Append To Table", - IcebergIO.writeRows(catalog) - .to(tableId) + writeTransform(catalog, tableId) .withTriggeringFrequency(Duration.standardSeconds(3))) .getSnapshots(); // verify that 2 snapshots are created (one per triggering interval) @@ -400,4 +465,313 @@ public void testStreamingWrite() { List writtenRecords = ImmutableList.copyOf(IcebergGenerics.read(table).build()); assertThat(writtenRecords, Matchers.containsInAnyOrder(TestFixtures.FILE1SNAPSHOT1.toArray())); } + + @Test + public void testHashDistribution() { + assumeTrue(distributionMode.equals(HASH_WITH_AUTOSHARDING)); + Schema schema = Schema.builder().addInt64Field("id").addStringField("name").build(); + + TableIdentifier tableId = + TableIdentifier.of("default", "hash_" + Long.toString(UUID.randomUUID().hashCode(), 16)); + Map catalogProps = + ImmutableMap.builder() + .put("type", CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP) + .put("warehouse", warehouse.location) + .build(); + IcebergCatalogConfig catalog = + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties(catalogProps) + .build(); + + // create table with two partitions + catalog + .catalog() + .createTable( + tableId, + IcebergUtils.beamSchemaToIcebergSchema(schema), + PartitionSpec.builderFor(IcebergUtils.beamSchemaToIcebergSchema(schema)) + .bucket("id", 2) + .build()); + + // Prepare 100 rows and split them up into separate keys. + // The "none" distribution will process each key in a separate writer DoFn, + // essentially creating one file per parallel thread. This means one file per + // record since each record is in its own key. + // The "hash" distribution will group records by partition key first, resulting + // in a much smaller number of files created. + PCollection rows = + testPipeline + .apply(GenerateSequence.from(0).to(100)) + .apply( + "Make rows", + MapElements.into(TypeDescriptors.rows()) + .via(i -> Row.withSchema(schema).addValues(i, "name_" + i).build())) + .setRowSchema(schema) + .apply(WithKeys.of(1L)) + .setCoder(KvCoder.of(VarLongCoder.of(), SchemaCoder.of(schema))) + .apply(Redistribute.byKey()) + .apply(Values.create()); + + Function, KV>> getAddedFilesFunc = + (distribution) -> + MapElements.into(kvs(strings(), integers())) + .via( + snapshot -> + KV.of( + distribution, + Integer.parseInt( + checkStateNotNull(snapshot.getValue().getSummary()) + .get("added-data-files")))); + + // 1. Write files without any additional config + PCollection> noneDistributionAddedFiles = + rows.apply( + "none distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withDistributionMode(DistributionMode.NONE)) + .getSnapshots() + .apply("Get none files", getAddedFilesFunc.apply(NONE)); + // 2. Write files with hash distribution + PCollection> hashDistributionAddedFiles = + rows.apply( + "hash distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withDistributionMode(DistributionMode.HASH)) + .getSnapshots() + .apply("Get hash files", getAddedFilesFunc.apply(HASH)); + // 3. Write files with hash distribution AND auto-sharding + PCollection> hashAutoShardingDistributionAddedFiles = + rows.apply( + "hash distribution + autosharding write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withDistributionMode(DistributionMode.HASH) + .withAutosharding()) + .getSnapshots() + .apply("Get hash autosharded files", getAddedFilesFunc.apply(HASH_WITH_AUTOSHARDING)); + + PCollectionList.of( + Arrays.asList( + hashDistributionAddedFiles, + noneDistributionAddedFiles, + hashAutoShardingDistributionAddedFiles)) + .apply(Flatten.pCollections()) + .apply("add dummy key", WithKeys.of(1)) + .apply("group together", GroupByKey.create()) + .apply("unwrap values", Values.create()) + .apply( + "validate num files", + ParDo.of( + new DoFn>, Void>() { + @ProcessElement + public void process(@Element Iterable> sums) { + List> sumList = Lists.newArrayList(sums.iterator()); + assertEquals(3, sumList.size()); + + int numFilesAddedNoneDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(NONE)) + .map(KV::getValue) + .collect(Collectors.toList())); + + int numFilesAddedHashDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(HASH)) + .map(KV::getValue) + .collect(Collectors.toList())); + + int numFilesAddedHashAutoShardingDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(HASH_WITH_AUTOSHARDING)) + .map(KV::getValue) + .collect(Collectors.toList())); + + System.out.println("none: " + numFilesAddedNoneDist); + System.out.println("hash: " + numFilesAddedHashDist); + System.out.println( + "hash with autosharding: " + numFilesAddedHashAutoShardingDist); + // plain hash distribution should have exactly the same number of partitions + assertEquals(2, numFilesAddedHashDist); + // hash with autosharding may create sub-shards and lead to more than just 2 + // files. + // should still be less than 'none' distribution though + assertTrue(numFilesAddedHashDist < numFilesAddedNoneDist); + } + })); + + testPipeline.run().waitUntilFinish(); + } + + @Test + public void testHashDistributionStreaming() { + assumeTrue(distributionMode.equals(HASH_WITH_AUTOSHARDING)); + Schema schema = Schema.builder().addInt64Field("id").addStringField("name").build(); + + TableIdentifier tableId = + TableIdentifier.of( + "default", "hash_streaming" + Long.toString(UUID.randomUUID().hashCode(), 16)); + Map catalogProps = + ImmutableMap.builder() + .put("type", CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP) + .put("warehouse", warehouse.location) + .build(); + IcebergCatalogConfig catalog = + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties(catalogProps) + .build(); + + // create table with two partitions + catalog + .catalog() + .createTable( + tableId, + IcebergUtils.beamSchemaToIcebergSchema(schema), + PartitionSpec.builderFor(IcebergUtils.beamSchemaToIcebergSchema(schema)) + .bucket("id", 2) + .build()); + + // Prepare 100 rows and split them up into separate keys. + // The "none" distribution will process each key in a separate writer DoFn, + // essentially creating one file per parallel thread. This means one file per + // record since each record is in its own key. + // The "hash" distribution will group records by partition key first, resulting + // in a much smaller number of files created. + PCollection rows = + testPipeline + .apply( + TestStream.create(VarLongCoder.of()) + .addElements(0L, LongStream.range(1, 10).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(10L, LongStream.range(11, 20).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(20L, LongStream.range(21, 30).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(30L, LongStream.range(31, 40).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(40L, LongStream.range(41, 50).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .advanceWatermarkToInfinity()) + .apply( + "Make rows", + MapElements.into(TypeDescriptors.rows()) + .via(i -> Row.withSchema(schema).addValues(i, "name_" + i).build())) + .setRowSchema(schema) + .apply(WithKeys.of(r -> r.getInt64("id"))) + .setCoder(KvCoder.of(VarLongCoder.of(), SchemaCoder.of(schema))) + .apply(Redistribute.byKey()) + .apply(Values.create()); + + Function, KV>> getAddedFilesFunc = + (distribution) -> + MapElements.into(kvs(strings(), integers())) + .via( + snapshot -> + KV.of( + distribution, + Integer.parseInt( + checkStateNotNull(snapshot.getValue().getSummary()) + .get("added-data-files")))); + + // 1. Write files without any additional config + PCollection> noneDistributionAddedFiles = + rows.apply( + "none distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withTriggeringFrequency(Duration.standardSeconds(5)) + .withDistributionMode(DistributionMode.NONE)) + .getSnapshots() + .apply("Get none files", getAddedFilesFunc.apply(NONE)); + // 2. Write files with hash distribution + PCollection> hashDistributionAddedFiles = + rows.apply( + "hash distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withTriggeringFrequency(Duration.standardSeconds(5)) + .withDistributionMode(DistributionMode.HASH)) + .getSnapshots() + .apply("Get hash files", getAddedFilesFunc.apply(HASH)); + // 3. Write files with hash distribution AND auto-sharding + PCollection> hashAutoShardingDistributionAddedFiles = + rows.apply( + "hash distribution + autosharding write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withTriggeringFrequency(Duration.standardSeconds(5)) + .withDistributionMode(DistributionMode.HASH) + .withAutosharding()) + .getSnapshots() + .apply("Get hash autosharded files", getAddedFilesFunc.apply(HASH_WITH_AUTOSHARDING)); + + PCollectionList.of( + Arrays.asList( + hashDistributionAddedFiles, + noneDistributionAddedFiles, + hashAutoShardingDistributionAddedFiles)) + .apply(Flatten.pCollections()) + .apply("add dummy key", WithKeys.of(1)) + .apply("group together", GroupByKey.create()) + .apply( + "validate num files", + ParDo.of( + new DoFn>>, Void>() { + private final Counter numWaves = + Metrics.counter(IcebergIOWriteTest.class, "numWaves"); + + @ProcessElement + public void process(@Element KV>> sums) { + List> sumList = + Lists.newArrayList(sums.getValue().iterator()); + // each wave should have one snapshot per write branch + System.out.println("list: " + sumList); + assertEquals(3, sumList.size()); + + // get the number of files written by each branch + int numFilesAddedHashDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(HASH)) + .map(KV::getValue) + .collect(Collectors.toList())); + + // plain hash distribution should have exactly the same number of partitions + assertEquals(2, numFilesAddedHashDist); + // hash with autosharding may create sub-shards and lead to more than just 2 + // files. + // In a production runner like Dataflow, hash + autosharding would still + // make less files than 'none' distribution. + // We're testing with DirectRunner though, which doesn't have a smart + // autosharding implementation, so it may sometimes make more files + // than even 'none' distribution. + // assertTrue(numFilesAddedHashAutoShardingDist < numFilesAddedNoneDist); + numWaves.inc(); + } + })); + + PipelineResult result = testPipeline.run(); + result.waitUntilFinish(); + + // verify total number of snapshot commit waves + long numWaves = + result + .metrics() + .queryMetrics( + MetricsFilter.builder() + .addNameFilter(MetricNameFilter.named(IcebergIOWriteTest.class, "numWaves")) + .build()) + .getCounters() + .iterator() + .next() + .getCommitted(); + assertEquals(5L, numWaves); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java index 7028a394d2fd..c5fc5a6b6fe7 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.iceberg; +import static java.util.Arrays.asList; import static org.apache.beam.sdk.io.iceberg.IcebergWriteSchemaTransformProvider.Configuration; import static org.apache.beam.sdk.io.iceberg.IcebergWriteSchemaTransformProvider.INPUT_TAG; import static org.apache.beam.sdk.io.iceberg.IcebergWriteSchemaTransformProvider.SNAPSHOTS_TAG; @@ -24,6 +25,7 @@ import static org.apache.iceberg.util.DateTimeUtil.timestampFromMicros; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; import java.time.LocalDate; import java.time.LocalDateTime; @@ -56,6 +58,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; @@ -72,12 +75,19 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; import org.yaml.snakeyaml.Yaml; /** Tests for {@link IcebergWriteSchemaTransformProvider}. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class IcebergWriteSchemaTransformProviderTest { + @Parameterized.Parameters + public static Iterable data() { + return asList(new Object[][] {{DistributionMode.NONE}, {DistributionMode.HASH}}); + } + + @Parameterized.Parameter(0) + public DistributionMode distributionMode; @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); @@ -115,6 +125,7 @@ public void testSimpleAppend() { .setTable(identifier) .setCatalogName("name") .setCatalogProperties(properties) + .setDistributionMode(distributionMode.name()) .build(); PCollectionRowTuple input = @@ -151,10 +162,14 @@ public void testWriteUsingManagedTransform() { String.format( "table: %s\n" + "catalog_name: test-name\n" + + "distribution_mode: %s\n" + "catalog_properties: \n" + " type: %s\n" + " warehouse: %s", - identifier, CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP, warehouse.location); + identifier, + distributionMode.name(), + CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP, + warehouse.location); Map configMap = new Yaml().load(yamlConfig); PCollection inputRows = @@ -204,6 +219,7 @@ private void writeToDynamicDestinationsAndFilter(@Nullable String operation, boo ImmutableMap.builder() .put("table", destinationTemplate) .put("catalog_name", "test-name") + .put("distribution_mode", distributionMode.name()) .put( "catalog_properties", ImmutableMap.builder() @@ -372,8 +388,18 @@ public Void apply(Iterable input) { } } + @Test + public void testWritePartitionedDataWithAutosharding() { + assumeTrue(distributionMode.equals(DistributionMode.HASH)); + writePartitionedData(true); + } + @Test public void testWritePartitionedData() { + writePartitionedData(false); + } + + public void writePartitionedData(boolean autosharding) { Schema schema = Schema.builder() .addStringField("str") @@ -415,7 +441,11 @@ public void testWritePartitionedData() { "table", identifier, "catalog_properties", - ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)); + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), + "distribution_mode", + distributionMode.name(), + "autosharding", + autosharding); List rows = new ArrayList<>(); for (int i = 0; i < 30; i++) { @@ -491,7 +521,9 @@ public void testWriteCreateTableWithPartitionSpec() { "year(y_datetime)", "month(m_date)", "day(d_date)", - "hour(h_datetimetz)")); + "hour(h_datetimetz)"), + "distribution_mode", + distributionMode.name()); List rows = new ArrayList<>(); for (int i = 0; i < 30; i++) { @@ -563,7 +595,9 @@ public void testWriteCreateTableWithTablePropertiesSpec() { "catalog_properties", ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), "table_properties", - tableProperties); + tableProperties, + "distribution_mode", + distributionMode.name()); List rows = new ArrayList<>(); for (int i = 0; i < 10; i++) { @@ -622,7 +656,9 @@ public void testWriteCreateTableWithTableProperties() { "table", identifier, "catalog_properties", - ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)); + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), + "distribution_mode", + distributionMode.name()); PCollection result = testPipeline diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java index 606fb492e4ba..74408d67ed86 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java @@ -712,6 +712,32 @@ public void testWriteWithSortOrder() throws IOException { returnedRecords, containsInAnyOrder(inputRows.stream().map(RECORD_FUNC::apply).toArray())); } + @Test + public void testWriteToPartitionedTableWithHashDistribution() throws IOException { + Map config = new HashMap<>(managedIcebergConfig(tableId())); + int truncLength = "value_x".length(); + List partitionFields = + Arrays.asList("bool_field", "hour(datetime)", "truncate(str, " + truncLength + ")"); + config.put("partition_fields", partitionFields); + config.put("distribution_mode", "hash"); + PCollection input = pipeline.apply(Create.of(inputRows)).setRowSchema(BEAM_SCHEMA); + input.apply(Managed.write(ICEBERG).withConfig(config)); + pipeline.run().waitUntilFinish(); + + // Read back and check records are correct + Table table = catalog.loadTable(TableIdentifier.parse(tableId())); + List returnedRecords = readRecords(table); + PartitionSpec expectedSpec = + PartitionSpec.builderFor(table.schema()) + .identity("bool_field") + .hour("datetime") + .truncate("str", truncLength) + .build(); + assertEquals(expectedSpec, table.spec()); + assertThat( + returnedRecords, containsInAnyOrder(inputRows.stream().map(RECORD_FUNC::apply).toArray())); + } + private PeriodicImpulse getStreamingSource() { return PeriodicImpulse.create() .stopAfter(Duration.millis(numRecords() - 1)) @@ -824,6 +850,8 @@ private void writeToDynamicDestinations( if (partitioning) { Preconditions.checkState(filterOp == null || !filterOp.equals("only")); writeConfig.put("partition_fields", Arrays.asList("bool_field", "modulo_5")); + writeConfig.put("distribution_mode", "hash"); + writeConfig.put("autosharding", true); } // Write with Beam diff --git a/sdks/python/apache_beam/yaml/yaml_io.py b/sdks/python/apache_beam/yaml/yaml_io.py index f8702a1da209..336e32adc253 100644 --- a/sdks/python/apache_beam/yaml/yaml_io.py +++ b/sdks/python/apache_beam/yaml/yaml_io.py @@ -562,6 +562,7 @@ def write_to_iceberg( keep: Optional[Iterable[str]] = None, drop: Optional[Iterable[str]] = None, only: Optional[str] = None, + distribution_mode: Optional[str] = None, ): # TODO(robertwb): It'd be nice to derive this list of parameters, along with # their types and docs, programmatically from the iceberg (or managed) @@ -611,6 +612,10 @@ def write_to_iceberg( only: The name of exactly one field to keep as the top level record when writing to the destination. All other fields are dropped. This field must be of row type. Mutually exclusive with drop and keep. + distribution_mode: Defines distribution of write data. Supported + distributions: + - none: don't shuffle rows (default) + - hash: shuffle rows by partition key before writing data """ return beam.managed.Write( "iceberg", @@ -624,7 +629,8 @@ def write_to_iceberg( triggering_frequency_seconds=triggering_frequency_seconds, keep=keep, drop=drop, - only=only)) + only=only, + distribution_mode=distribution_mode)) def io_providers(): From cc06e31ecd9ecb95ffadf0a10028fad485ebd4d1 Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Tue, 2 Dec 2025 13:44:10 +0100 Subject: [PATCH 070/490] bump opentelemetry version. --- .../groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 3 ++- sdks/java/container/license_scripts/dep_urls_java.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 22655f696268..c3428bc1a62c 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -633,7 +633,8 @@ class BeamModulePlugin implements Plugin { // [bomupgrader] determined by: io.grpc:grpc-netty, consistent with: google_cloud_platform_libraries_bom def netty_version = "4.1.130.Final" // [bomupgrader] determined by: io.opentelemetry:opentelemetry-sdk, consistent with: google_cloud_platform_libraries_bom - def opentelemetry_version = "1.51.0" + def opentelemetry_version = "1.56.0" + def opentelemetry_contrib_version = "1.52.0" def postgres_version = "42.6.2" // [bomupgrader] determined by: com.google.protobuf:protobuf-java, consistent with: google_cloud_platform_libraries_bom def protobuf_version = "4.33.2" diff --git a/sdks/java/container/license_scripts/dep_urls_java.yaml b/sdks/java/container/license_scripts/dep_urls_java.yaml index 8b021e0347d4..725e70f227b7 100644 --- a/sdks/java/container/license_scripts/dep_urls_java.yaml +++ b/sdks/java/container/license_scripts/dep_urls_java.yaml @@ -66,12 +66,12 @@ org.eclipse.jgit: license: "https://www.eclipse.org/org/documents/edl-v10.html" type: "Eclipse Distribution License - v1.0" opentelemetry-bom: - '1.51.0': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.51.0/LICENSE" + '1.56.0': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" type: "Apache License 2.0" opentelemetry-bom-alpha: - '1.51.0-alpha': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.51.0/LICENSE" + '1.56.0-alpha': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" type: "Apache License 2.0" zstd-jni: '1.5.2-5': From 4b1f6555774832db45194cfc1a9d2b9706086fbd Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Tue, 14 Apr 2026 17:29:57 +0200 Subject: [PATCH 071/490] add libraries --- .../groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index c3428bc1a62c..362eef052438 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -861,6 +861,11 @@ class BeamModulePlugin implements Plugin { netty_transport_native_epoll : "io.netty:netty-transport-native-epoll:$netty_version", opentelemetry_api : "io.opentelemetry:opentelemetry-api", // google_cloud_platform_libraries_bom sets version opentelemetry_bom : "io.opentelemetry:opentelemetry-bom-alpha:$opentelemetry_version-alpha", // alpha required by extensions + opentelemetry_context : "io.opentelemetry:opentelemetry-context", // google_cloud_platform_libraries_bom sets version + opentelemetry_gcp_auth : "io.opentelemetry.contrib:opentelemetry-gcp-auth-extension:$opentelemetry_contrib_version-alpha", + opentelemetry_sdk : "io.opentelemetry:opentelemetry-sdk", // google_cloud_platform_libraries_bom sets version + opentelemetry_exporter_otlp : "io.opentelemetry:opentelemetry-exporter-otlp", // google_cloud_platform_libraries_bom sets version + opentelemetry_extension_autoconfigure : "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", // google_cloud_platform_libraries_bom sets version postgres : "org.postgresql:postgresql:$postgres_version", protobuf_java : "com.google.protobuf:protobuf-java:$protobuf_version", protobuf_java_util : "com.google.protobuf:protobuf-java-util:$protobuf_version", From 006475697401deb7fa0712f66ad9d19b74ea76eb Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Tue, 24 Mar 2026 17:00:45 +0100 Subject: [PATCH 072/490] Model changes to allow OpenTelemetry context propagation spotless. rename fields, cleanup groovy spotless rename method, fix constructor invocation. --- ...oundedSplittableProcessElementInvoker.java | 6 +- .../SplittableParDoViaKeyedWorkItems.java | 3 +- .../google-cloud-dataflow-java/build.gradle | 1 + .../runners/dataflow/BatchViewOverrides.java | 6 + .../worker/build.gradle | 1 + .../worker/UngroupedWindmillReader.java | 14 +- .../worker/WindmillKeyedWorkItem.java | 2 +- .../worker/util/ValueInEmptyWindows.java | 6 + runners/spark/spark_runner.gradle | 1 + .../beam/runners/spark/util/TimerUtils.java | 6 + sdks/java/core/build.gradle | 1 + .../beam/sdk/transforms/DoFnTester.java | 11 +- .../org/apache/beam/sdk/transforms/Reify.java | 3 +- .../OpenTelemetryContextPropagator.java | 71 +++++++ .../apache/beam/sdk/values/OutputBuilder.java | 3 + .../beam/sdk/values/ValueInSingleWindow.java | 37 +++- .../apache/beam/sdk/values/WindowedValue.java | 4 + .../beam/sdk/values/WindowedValues.java | 196 +++++++++++++----- .../beam/sdk/util/WindowedValueTest.java | 21 +- .../beam/fn/harness/FnApiDoFnRunner.java | 3 +- .../beam/sdk/io/gcp/GcpApiSurfaceTest.java | 1 + 21 files changed, 327 insertions(+), 70 deletions(-) create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java index 07e4756885dd..dc84b15c89f4 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java @@ -451,7 +451,8 @@ public void outputWithTimestamp(TupleTag tag, T value, Instant timestamp) element.getPaneInfo(), element.getRecordId(), element.getRecordOffset(), - element.causedByDrain())); + element.causedByDrain(), + element.getOpenTelemetryContext())); } @Override @@ -474,7 +475,8 @@ public void outputWindowedValue( paneInfo, element.getRecordId(), element.getRecordOffset(), - element.causedByDrain())); + element.causedByDrain(), + element.getOpenTelemetryContext())); } @Override diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java index 3519a74aada7..7a0f984479ab 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java @@ -472,7 +472,8 @@ public String getErrorContext() { read.getPaneInfo(), read.getRecordId(), read.getRecordOffset(), - CausedByDrain.CAUSED_BY_DRAIN); + CausedByDrain.CAUSED_BY_DRAIN, + read.getOpenTelemetryContext()); } elementAndRestriction = KV.of(read, restrictionState.read()); watermarkEstimatorStateT = watermarkEstimatorState.read(); diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index ef2ce36dc5ae..e4fce90f2e9a 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -112,6 +112,7 @@ dependencies { permitUnusedDeclared library.java.google_http_client_gson // BEAM-11761 implementation library.java.google_cloud_logging permitUnusedDeclared library.java.google_cloud_logging // BEAM-11761 + implementation library.java.opentelemetry_context implementation library.java.hamcrest implementation library.java.jackson_annotations implementation library.java.jackson_core diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java index d4cd1af386e3..e5ac4e99285d 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java @@ -20,6 +20,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -1404,6 +1405,11 @@ public PaneInfo getPaneInfo() { return null; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return null; + } + @Override public @Nullable Long getRecordOffset() { return null; diff --git a/runners/google-cloud-dataflow-java/worker/build.gradle b/runners/google-cloud-dataflow-java/worker/build.gradle index 610fa411459f..21879861e9d6 100644 --- a/runners/google-cloud-dataflow-java/worker/build.gradle +++ b/runners/google-cloud-dataflow-java/worker/build.gradle @@ -212,6 +212,7 @@ dependencies { implementation library.java.jackson_core implementation library.java.jackson_databind implementation library.java.joda_time + implementation library.java.opentelemetry_context implementation library.java.slf4j_api implementation library.java.vendored_grpc_1_69_0 implementation library.java.error_prone_annotations diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java index 2347529cf4a5..8ef0bf80323a 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java @@ -155,10 +155,19 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce @SuppressWarnings("unchecked") T result = (T) KV.of(decode(kvCoder.getKeyCoder(), key), decode(kvCoder.getValueCoder(), data)); + // todo #37030 parse context from previous stage return WindowedValues.of( - result, timestampMillis, windows, paneInfo, null, null, drainingValueFromUpstream); + result, + timestampMillis, + windows, + paneInfo, + null, + null, + drainingValueFromUpstream, + null); } else { notifyElementRead(data.available() + metadata.available()); + // todo #37030 parse context from previous stage return WindowedValues.of( decode(valueCoder, data), timestampMillis, @@ -166,7 +175,8 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce paneInfo, null, null, - drainingValueFromUpstream); + drainingValueFromUpstream, + null); } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java index c328719bfb50..4c968030eef5 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java @@ -159,7 +159,7 @@ public Iterable timersIterable() { InputStream inputStream = message.getData().newInput(); ElemT value = valueCoder.decode(inputStream, Coder.Context.OUTER); return WindowedValues.of( - value, timestamp, windows, paneInfo, null, null, drainingValueFromUpstream); + value, timestamp, windows, paneInfo, null, null, drainingValueFromUpstream, null); } catch (RuntimeException | IOException e) { if (!skipUndecodableElements) { throw new RuntimeException(e); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java index 2f7a5ce54fbf..67ffdc9f3b8b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java @@ -17,6 +17,7 @@ */ package org.apache.beam.runners.dataflow.worker.util; +import io.opentelemetry.context.Context; import java.util.Collection; import java.util.Collections; import java.util.Objects; @@ -65,6 +66,11 @@ public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return null; + } + @Override public Iterable> explodeWindows() { return Collections.emptyList(); diff --git a/runners/spark/spark_runner.gradle b/runners/spark/spark_runner.gradle index 0e77821e533e..9d7be01e8825 100644 --- a/runners/spark/spark_runner.gradle +++ b/runners/spark/spark_runner.gradle @@ -243,6 +243,7 @@ dependencies { implementation library.java.jackson_annotations implementation library.java.slf4j_api implementation library.java.joda_time + implementation library.java.opentelemetry_context implementation library.java.commons_lang3 implementation library.java.args4j implementation project(path: ":model:fn-execution", configuration: "shadow") diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java index 4bc01bd205a9..b6dda82239ca 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java @@ -17,6 +17,7 @@ */ package org.apache.beam.runners.spark.util; +import io.opentelemetry.context.Context; import java.io.Serializable; import java.util.Collection; import java.util.Collections; @@ -116,6 +117,11 @@ public PaneInfo getPaneInfo() { return null; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return null; + } + @Override public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index 3b2cbc81f08a..f532e9d14166 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -98,6 +98,7 @@ dependencies { shadow library.java.jackson_databind shadow platform(library.java.opentelemetry_bom) shadow library.java.opentelemetry_api + shadow library.java.opentelemetry_context shadow library.java.slf4j_api shadow library.java.snappy_java shadow library.java.joda_time diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java index 4de2c3d2c9c0..05d7b7b0d920 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java @@ -502,7 +502,8 @@ public void output(TupleTag tag, T output, Instant timestamp, BoundedWind PaneInfo.NO_FIRING, null, null, - CausedByDrain.NORMAL)); + CausedByDrain.NORMAL, + null)); } }; } @@ -646,7 +647,8 @@ public void outputWithTimestamp(TupleTag tag, T output, Instant timestamp element.getPaneInfo(), null, null, - CausedByDrain.NORMAL)); + CausedByDrain.NORMAL, + element.getOpenTelemetryContext())); } @Override @@ -666,7 +668,8 @@ public void outputWindowedValue(TupleTag tag, WindowedValue windowedVa windowedValue.getPaneInfo(), windowedValue.getRecordId(), windowedValue.getRecordOffset(), - windowedValue.causedByDrain())); + windowedValue.causedByDrain(), + windowedValue.getOpenTelemetryContext())); } } @@ -681,7 +684,7 @@ public void outputWindowedValue( getMutableOutput(tag) .add( ValueInSingleWindow.of( - output, timestamp, w, paneInfo, null, null, CausedByDrain.NORMAL)); + output, timestamp, w, paneInfo, null, null, CausedByDrain.NORMAL, null)); } } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java index 9974d29bfa0f..1e3203744d05 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java @@ -159,7 +159,8 @@ public void processElement( paneInfo, pc.currentRecordId(), pc.currentRecordOffset(), - causedByDrain))); + causedByDrain, + null))); } })) .setCoder( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java new file mode 100644 index 000000000000..20120bf2172d --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.values; + +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.beam.model.fnexecution.v1.BeamFnApi; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.checkerframework.checker.nullness.qual.Nullable; + +class OpenTelemetryContextPropagator { + + private static final TextMapSetter SETTER = + (carrier, key, value) -> { + if (carrier == null) { + return; + } + if ("traceparent".equals(key)) { + carrier.setTraceparent(value); + } else if ("tracestate".equals(key)) { + carrier.setTracestate(value); + } + }; + + private static final TextMapGetter GETTER = + new TextMapGetter() { + @Override + public Iterable keys(BeamFnApi.Elements.ElementMetadata carrier) { + return Lists.newArrayList("traceparent", "tracestate"); + } + + @Override + public @Nullable String get( + BeamFnApi.Elements.@Nullable ElementMetadata carrier, String key) { + if (carrier == null) { + return null; + } + if ("traceparent".equals(key)) { + return carrier.getTraceparent(); + } else if ("tracestate".equals(key)) { + return carrier.getTracestate(); + } + return null; + } + }; + + static void set(Context from, BeamFnApi.Elements.ElementMetadata.Builder builder) { + W3CTraceContextPropagator.getInstance().inject(from, builder, SETTER); + } + + static Context read(BeamFnApi.Elements.ElementMetadata from) { + return W3CTraceContextPropagator.getInstance().extract(Context.root(), from, GETTER); + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java index 67473e567d63..ea73a5c35d7b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.values; +import io.opentelemetry.context.Context; import java.util.Collection; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -50,5 +51,7 @@ public interface OutputBuilder extends WindowedValue { OutputBuilder setCausedByDrain(CausedByDrain causedByDrain); + OutputBuilder setOpenTelemetryContext(@Nullable Context openTelemetryContext); + void output(); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java index 0d8b2f7515e8..4cb8b920d64f 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.values; import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -68,6 +69,8 @@ public T getValue() { public abstract CausedByDrain getCausedByDrain(); + public abstract @Nullable Context getOpenTelemetryContext(); + // todo #33176 specify additional metadata in the future public static ValueInSingleWindow of( T value, @@ -76,14 +79,22 @@ public static ValueInSingleWindow of( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { return new AutoValue_ValueInSingleWindow<>( - value, timestamp, window, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + window, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); } public static ValueInSingleWindow of( T value, Instant timestamp, BoundedWindow window, PaneInfo paneInfo) { - return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL); + return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL, null); } /** A coder for {@link ValueInSingleWindow}. */ @@ -108,11 +119,12 @@ public static Coder of( @Override public void encode(ValueInSingleWindow windowedElem, OutputStream outStream) throws IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(ValueInSingleWindow windowedElem, OutputStream outStream, Context context) + public void encode( + ValueInSingleWindow windowedElem, OutputStream outStream, Coder.Context context) throws IOException { InstantCoder.of().encode(windowedElem.getTimestamp(), outStream); windowCoder.encode(windowedElem.getWindow(), outStream); @@ -127,6 +139,12 @@ public void encode(ValueInSingleWindow windowedElem, OutputStream outStream, windowedElem.getCausedByDrain() == CausedByDrain.CAUSED_BY_DRAIN ? BeamFnApi.Elements.DrainMode.Enum.DRAINING : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING); + io.opentelemetry.context.Context openTelemetryContext = + windowedElem.getOpenTelemetryContext(); + if (openTelemetryContext != null) { + + OpenTelemetryContextPropagator.set(openTelemetryContext, builder); + } BeamFnApi.Elements.ElementMetadata metadata = builder.build(); ByteArrayCoder.of().encode(metadata.toByteArray(), outStream); } @@ -136,16 +154,18 @@ public void encode(ValueInSingleWindow windowedElem, OutputStream outStream, @Override public ValueInSingleWindow decode(InputStream inStream) throws IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override @SuppressWarnings("IgnoredPureGetter") - public ValueInSingleWindow decode(InputStream inStream, Context context) throws IOException { + public ValueInSingleWindow decode(InputStream inStream, Coder.Context context) + throws IOException { Instant timestamp = InstantCoder.of().decode(inStream); BoundedWindow window = windowCoder.decode(inStream); PaneInfo paneInfo = PaneInfo.PaneInfoCoder.INSTANCE.decode(inStream); CausedByDrain causedByDrain = CausedByDrain.NORMAL; + io.opentelemetry.context.@Nullable Context openTelemetryContext = null; if (WindowedValues.WindowedValueCoder.isMetadataSupported() && paneInfo.isElementMetadata()) { BeamFnApi.Elements.ElementMetadata elementMetadata = BeamFnApi.Elements.ElementMetadata.parseFrom(ByteArrayCoder.of().decode(inStream)); @@ -153,12 +173,13 @@ public ValueInSingleWindow decode(InputStream inStream, Context context) thro elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + openTelemetryContext = OpenTelemetryContextPropagator.read(elementMetadata); } T value = valueCoder.decode(inStream, context); // todo #33176 specify additional metadata in the future return new AutoValue_ValueInSingleWindow<>( - value, timestamp, window, paneInfo, null, null, causedByDrain); + value, timestamp, window, paneInfo, null, null, causedByDrain, openTelemetryContext); } @Override diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java index daebeb31a39c..3bf51b266304 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.values; +import io.opentelemetry.context.Context; import java.util.Collection; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -49,6 +50,9 @@ public interface WindowedValue { @Nullable String getRecordId(); + @Nullable + Context getOpenTelemetryContext(); + @Nullable Long getRecordOffset(); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java index ba2720f5e39b..570b60ed035c 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java @@ -22,6 +22,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; +import io.opentelemetry.context.Context; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -81,7 +82,8 @@ public static Builder builder(WindowedValue template) { .setPaneInfo(template.getPaneInfo()) .setRecordOffset(template.getRecordOffset()) .setRecordId(template.getRecordId()) - .setCausedByDrain(template.causedByDrain()); + .setCausedByDrain(template.causedByDrain()) + .setOpenTelemetryContext(template.getOpenTelemetryContext()); } public static class Builder implements OutputBuilder { @@ -104,6 +106,7 @@ public static class Builder implements OutputBuilder { private @Nullable String recordId; private @Nullable Long recordOffset; private CausedByDrain causedByDrain = CausedByDrain.NORMAL; + private @Nullable Context openTelemetryContext; @Override public Builder setValue(T value) { @@ -154,6 +157,12 @@ public Builder setCausedByDrain(CausedByDrain causedByDrain) { return this; } + @Override + public Builder setOpenTelemetryContext(@Nullable Context openTelemetryContext) { + this.openTelemetryContext = openTelemetryContext; + return this; + } + public Builder setReceiver(WindowedValueReceiver receiver) { this.receiver = receiver; return this; @@ -180,6 +189,11 @@ public Instant getTimestamp() { return timestamp; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return openTelemetryContext; + } + @Override public Collection getWindows() { checkStateNotNull(windows, "Windows not set"); @@ -243,7 +257,8 @@ public WindowedValue build() { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext()); } @Override @@ -261,7 +276,7 @@ public String toString() { public static WindowedValue of( T value, Instant timestamp, Collection windows, PaneInfo paneInfo) { - return of(value, timestamp, windows, paneInfo, null, null, CausedByDrain.NORMAL); + return of(value, timestamp, windows, paneInfo, null, null, CausedByDrain.NORMAL, null); } /** Returns a {@code WindowedValue} with the given value, timestamp, and windows. */ @@ -272,7 +287,8 @@ public static WindowedValue of( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); checkArgument(windows.size() > 0, "WindowedValue requires windows, but there were none"); checkArgument(causedByDrain != null, "WindowedValue requires CausedByDrain, but it was null"); @@ -284,10 +300,18 @@ public static WindowedValue of( paneInfo, currentRecordId, currentRecordOffset, - causedByDrain); + causedByDrain, + openTelemetryContext); } else { return new TimestampedValueInMultipleWindows<>( - value, timestamp, windows, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + windows, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); } } @@ -298,12 +322,21 @@ static WindowedValue createWithoutValidation( Instant timestamp, Collection windows, PaneInfo paneInfo, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { if (windows.size() == 1) { - return of(value, timestamp, windows.iterator().next(), paneInfo, null, null, causedByDrain); + return of( + value, + timestamp, + windows.iterator().next(), + paneInfo, + null, + null, + causedByDrain, + openTelemetryContext); } else { return new TimestampedValueInMultipleWindows<>( - value, timestamp, windows, paneInfo, null, null, causedByDrain); + value, timestamp, windows, paneInfo, null, null, causedByDrain, openTelemetryContext); } } @@ -312,7 +345,7 @@ public static WindowedValue of( T value, Instant timestamp, BoundedWindow window, PaneInfo paneInfo) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); - return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL); + return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL, null); } /** Returns a {@code WindowedValue} with the given value, timestamp, and window. */ @@ -323,20 +356,39 @@ public static WindowedValue of( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); checkArgument(causedByDrain != null, "WindowedValue requires CausedByDrain, but it was null"); boolean isGlobal = GlobalWindow.INSTANCE.equals(window); if (isGlobal && BoundedWindow.TIMESTAMP_MIN_VALUE.equals(timestamp)) { return new ValueInGlobalWindow<>( - value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); } else if (isGlobal) { return new TimestampedValueInGlobalWindow<>( - value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); } else { return new TimestampedValueInSingleWindow<>( - value, timestamp, window, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + window, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); } } @@ -345,7 +397,8 @@ public static WindowedValue of( * default timestamp and pane. */ public static WindowedValue valueInGlobalWindow(T value) { - return new ValueInGlobalWindow<>(value, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL); + return new ValueInGlobalWindow<>( + value, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL, null); } /** @@ -353,7 +406,7 @@ public static WindowedValue valueInGlobalWindow(T value) { * default timestamp and the specified pane. */ public static WindowedValue valueInGlobalWindow(T value, PaneInfo paneInfo) { - return new ValueInGlobalWindow<>(value, paneInfo, null, null, CausedByDrain.NORMAL); + return new ValueInGlobalWindow<>(value, paneInfo, null, null, CausedByDrain.NORMAL, null); } /** @@ -365,7 +418,7 @@ public static WindowedValue timestampedValueInGlobalWindow(T value, Insta return valueInGlobalWindow(value); } else { return new TimestampedValueInGlobalWindow<>( - value, timestamp, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL); + value, timestamp, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL, null); } } @@ -379,7 +432,7 @@ public static WindowedValue timestampedValueInGlobalWindow( return timestampedValueInGlobalWindow(value, timestamp); } else { return new TimestampedValueInGlobalWindow<>( - value, timestamp, paneInfo, null, null, CausedByDrain.NORMAL); + value, timestamp, paneInfo, null, null, CausedByDrain.NORMAL, null); } } @@ -396,7 +449,8 @@ public static WindowedValue withValue( windowedValue.getPaneInfo(), windowedValue.getRecordId(), windowedValue.getRecordOffset(), - windowedValue.causedByDrain()); + windowedValue.causedByDrain(), + windowedValue.getOpenTelemetryContext()); } public static boolean equals( @@ -448,6 +502,7 @@ private abstract static class SimpleWindowedValue implements WindowedValue private final @Nullable String currentRecordId; private final @Nullable Long currentRecordOffset; private final CausedByDrain causedByDrain; + private final @Nullable Context context; @Override public @Nullable String getRecordId() { @@ -469,12 +524,14 @@ protected SimpleWindowedValue( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context context) { this.value = value; this.paneInfo = checkNotNull(paneInfo); this.currentRecordId = currentRecordId; this.currentRecordOffset = currentRecordOffset; this.causedByDrain = causedByDrain; + this.context = context; } @Override @@ -487,6 +544,11 @@ public T getValue() { return value; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return context; + } + @Override public Iterable> explodeWindows() { if (this.getWindows().size() == 1) { @@ -523,8 +585,9 @@ public MinTimestampWindowedValue( PaneInfo pane, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, pane, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context) { + super(value, pane, currentRecordId, currentRecordOffset, causedByDrain, context); } @Override @@ -542,8 +605,9 @@ public ValueInGlobalWindow( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context) { + super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context); } @Override @@ -559,7 +623,12 @@ public BoundedWindow getWindow() { @Override public WindowedValue withValue(NewT newValue) { return new ValueInGlobalWindow<>( - newValue, getPaneInfo(), getRecordId(), getRecordOffset(), causedByDrain()); + newValue, + getPaneInfo(), + getRecordId(), + getRecordOffset(), + causedByDrain(), + getOpenTelemetryContext()); } @Override @@ -598,8 +667,9 @@ public TimestampedWindowedValue( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context) { + super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context); this.timestamp = checkNotNull(timestamp); } @@ -622,8 +692,10 @@ public TimestampedValueInGlobalWindow( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context) { + super( + value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context); } @Override @@ -644,7 +716,8 @@ public WindowedValue withValue(NewT newValue) { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext()); } @Override @@ -695,8 +768,16 @@ public TimestampedValueInSingleWindow( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { + super( + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); this.window = checkNotNull(window); } @@ -709,7 +790,8 @@ public WindowedValue withValue(NewT newValue) { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext()); } @Override @@ -767,8 +849,16 @@ public TimestampedValueInMultipleWindows( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { + super( + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext); this.windows = checkNotNull(windows); } @@ -786,7 +876,8 @@ public WindowedValue withValue(NewT newValue) { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext()); } @Override @@ -927,11 +1018,11 @@ public WindowedValueCoder withValueCoder(Coder valueCoder) { @Override public void encode(WindowedValue windowedElem, OutputStream outStream) throws CoderException, IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(WindowedValue windowedElem, OutputStream outStream, Context context) + public void encode(WindowedValue windowedElem, OutputStream outStream, Coder.Context context) throws CoderException, IOException { InstantCoder.of().encode(windowedElem.getTimestamp(), outStream); windowsCoder.encode(windowedElem.getWindows(), outStream); @@ -941,6 +1032,11 @@ public void encode(WindowedValue windowedElem, OutputStream outStream, Contex if (metadataSupported) { BeamFnApi.Elements.ElementMetadata.Builder builder = BeamFnApi.Elements.ElementMetadata.newBuilder(); + io.opentelemetry.context.Context openTelemetryContext = + windowedElem.getOpenTelemetryContext(); + if (openTelemetryContext != null) { + OpenTelemetryContextPropagator.set(openTelemetryContext, builder); + } BeamFnApi.Elements.ElementMetadata em = builder .setDrain( @@ -956,16 +1052,17 @@ public void encode(WindowedValue windowedElem, OutputStream outStream, Contex @Override public WindowedValue decode(InputStream inStream) throws CoderException, IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override - public WindowedValue decode(InputStream inStream, Context context) + public WindowedValue decode(InputStream inStream, Coder.Context context) throws CoderException, IOException { Instant timestamp = InstantCoder.of().decode(inStream); Collection windows = windowsCoder.decode(inStream); PaneInfo paneInfo = PaneInfoCoder.INSTANCE.decode(inStream); CausedByDrain causedByDrain = CausedByDrain.NORMAL; + io.opentelemetry.context.Context openTelemetryContext = null; if (isMetadataSupported() && paneInfo.isElementMetadata()) { BeamFnApi.Elements.ElementMetadata elementMetadata = BeamFnApi.Elements.ElementMetadata.parseFrom(ByteArrayCoder.of().decode(inStream)); @@ -973,13 +1070,14 @@ public WindowedValue decode(InputStream inStream, Context context) elementMetadata.getDrain().equals(BeamFnApi.Elements.DrainMode.Enum.DRAINING) ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + openTelemetryContext = OpenTelemetryContextPropagator.read(elementMetadata); } T value = valueCoder.decode(inStream, context); // Because there are some remaining (incorrect) uses of WindowedValue with no windows, // we call this deprecated no-validation path when decoding return WindowedValues.createWithoutValidation( - value, timestamp, windows, paneInfo, causedByDrain); + value, timestamp, windows, paneInfo, causedByDrain, openTelemetryContext); } @Override @@ -1045,22 +1143,22 @@ public WindowedValueCoder withValueCoder(Coder valueCoder) { @Override public void encode(WindowedValue windowedElem, OutputStream outStream) throws CoderException, IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(WindowedValue windowedElem, OutputStream outStream, Context context) + public void encode(WindowedValue windowedElem, OutputStream outStream, Coder.Context context) throws CoderException, IOException { valueCoder.encode(windowedElem.getValue(), outStream, context); } @Override public WindowedValue decode(InputStream inStream) throws CoderException, IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override - public WindowedValue decode(InputStream inStream, Context context) + public WindowedValue decode(InputStream inStream, Coder.Context context) throws CoderException, IOException { T value = valueCoder.decode(inStream, context); return WindowedValues.valueInGlobalWindow(value); @@ -1156,22 +1254,22 @@ public WindowedValueCoder withValueCoder(Coder valueCoder) { @Override public void encode(WindowedValue windowedElem, OutputStream outStream) throws CoderException, IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(WindowedValue windowedElem, OutputStream outStream, Context context) + public void encode(WindowedValue windowedElem, OutputStream outStream, Coder.Context context) throws CoderException, IOException { valueCoder.encode(windowedElem.getValue(), outStream, context); } @Override public WindowedValue decode(InputStream inStream) throws CoderException, IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override - public WindowedValue decode(InputStream inStream, Context context) + public WindowedValue decode(InputStream inStream, Coder.Context context) throws CoderException, IOException { return WindowedValues.withValue(windowedValuePrototype, valueCoder.decode(inStream, context)); } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java index 97b99321e5c7..830ad654b45c 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java @@ -23,6 +23,11 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; import java.util.Arrays; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.CoderException; @@ -81,6 +86,17 @@ public void testWindowedValueCoder() throws CoderException { @Test public void testWindowedValueWithElementMetadataCoder() throws CoderException { WindowedValues.WindowedValueCoder.setMetadataSupported(); + + Context context = + Context.current() + .with( + Span.wrap( + SpanContext.create( + "ff000000000000000000000000000041", + "ff00000000000041", + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").put("bar", "baz").build()))); + Instant timestamp = new Instant(1234); WindowedValue value = WindowedValues.of( @@ -93,12 +109,14 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { PaneInfo.NO_FIRING, null, null, - CausedByDrain.CAUSED_BY_DRAIN); // drain is persisted as part of metadata + CausedByDrain.CAUSED_BY_DRAIN, + context); // drain is persisted as part of metadata Coder> windowedValueCoder = WindowedValues.getFullCoder(StringUtf8Coder.of(), IntervalWindow.getCoder()); byte[] encodedValue = CoderUtils.encodeToByteArray(windowedValueCoder, value); + WindowedValue decodedValue = CoderUtils.decodeFromByteArray(windowedValueCoder, encodedValue); @@ -106,6 +124,7 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { Assert.assertEquals(value.getTimestamp(), decodedValue.getTimestamp()); Assert.assertArrayEquals(value.getWindows().toArray(), decodedValue.getWindows().toArray()); Assert.assertEquals(CausedByDrain.CAUSED_BY_DRAIN, value.causedByDrain()); + Assert.assertNotNull(value.getOpenTelemetryContext()); } @Test diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java index 0aaf71c016d4..68fdcdbf0a90 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java @@ -2364,7 +2364,8 @@ public void output(TupleTag tag, T output) { currentTimer.getPaneInfo(), null, null, - currentTimer.causedByDrain())); + currentTimer.causedByDrain(), + null)); } @Override diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java index d9f63b5177ac..c3067ba0934f 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java @@ -122,6 +122,7 @@ public void testGcpApiSurface() throws Exception { classesInPackage("com.fasterxml.jackson.core"), classesInPackage("com.fasterxml.jackson.databind"), classesInPackage("io.grpc"), + classesInPackage("io.opentelemetry.context"), classesInPackage("java"), classesInPackage("javax"), classesInPackage("org.apache.avro"), From 5cd29a795afd150fe336a1d61dcbb20650666077 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Tue, 5 May 2026 10:44:56 -0400 Subject: [PATCH 073/490] [Gemini] Migrate all remaining uses of typing types with built-in equivalents (#38334) * [Gemini] Migrate all remaining uses of typing types with built-in equivalents * Fix comment type hint imports * move noqa * trigger postcommit for extra validation * remove weird double import * restore import in example * other misc weird import changes * enable UP006 by default * yapf * fix gen_xlang_wrappers.py * Fix gen_managed_doc.py * last yapf fixes * fix pretty_type for built-in generics * strip nested typing module tags * restore type checking import in worker_handlers.py --- .../trigger_files/beam_PostCommit_Python.json | 2 +- sdks/python/apache_beam/coders/coder_impl.py | 1 + sdks/python/apache_beam/coders/coders.py | 11 +- .../apache_beam/coders/coders_test_common.py | 3 +- .../apache_beam/coders/observable_test.py | 3 +- .../apache_beam/coders/row_coder_test.py | 4 +- sdks/python/apache_beam/coders/slow_stream.py | 3 +- .../coders/standard_coders_test.py | 4 +- sdks/python/apache_beam/coders/typecoders.py | 13 +- .../apache_beam/dataframe/frame_base.py | 3 +- .../apache_beam/dataframe/schemas_test.py | 85 ++++------- .../composite_transform.py | 3 +- .../tfx_bsl/build_tensorflow_model.py | 3 +- .../examples/snippets/snippets_test.py | 13 +- .../apache_beam/internal/dill_pickler.py | 6 +- .../apache_beam/internal/metrics/metric.py | 12 +- sdks/python/apache_beam/internal/util.py | 10 +- sdks/python/apache_beam/io/avroio.py | 6 +- sdks/python/apache_beam/io/avroio_test.py | 4 +- .../apache_beam/io/components/rate_limiter.py | 4 +- sdks/python/apache_beam/io/debezium.py | 5 +- .../io/external/xlang_jdbcio_it_test.py | 2 +- .../io/external/xlang_kafkaio_it_test.py | 2 +- .../io/external/xlang_kafkaio_perf_test.py | 3 +- .../apache_beam/io/filebasedio_perf_test.py | 3 +- sdks/python/apache_beam/io/filebasedsource.py | 3 +- sdks/python/apache_beam/io/fileio.py | 21 ++- sdks/python/apache_beam/io/filesystem.py | 8 +- .../flink/flink_streaming_impulse_source.py | 3 +- sdks/python/apache_beam/io/gcp/bigquery.py | 39 +++-- .../apache_beam/io/gcp/bigquery_avro_tools.py | 9 +- .../io/gcp/bigquery_change_history.py | 45 +++--- .../io/gcp/bigquery_read_internal.py | 6 +- .../apache_beam/io/gcp/bigquery_tools.py | 3 +- sdks/python/apache_beam/io/gcp/bigtableio.py | 4 +- .../io/gcp/datastore/v1new/helper.py | 3 +- .../io/gcp/datastore/v1new/types.py | 3 +- .../io/gcp/experimental/spannerio.py | 8 +- sdks/python/apache_beam/io/gcp/pubsub.py | 6 +- sdks/python/apache_beam/io/iobase.py | 5 +- sdks/python/apache_beam/io/jdbc.py | 2 +- sdks/python/apache_beam/io/kafka.py | 6 +- sdks/python/apache_beam/io/requestresponse.py | 19 ++- .../apache_beam/io/requestresponse_it_test.py | 3 +- .../apache_beam/io/restriction_trackers.py | 4 +- sdks/python/apache_beam/io/textio.py | 3 +- sdks/python/apache_beam/metrics/cells.py | 1 + sdks/python/apache_beam/metrics/execution.py | 1 + sdks/python/apache_beam/metrics/metric.py | 41 +++--- sdks/python/apache_beam/metrics/metricbase.py | 3 +- .../apache_beam/metrics/monitoring_infos.py | 6 +- sdks/python/apache_beam/ml/gcp/cloud_dlp.py | 3 +- .../apache_beam/ml/gcp/naturallanguageml.py | 5 +- .../apache_beam/ml/gcp/recommendations_ai.py | 13 +- .../apache_beam/ml/gcp/videointelligenceml.py | 3 +- sdks/python/apache_beam/ml/gcp/visionml.py | 10 +- .../apache_beam/ml/inference/model_manager.py | 8 +- .../apache_beam/ml/rag/chunking/base.py | 5 +- .../apache_beam/ml/rag/chunking/base_test.py | 3 +- .../apache_beam/ml/rag/chunking/langchain.py | 8 +- .../apache_beam/ml/rag/embeddings/base.py | 5 +- .../rag/enrichment/bigquery_vector_search.py | 21 ++- .../ml/rag/enrichment/milvus_search.py | 49 +++---- .../rag/enrichment/milvus_search_it_test.py | 3 +- .../apache_beam/ml/rag/ingestion/alloydb.py | 12 +- .../ml/rag/ingestion/alloydb_it_test.py | 5 +- .../apache_beam/ml/rag/ingestion/bigquery.py | 7 +- .../apache_beam/ml/rag/ingestion/cloudsql.py | 22 ++- .../ml/rag/ingestion/cloudsql_it_test.py | 3 +- .../ml/rag/ingestion/jdbc_common.py | 8 +- .../ml/rag/ingestion/milvus_search.py | 12 +- .../apache_beam/ml/rag/ingestion/mysql.py | 15 +- .../ml/rag/ingestion/mysql_common.py | 25 ++-- .../apache_beam/ml/rag/ingestion/postgres.py | 8 +- .../ml/rag/ingestion/postgres_common.py | 30 ++-- .../ml/rag/ingestion/postgres_it_test.py | 5 +- .../apache_beam/ml/rag/ingestion/spanner.py | 26 ++-- .../ml/rag/ingestion/test_utils.py | 3 +- sdks/python/apache_beam/ml/rag/test_utils.py | 5 +- sdks/python/apache_beam/ml/rag/types.py | 17 +-- sdks/python/apache_beam/ml/rag/utils.py | 14 +- sdks/python/apache_beam/ml/transforms/base.py | 22 ++- .../apache_beam/options/pipeline_options.py | 15 +- .../apache_beam/options/value_provider.py | 3 +- sdks/python/apache_beam/pipeline.py | 5 +- sdks/python/apache_beam/pvalue.py | 3 +- sdks/python/apache_beam/runners/common.py | 1 + .../apache_beam/runners/dask/dask_runner.py | 4 +- .../apache_beam/runners/dask/overrides.py | 16 +-- .../runners/dask/transform_evaluator.py | 8 +- .../runners/direct/bundle_factory.py | 5 +- .../consumer_tracking_pipeline_visitor.py | 9 +- .../runners/direct/direct_runner.py | 20 +-- .../runners/direct/evaluation_context.py | 24 ++-- .../apache_beam/runners/direct/executor.py | 11 +- .../runners/direct/transform_evaluator.py | 14 +- .../runners/direct/watermark_manager.py | 18 +-- .../runners/interactive/augmented_pipeline.py | 6 +- .../runners/interactive/caching/read_cache.py | 6 +- .../interactive/caching/write_cache.py | 4 +- .../dataproc/dataproc_cluster_manager.py | 5 +- .../interactive/display/pipeline_graph.py | 11 +- .../display/pipeline_graph_renderer.py | 3 +- .../yaml_parse_utils.py | 12 +- .../interactive/options/capture_control.py | 5 +- .../interactive/pipeline_instrument.py | 5 +- .../interactive/sql/beam_sql_magics.py | 21 ++- .../runners/interactive/sql/sql_chain.py | 10 +- .../runners/interactive/sql/utils.py | 12 +- .../runners/interactive/testing/mock_env.py | 3 +- .../apache_beam/runners/interactive/utils.py | 13 +- .../apache_beam/runners/pipeline_context.py | 17 +-- .../portability/abstract_job_service.py | 8 +- .../runners/portability/artifact_service.py | 11 +- .../portability/expansion_service_test.py | 2 +- .../portability/fn_api_runner/execution.py | 79 +++++----- .../portability/fn_api_runner/fn_runner.py | 61 ++++---- .../fn_api_runner/fn_runner_test.py | 11 +- .../portability/fn_api_runner/translations.py | 26 ++-- .../fn_api_runner/trigger_manager.py | 24 ++-- .../fn_api_runner/visualization_tools.py | 6 +- .../fn_api_runner/watermark_manager.py | 19 ++- .../fn_api_runner/worker_handlers.py | 6 +- .../runners/portability/local_job_service.py | 9 +- .../runners/portability/portable_runner.py | 8 +- .../runners/portability/prism_runner.py | 5 +- .../portability/sdk_container_builder.py | 3 +- .../apache_beam/runners/portability/stager.py | 32 ++--- .../runners/portability/stager_test.py | 3 +- sdks/python/apache_beam/runners/sdf_utils.py | 3 +- .../apache_beam/runners/trivial_runner.py | 5 +- .../runners/worker/bundle_processor.py | 135 +++++++++--------- .../runners/worker/data_sampler.py | 21 ++- .../runners/worker/data_sampler_test.py | 5 +- .../apache_beam/runners/worker/log_handler.py | 3 +- .../apache_beam/runners/worker/logger.py | 1 + .../apache_beam/runners/worker/opcounters.py | 1 + .../apache_beam/runners/worker/operations.py | 1 + .../apache_beam/runners/worker/sdk_worker.py | 10 +- .../apache_beam/runners/worker/statecache.py | 8 +- .../runners/worker/statesampler.py | 3 +- .../runners/worker/worker_pool_main.py | 6 +- .../testing/analyzers/github_issues_utils.py | 12 +- .../testing/analyzers/perf_analysis.py | 7 +- .../testing/analyzers/perf_analysis_utils.py | 25 ++-- .../benchmarks/nexmark/queries/query3.py | 8 +- .../load_tests/load_test_metrics_utils.py | 12 +- .../testing/load_tests/sideinput_test.py | 8 +- .../apache_beam/testing/synthetic_pipeline.py | 3 +- sdks/python/apache_beam/testing/util.py | 3 +- .../runtime_type_check_microbenchmark.py | 9 +- sdks/python/apache_beam/transforms/core.py | 4 +- .../apache_beam/transforms/cy_combiners.py | 1 + .../apache_beam/transforms/deduplicate.py | 4 +- .../enrichment_handlers/cloudsql.py | 18 ++- .../apache_beam/transforms/external_test.py | 9 +- .../external_transform_provider_it_test.py | 18 +-- .../apache_beam/transforms/ptransform_test.py | 98 ++++++------- .../apache_beam/transforms/sideinputs_test.py | 8 +- sdks/python/apache_beam/transforms/stats.py | 1 + sdks/python/apache_beam/transforms/util.py | 18 ++- .../transforms/validate_runner_xlang_test.py | 15 +- .../typehints/arrow_type_compatibility.py | 1 + .../apache_beam/typehints/batch_test.py | 1 + .../apache_beam/typehints/decorators.py | 1 + .../apache_beam/typehints/decorators_test.py | 1 + .../typehints/native_type_compatibility.py | 1 + .../native_type_compatibility_test.py | 1 + .../typehints/pandas_type_compatibility.py | 1 + sdks/python/apache_beam/typehints/row_type.py | 1 + sdks/python/apache_beam/typehints/schemas.py | 1 + .../apache_beam/typehints/schemas_test.py | 1 + .../apache_beam/typehints/typecheck_test.py | 1 + .../typehints/typed_pipeline_test.py | 1 + .../python/apache_beam/typehints/typehints.py | 1 + .../apache_beam/typehints/typehints_test.py | 1 + sdks/python/apache_beam/utils/counters.py | 1 + .../apache_beam/utils/multi_process_shared.py | 3 +- sdks/python/apache_beam/utils/proto_utils.py | 9 +- .../apache_beam/utils/subprocess_server.py | 3 +- sdks/python/apache_beam/utils/urns.py | 13 +- .../apache_beam/utils/windowed_value.py | 1 + .../yaml/examples/testing/examples_test.py | 52 ++++--- sdks/python/apache_beam/yaml/yaml_testing.py | 7 +- sdks/python/apache_beam/yaml/yaml_utils.py | 3 +- sdks/python/gen_managed_doc.py | 3 +- sdks/python/gen_xlang_wrappers.py | 20 +-- sdks/python/ruff.toml | 2 +- 188 files changed, 883 insertions(+), 1171 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 69f759d8463d..91226bd08ee3 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "38069", - "modification": 40 + "modification": 41 } diff --git a/sdks/python/apache_beam/coders/coder_impl.py b/sdks/python/apache_beam/coders/coder_impl.py index cd66a0c09e01..0bded25e05d2 100644 --- a/sdks/python/apache_beam/coders/coder_impl.py +++ b/sdks/python/apache_beam/coders/coder_impl.py @@ -30,6 +30,7 @@ """ # pytype: skip-file +# ruff: noqa: UP006 import dataclasses import decimal import enum diff --git a/sdks/python/apache_beam/coders/coders.py b/sdks/python/apache_beam/coders/coders.py index 556b18043189..5f22bff5351b 100644 --- a/sdks/python/apache_beam/coders/coders.py +++ b/sdks/python/apache_beam/coders/coders.py @@ -43,13 +43,9 @@ from typing import TYPE_CHECKING from typing import Any from typing import Callable -from typing import Dict from typing import Iterable -from typing import List from typing import Optional from typing import Sequence -from typing import Tuple -from typing import Type from typing import TypeVar from typing import overload @@ -68,7 +64,6 @@ from apache_beam.utils import windowed_value if TYPE_CHECKING: - from apache_beam.coders.typecoders import CoderRegistry from apache_beam.runners.pipeline_context import PipelineContext # pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports @@ -122,7 +117,7 @@ T = TypeVar('T') CoderT = TypeVar('CoderT', bound='Coder') ProtoCoderT = TypeVar('ProtoCoderT', bound='ProtoCoder') -ConstructorFn = Callable[[Optional[Any], List['Coder'], 'PipelineContext'], Any] +ConstructorFn = Callable[[Optional[Any], list['Coder'], 'PipelineContext'], Any] def serialize_coder(coder): @@ -1508,7 +1503,7 @@ def __hash__(self): class _OrderedUnionCoder(FastCoder): def __init__( - self, *coder_types: Tuple[type, Coder], fallback_coder: Optional[Coder]): + self, *coder_types: tuple[type, Coder], fallback_coder: Optional[Coder]): self._coder_types = coder_types self._fallback_coder = fallback_coder @@ -1816,7 +1811,7 @@ def _create_impl(self): def to_type_hint(self): return self._window_coder.to_type_hint() - def _get_component_coders(self) -> List[Coder]: + def _get_component_coders(self) -> list[Coder]: return [self._window_coder] def is_deterministic(self) -> bool: diff --git a/sdks/python/apache_beam/coders/coders_test_common.py b/sdks/python/apache_beam/coders/coders_test_common.py index 5e5cfc8a5b62..422d494b61c7 100644 --- a/sdks/python/apache_beam/coders/coders_test_common.py +++ b/sdks/python/apache_beam/coders/coders_test_common.py @@ -31,7 +31,6 @@ import unittest from decimal import Decimal from typing import Any -from typing import List from typing import NamedTuple import pytest @@ -145,7 +144,7 @@ class CodersTest(unittest.TestCase): # nested and unnested context. # Common test values representing Python's built-in types. - test_values_deterministic: List[Any] = [ + test_values_deterministic: list[Any] = [ None, 1, -1, diff --git a/sdks/python/apache_beam/coders/observable_test.py b/sdks/python/apache_beam/coders/observable_test.py index df4e7ef09408..ffe2ac8fed0e 100644 --- a/sdks/python/apache_beam/coders/observable_test.py +++ b/sdks/python/apache_beam/coders/observable_test.py @@ -20,7 +20,6 @@ import logging import unittest -from typing import List from typing import Optional from apache_beam.coders import observable @@ -29,7 +28,7 @@ class ObservableMixinTest(unittest.TestCase): observed_count = 0 observed_sum = 0 - observed_keys: List[Optional[str]] = [] + observed_keys: list[Optional[str]] = [] def observer(self, value, key=None): self.observed_count += 1 diff --git a/sdks/python/apache_beam/coders/row_coder_test.py b/sdks/python/apache_beam/coders/row_coder_test.py index c2176e99a999..396cdc9b3f66 100644 --- a/sdks/python/apache_beam/coders/row_coder_test.py +++ b/sdks/python/apache_beam/coders/row_coder_test.py @@ -43,7 +43,7 @@ ("name", str), ("age", np.int32), ("address", typing.Optional[str]), - ("aliases", typing.List[str]), + ("aliases", list[str]), ("knows_javascript", bool), ("payload", typing.Optional[bytes]), ("custom_metadata", typing.Mapping[str, int]), @@ -53,7 +53,7 @@ NullablePerson = typing.NamedTuple( "NullablePerson", [("name", typing.Optional[str]), ("age", np.int32), - ("address", typing.Optional[str]), ("aliases", typing.List[str]), + ("address", typing.Optional[str]), ("aliases", list[str]), ("knows_javascript", bool), ("payload", typing.Optional[bytes]), ("custom_metadata", typing.Mapping[str, int]), ("favorite_time", typing.Optional[Timestamp]), diff --git a/sdks/python/apache_beam/coders/slow_stream.py b/sdks/python/apache_beam/coders/slow_stream.py index fb4aa50f233d..c1843bbe957d 100644 --- a/sdks/python/apache_beam/coders/slow_stream.py +++ b/sdks/python/apache_beam/coders/slow_stream.py @@ -22,7 +22,6 @@ # pytype: skip-file import struct -from typing import List class OutputStream(object): @@ -30,7 +29,7 @@ class OutputStream(object): A pure Python implementation of stream.OutputStream.""" def __init__(self): - self.data: List[bytes] = [] + self.data: list[bytes] = [] self.byte_count = 0 def write(self, b: bytes, nested: bool = False) -> None: diff --git a/sdks/python/apache_beam/coders/standard_coders_test.py b/sdks/python/apache_beam/coders/standard_coders_test.py index b6f0dbf12208..e2301ada19ea 100644 --- a/sdks/python/apache_beam/coders/standard_coders_test.py +++ b/sdks/python/apache_beam/coders/standard_coders_test.py @@ -26,8 +26,6 @@ import sys import unittest from copy import deepcopy -from typing import Dict -from typing import Tuple import numpy as np import yaml @@ -283,7 +281,7 @@ def json_value_parser(self, coder_spec): # Used when --fix is passed. fix = False - to_fix: Dict[Tuple[int, bytes], bytes] = {} + to_fix: dict[tuple[int, bytes], bytes] = {} @classmethod def tearDownClass(cls): diff --git a/sdks/python/apache_beam/coders/typecoders.py b/sdks/python/apache_beam/coders/typecoders.py index 9ecb14c60f75..cf594475c87d 100644 --- a/sdks/python/apache_beam/coders/typecoders.py +++ b/sdks/python/apache_beam/coders/typecoders.py @@ -66,10 +66,7 @@ def MakeXyzs(v): # pytype: skip-file from typing import Any -from typing import Dict from typing import Iterable -from typing import List -from typing import Type from apache_beam.coders import coders from apache_beam.typehints import typehints @@ -81,8 +78,8 @@ def MakeXyzs(v): class CoderRegistry(object): """A coder registry for typehint/coder associations.""" def __init__(self, fallback_coder=None): - self._coders: Dict[Any, Type[coders.Coder]] = {} - self.custom_types: List[Any] = [] + self._coders: dict[Any, type[coders.Coder]] = {} + self.custom_types: list[Any] = [] self.register_standard_coders(fallback_coder) def register_standard_coders(self, fallback_coder): @@ -110,7 +107,7 @@ def register_fallback_coder(self, fallback_coder): def _register_coder_internal( self, typehint_type: Any, - typehint_coder_class: Type[coders.Coder]) -> None: + typehint_coder_class: type[coders.Coder]) -> None: self._coders[typehint_type] = typehint_coder_class @staticmethod @@ -123,7 +120,7 @@ def _normalize_typehint_type(typehint_type): def register_coder( self, typehint_type: Any, - typehint_coder_class: Type[coders.Coder]) -> None: + typehint_coder_class: type[coders.Coder]) -> None: """ Register a user type with a coder. @@ -244,7 +241,7 @@ class FirstOf(object): """For internal use only; no backwards-compatibility guarantees. A class used to get the first matching coder from a list of coders.""" - def __init__(self, coders: Iterable[Type[coders.Coder]]) -> None: + def __init__(self, coders: Iterable[type[coders.Coder]]) -> None: self._coders = coders def from_type_hint(self, typehint, registry): diff --git a/sdks/python/apache_beam/dataframe/frame_base.py b/sdks/python/apache_beam/dataframe/frame_base.py index 8e206fc5e037..f5bf024b177b 100644 --- a/sdks/python/apache_beam/dataframe/frame_base.py +++ b/sdks/python/apache_beam/dataframe/frame_base.py @@ -25,7 +25,6 @@ from inspect import unwrap from typing import Any from typing import Optional -from typing import Tuple from typing import Union import pandas as pd @@ -163,7 +162,7 @@ def binop(self, other): DeferredBase._pandas_type_map[None] = _DeferredScalar -def name_and_func(method: Union[str, Callable]) -> Tuple[str, Callable]: +def name_and_func(method: Union[str, Callable]) -> tuple[str, Callable]: """For the given method name or method, return the method name and the method itself. diff --git a/sdks/python/apache_beam/dataframe/schemas_test.py b/sdks/python/apache_beam/dataframe/schemas_test.py index 4c196e29e712..135d9ca21b3d 100644 --- a/sdks/python/apache_beam/dataframe/schemas_test.py +++ b/sdks/python/apache_beam/dataframe/schemas_test.py @@ -64,57 +64,36 @@ def check_df_pcoll_equal(actual): # pd.Series([b'abc'], dtype=bytes).dtype != 'S' # pd.Series([b'abc'], dtype=bytes).astype(bytes).dtype == 'S' # (test data, pandas_type, column_name, beam_type) -COLUMNS: typing.List[typing.Tuple[typing.List[typing.Any], - typing.Any, - str, - typing.Any]] = [ - ([375, 24, 0, 10, 16], - np.int32, - 'i32', - np.int32), - ([375, 24, 0, 10, 16], - np.int64, - 'i64', - np.int64), - ([375, 24, None, 10, 16], - pd.Int32Dtype(), - 'i32_nullable', - typing.Optional[np.int32]), - ([375, 24, None, 10, 16], - pd.Int64Dtype(), - 'i64_nullable', - typing.Optional[np.int64]), - ([375., 24., None, 10., 16.], - np.float64, - 'f64', - typing.Optional[np.float64]), - ([375., 24., None, 10., 16.], - np.float32, - 'f32', - typing.Optional[np.float32]), - ([True, False, True, True, False], - bool, - 'bool', - bool), - (['Falcon', 'Ostrich', None, 3.14, 0], - object, - 'any', - typing.Any), - ([True, False, True, None, False], - pd.BooleanDtype(), - 'bool_nullable', - typing.Optional[bool]), - ([ - 'Falcon', - 'Ostrich', - None, - 'Aardvark', - 'Elephant' - ], - pd.StringDtype(), - 'strdtype', - typing.Optional[str]), - ] +COLUMNS: list[tuple[list[typing.Any], typing.Any, str, typing.Any]] = [ + ([375, 24, 0, 10, 16], np.int32, 'i32', np.int32), + ([375, 24, 0, 10, 16], np.int64, 'i64', np.int64), + ([375, 24, None, 10, 16], + pd.Int32Dtype(), + 'i32_nullable', + typing.Optional[np.int32]), + ([375, 24, None, 10, 16], + pd.Int64Dtype(), + 'i64_nullable', + typing.Optional[np.int64]), + ([375., 24., None, 10., 16.], + np.float64, + 'f64', + typing.Optional[np.float64]), + ([375., 24., None, 10., 16.], + np.float32, + 'f32', + typing.Optional[np.float32]), + ([True, False, True, True, False], bool, 'bool', bool), + (['Falcon', 'Ostrich', None, 3.14, 0], object, 'any', typing.Any), + ([True, False, True, None, False], + pd.BooleanDtype(), + 'bool_nullable', + typing.Optional[bool]), + (['Falcon', 'Ostrich', None, 'Aardvark', 'Elephant'], + pd.StringDtype(), + 'strdtype', + typing.Optional[str]), +] NICE_TYPES_DF = pd.DataFrame(columns=[name for _, _, name, _ in COLUMNS]) for arr, dtype, name, _ in COLUMNS: @@ -125,9 +104,7 @@ def check_df_pcoll_equal(actual): SERIES_TESTS = [(pd.Series(arr, dtype=dtype, name=name), arr, beam_type) for (arr, dtype, name, beam_type) in COLUMNS] -_TEST_ARRAYS: typing.List[typing.List[typing.Any]] = [ - arr for (arr, _, _, _) in COLUMNS -] +_TEST_ARRAYS: list[list[typing.Any]] = [arr for (arr, _, _, _) in COLUMNS] DF_RESULT = list(zip(*_TEST_ARRAYS)) BEAM_SCHEMA = typing.NamedTuple( # type: ignore 'BEAM_SCHEMA', [(name, beam_type) for _, _, name, beam_type in COLUMNS]) diff --git a/sdks/python/apache_beam/examples/inference/multi_language_inference/multi_language_custom_transform/multi_language_custom_transform/composite_transform.py b/sdks/python/apache_beam/examples/inference/multi_language_inference/multi_language_custom_transform/multi_language_custom_transform/composite_transform.py index 3e1794bc5829..8fbfed085573 100644 --- a/sdks/python/apache_beam/examples/inference/multi_language_inference/multi_language_custom_transform/multi_language_custom_transform/composite_transform.py +++ b/sdks/python/apache_beam/examples/inference/multi_language_inference/multi_language_custom_transform/multi_language_custom_transform/composite_transform.py @@ -15,7 +15,6 @@ # import logging -import signal import typing import apache_beam as beam @@ -84,7 +83,7 @@ def __init__(self, bert_tokenizer): self.bert_tokenizer = bert_tokenizer logging.info('Starting Postprocess') - def process(self, element: typing.Tuple[str, PredictionResult]) \ + def process(self, element: tuple[str, PredictionResult]) \ -> typing.Iterable[str]: text, prediction_result = element inputs = prediction_result.example diff --git a/sdks/python/apache_beam/examples/inference/tfx_bsl/build_tensorflow_model.py b/sdks/python/apache_beam/examples/inference/tfx_bsl/build_tensorflow_model.py index 9230f84955eb..a8541c01a410 100644 --- a/sdks/python/apache_beam/examples/inference/tfx_bsl/build_tensorflow_model.py +++ b/sdks/python/apache_beam/examples/inference/tfx_bsl/build_tensorflow_model.py @@ -17,7 +17,6 @@ # Intended only for internal testing. -from typing import Dict from typing import Optional import tensorflow as tf @@ -114,7 +113,7 @@ def save_tf_model_with_signature( model=None, preprocess_input=None, input_dtype=tf.float32, - feature_description: Optional[Dict] = None, + feature_description: Optional[dict] = None, **kwargs, ): """ diff --git a/sdks/python/apache_beam/examples/snippets/snippets_test.py b/sdks/python/apache_beam/examples/snippets/snippets_test.py index d7dd5e6af191..e4f2d16ac144 100644 --- a/sdks/python/apache_beam/examples/snippets/snippets_test.py +++ b/sdks/python/apache_beam/examples/snippets/snippets_test.py @@ -333,12 +333,12 @@ def process(self, element): # One can assert outputs and apply them to transforms as well. # Helps document the contract and checks it at pipeline construction time. # [START type_hints_transform] - from typing import Tuple, TypeVar + from typing import TypeVar T = TypeVar('T') @beam.typehints.with_input_types(T) - @beam.typehints.with_output_types(Tuple[int, T]) + @beam.typehints.with_output_types(tuple[int, T]) class MyTransform(beam.PTransform): def expand(self, pcoll): return pcoll | beam.Map(lambda x: (len(x), x)) @@ -351,7 +351,7 @@ def expand(self, pcoll): # pylint: disable=expression-not-assigned with self.assertRaises(typehints.TypeCheckError): - words_with_lens | beam.Map(lambda x: x).with_input_types(Tuple[int, int]) + words_with_lens | beam.Map(lambda x: x).with_input_types(tuple[int, int]) def test_bad_types_annotations(self): p = TestPipeline(options=PipelineOptions(pipeline_type_check=True)) @@ -394,10 +394,10 @@ def process(self, element: int) -> Iterable[int]: # annotation has an additional Optional for the else clause. with self.assertRaises(typehints.TypeCheckError): # [START type_hints_do_fn_annotations_optional] - from typing import List, Optional + from typing import Optional class FilterEvensDoubleDoFn(beam.DoFn): - def process(self, element: int) -> Optional[List[int]]: + def process(self, element: int) -> Optional[list[int]]: if element % 2 == 0: return [element, element] return None @@ -461,7 +461,6 @@ def test_deterministic_key(self): global Player # pylint: disable=global-variable-not-assigned # [START type_hints_deterministic_key] - from typing import Tuple class Player(object): def __init__(self, team, name): @@ -487,7 +486,7 @@ def parse_player_and_score(csv): totals = ( lines | beam.Map(parse_player_and_score) - | beam.CombinePerKey(sum).with_input_types(Tuple[Player, int])) + | beam.CombinePerKey(sum).with_input_types(tuple[Player, int])) # [END type_hints_deterministic_key] assert_that( diff --git a/sdks/python/apache_beam/internal/dill_pickler.py b/sdks/python/apache_beam/internal/dill_pickler.py index e88cb3c1e138..60e309ae3a6b 100644 --- a/sdks/python/apache_beam/internal/dill_pickler.py +++ b/sdks/python/apache_beam/internal/dill_pickler.py @@ -39,8 +39,6 @@ import types import zlib from typing import Any -from typing import Dict -from typing import Tuple import dill @@ -50,7 +48,7 @@ settings = {'dill_byref': None} -patch_save_code = sys.version_info >= (3, 10) and dill.__version__ == "0.3.1.1" +patch_save_code = dill.__version__ == "0.3.1.1" if patch_save_code: # The following function is based on 'save_code' from 'dill' @@ -315,7 +313,7 @@ def save_module(pickler, obj): # Pickle module dictionaries (commonly found in lambda's globals) # by referencing their module. old_save_module_dict = dill.dill.save_module_dict - known_module_dicts: Dict[int, Tuple[types.ModuleType, Dict[str, Any]]] = {} + known_module_dicts: dict[int, tuple[types.ModuleType, dict[str, Any]]] = {} @dill.dill.register(dict) def new_save_module_dict(pickler, obj): diff --git a/sdks/python/apache_beam/internal/metrics/metric.py b/sdks/python/apache_beam/internal/metrics/metric.py index 6f6788e059bd..85d63c2b6f6b 100644 --- a/sdks/python/apache_beam/internal/metrics/metric.py +++ b/sdks/python/apache_beam/internal/metrics/metric.py @@ -30,9 +30,7 @@ import threading import time from typing import TYPE_CHECKING -from typing import Dict from typing import Optional -from typing import Type from typing import Union from apache_beam.metrics import monitoring_infos @@ -59,7 +57,7 @@ class Metrics(object): @staticmethod def counter( urn: str, - labels: Optional[Dict[str, str]] = None, + labels: Optional[dict[str, str]] = None, process_wide: bool = False) -> UserMetrics.DelegatingCounter: """Obtains or creates a Counter metric. @@ -82,14 +80,14 @@ def counter( class MetricLogger(object): """Simple object to locally aggregate and log metrics.""" def __init__(self) -> None: - self._metric: Dict[MetricName, 'MetricCell'] = {} + self._metric: dict[MetricName, 'MetricCell'] = {} self._lock = threading.Lock() self._last_logging_millis = int(time.time() * 1000) self.minimum_logging_frequency_msec = 180000 def update( self, - cell_type: Union[Type['MetricCell'], 'MetricCellFactory'], + cell_type: Union[type['MetricCell'], 'MetricCellFactory'], metric_name: MetricName, value: object) -> None: cell = self._get_metric_cell(cell_type, metric_name) @@ -97,7 +95,7 @@ def update( def _get_metric_cell( self, - cell_type: Union[Type['MetricCell'], 'MetricCellFactory'], + cell_type: Union[type['MetricCell'], 'MetricCellFactory'], metric_name: MetricName) -> 'MetricCell': with self._lock: if metric_name not in self._metric: @@ -139,7 +137,7 @@ class ServiceCallMetric(object): def __init__( self, request_count_urn: str, - base_labels: Optional[Dict[str, str]] = None) -> None: + base_labels: Optional[dict[str, str]] = None) -> None: self.base_labels = base_labels if base_labels else {} self.request_count_urn = request_count_urn diff --git a/sdks/python/apache_beam/internal/util.py b/sdks/python/apache_beam/internal/util.py index cf2b5fdbb6b3..4384e6c74481 100644 --- a/sdks/python/apache_beam/internal/util.py +++ b/sdks/python/apache_beam/internal/util.py @@ -27,11 +27,7 @@ import weakref from multiprocessing.pool import ThreadPool from typing import Any -from typing import Dict from typing import Iterable -from typing import List -from typing import Tuple -from typing import Type from typing import TypeVar from typing import Union @@ -68,9 +64,9 @@ def __hash__(self): def remove_objects_from_args( args: Iterable[Any], - kwargs: Dict[str, Any], - pvalue_class: Union[Type[T], Tuple[Type[T], ...]] -) -> Tuple[List[Any], Dict[str, Any], List[T]]: + kwargs: dict[str, Any], + pvalue_class: Union[type[T], tuple[type[T], ...]] +) -> tuple[list[Any], dict[str, Any], list[T]]: """For internal use only; no backwards-compatibility guarantees. Replaces all objects of a given type in args/kwargs with a placeholder. diff --git a/sdks/python/apache_beam/io/avroio.py b/sdks/python/apache_beam/io/avroio.py index da904bf6fb55..000b824d0ed0 100644 --- a/sdks/python/apache_beam/io/avroio.py +++ b/sdks/python/apache_beam/io/avroio.py @@ -47,8 +47,6 @@ from functools import partial from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Union import fastavro @@ -564,10 +562,10 @@ def close(self, writer): for k, v in AVRO_PRIMITIVES_TO_BEAM_PRIMITIVES.items() } -_AvroSchemaType = Union[str, List, Dict] +_AvroSchemaType = Union[str, list, dict] -def avro_union_type_to_beam_type(union_type: List) -> schema_pb2.FieldType: +def avro_union_type_to_beam_type(union_type: list) -> schema_pb2.FieldType: """convert an avro union type to a beam type if the union type is a nullable, and it is a nullable union of an avro diff --git a/sdks/python/apache_beam/io/avroio_test.py b/sdks/python/apache_beam/io/avroio_test.py index 6669b6fb8abf..80c245ad3db4 100644 --- a/sdks/python/apache_beam/io/avroio_test.py +++ b/sdks/python/apache_beam/io/avroio_test.py @@ -27,7 +27,7 @@ import shutil import tempfile import unittest -from typing import List, Any +from typing import Any import fastavro import hamcrest as hc @@ -87,7 +87,7 @@ class AvroBase(object): - _temp_files: List[str] = [] + _temp_files: list[str] = [] def __init__(self, methodName='runTest'): super().__init__(methodName) diff --git a/sdks/python/apache_beam/io/components/rate_limiter.py b/sdks/python/apache_beam/io/components/rate_limiter.py index 2dc8a5340fdb..4d6bf2f17a8c 100644 --- a/sdks/python/apache_beam/io/components/rate_limiter.py +++ b/sdks/python/apache_beam/io/components/rate_limiter.py @@ -25,8 +25,6 @@ import random import threading import time -from typing import Dict -from typing import List import grpc from envoy_data_plane.envoy.extensions.common.ratelimit.v3 import RateLimitDescriptor @@ -94,7 +92,7 @@ def __init__( self, service_address: str, domain: str, - descriptors: List[Dict[str, str]], + descriptors: list[dict[str, str]], timeout: float = 5.0, block_until_allowed: bool = True, retries: int = 3, diff --git a/sdks/python/apache_beam/io/debezium.py b/sdks/python/apache_beam/io/debezium.py index d1ca02aa68d1..5b5efe86fba9 100644 --- a/sdks/python/apache_beam/io/debezium.py +++ b/sdks/python/apache_beam/io/debezium.py @@ -80,7 +80,6 @@ import json from enum import Enum -from typing import List from typing import NamedTuple from typing import Optional @@ -109,8 +108,8 @@ class DriverClassName(Enum): 'ReadFromDebeziumSchema', [('connector_class', str), ('username', str), ('password', str), ('host', str), ('port', str), ('max_number_of_records', Optional[int]), - ('connection_properties', List[str]), - ('start_offset', Optional[List[str]]), + ('connection_properties', list[str]), + ('start_offset', Optional[list[str]]), ('offset_storage_path', Optional[str])]) diff --git a/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py b/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py index 0e967f1beec3..848fc043a4db 100644 --- a/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py +++ b/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py @@ -117,7 +117,7 @@ class CrossLanguageJdbcIOTest(unittest.TestCase): DbData = typing.NamedTuple( 'DbData', - [('container_fn', typing.Any), ('classpath', typing.List[str]), + [('container_fn', typing.Any), ('classpath', list[str]), ('db_string', str), ('connector', str)]) DB_CONTAINER_CLASSPATH_STRING = { 'postgres': DbData( diff --git a/sdks/python/apache_beam/io/external/xlang_kafkaio_it_test.py b/sdks/python/apache_beam/io/external/xlang_kafkaio_it_test.py index 23178b0ee363..7fc2f4351cc8 100644 --- a/sdks/python/apache_beam/io/external/xlang_kafkaio_it_test.py +++ b/sdks/python/apache_beam/io/external/xlang_kafkaio_it_test.py @@ -81,7 +81,7 @@ def build_write_pipeline(self, pipeline): | 'Generate' >> beam.Create(range(NUM_RECORDS)) # pylint: disable=bad-option-value | 'MakeKV' >> beam.Map( lambda x: (None if self.null_key else b'key', str(x).encode())). - with_output_types(typing.Tuple[typing.Optional[bytes], bytes]) + with_output_types(tuple[typing.Optional[bytes], bytes]) | 'WriteToKafka' >> WriteToKafka( producer_config={'bootstrap.servers': self.bootstrap_servers}, topic=self.topic, diff --git a/sdks/python/apache_beam/io/external/xlang_kafkaio_perf_test.py b/sdks/python/apache_beam/io/external/xlang_kafkaio_perf_test.py index 50703144d109..34447748c276 100644 --- a/sdks/python/apache_beam/io/external/xlang_kafkaio_perf_test.py +++ b/sdks/python/apache_beam/io/external/xlang_kafkaio_perf_test.py @@ -17,7 +17,6 @@ import logging import sys -import typing import apache_beam as beam from apache_beam.io import iobase @@ -78,7 +77,7 @@ def test(self): self.pipeline | 'Generate records' >> iobase.Read( SyntheticSource(self.parse_synthetic_source_options())) \ - .with_output_types(typing.Tuple[bytes, bytes]) + .with_output_types(tuple[bytes, bytes]) | 'Count records' >> beam.ParDo(CountMessages(self.metrics_namespace)) | 'Avoid Fusion' >> Reshuffle() | 'Measure time' >> beam.ParDo(MeasureTime(self.metrics_namespace)) diff --git a/sdks/python/apache_beam/io/filebasedio_perf_test.py b/sdks/python/apache_beam/io/filebasedio_perf_test.py index 78a390d9bed4..75ed67313a41 100644 --- a/sdks/python/apache_beam/io/filebasedio_perf_test.py +++ b/sdks/python/apache_beam/io/filebasedio_perf_test.py @@ -20,7 +20,6 @@ import logging import sys import uuid -from typing import Tuple import apache_beam as beam from apache_beam import typehints @@ -69,7 +68,7 @@ def _add_argparse_args(cls, parser): @typehints.with_output_types(bytes) -@typehints.with_input_types(Tuple[bytes, bytes]) +@typehints.with_input_types(tuple[bytes, bytes]) class SyntheticRecordToStrFn(beam.DoFn): """ A DoFn that convert key-value bytes from synthetic source to string record. diff --git a/sdks/python/apache_beam/io/filebasedsource.py b/sdks/python/apache_beam/io/filebasedsource.py index b80e4fb8a841..a766f0554ea2 100644 --- a/sdks/python/apache_beam/io/filebasedsource.py +++ b/sdks/python/apache_beam/io/filebasedsource.py @@ -30,7 +30,6 @@ from typing import Callable from typing import Iterable -from typing import Tuple from typing import Union from apache_beam.internal import pickler @@ -348,7 +347,7 @@ def __init__( self._size_track = None def process(self, element: Union[str, FileMetadata], *args, - **kwargs) -> Iterable[Tuple[FileMetadata, OffsetRange]]: + **kwargs) -> Iterable[tuple[FileMetadata, OffsetRange]]: if isinstance(element, FileMetadata): metadata_list = [element] else: diff --git a/sdks/python/apache_beam/io/fileio.py b/sdks/python/apache_beam/io/fileio.py index 3251e567763f..7c8350743267 100644 --- a/sdks/python/apache_beam/io/fileio.py +++ b/sdks/python/apache_beam/io/fileio.py @@ -97,11 +97,7 @@ from typing import Any from typing import BinaryIO # pylint: disable=unused-import from typing import Callable -from typing import DefaultDict -from typing import Dict from typing import Iterable -from typing import List -from typing import Tuple from typing import Union import apache_beam as beam @@ -164,7 +160,7 @@ class _MatchAllFn(beam.DoFn): def __init__(self, empty_match_treatment): self._empty_match_treatment = empty_match_treatment - def process(self, file_pattern: str) -> List[filesystem.FileMetadata]: + def process(self, file_pattern: str) -> list[filesystem.FileMetadata]: # TODO: Should we batch the lookups? match_results = filesystems.FileSystems.match([file_pattern]) match_result = match_results[0] @@ -647,7 +643,7 @@ def expand(self, pcoll): def _create_writer( base_path, - writer_key: Tuple[str, IntervalWindow], + writer_key: tuple[str, IntervalWindow], create_metadata_fn: CreateFileMetadataFn, ): try: @@ -800,8 +796,9 @@ def __init__(self, destination: Callable[[Any], str], shards: int): self.shards = shards # We start the shards for a single destination at an arbitrary point. - self._shard_counter: DefaultDict[str, int] = collections.defaultdict( - lambda: random.randrange(self.shards)) + self._shard_counter: collections.defaultdict[ + str, + int] = collections.defaultdict(lambda: random.randrange(self.shards)) def _next_shard_for_destination(self, destination): self._shard_counter[destination] = ((self._shard_counter[destination] + 1) % @@ -821,9 +818,9 @@ class _WriteUnshardedRecordsFn(beam.DoFn): SPILLED_RECORDS = 'spilled_records' WRITTEN_FILES = 'written_files' - _writers_and_sinks: Dict[Tuple[str, BoundedWindow], Tuple[BinaryIO, + _writers_and_sinks: dict[tuple[str, BoundedWindow], tuple[BinaryIO, FileSink]] = None - _file_names: Dict[Tuple[str, BoundedWindow], str] = None + _file_names: dict[tuple[str, BoundedWindow], str] = None def __init__( self, @@ -903,7 +900,7 @@ class _RemoveDuplicates(beam.DoFn): def process( self, - element: Tuple[str, filesystem.FileMetadata], + element: tuple[str, filesystem.FileMetadata], count_state=beam.DoFn.StateParam(COUNT_STATE) ) -> Iterable[filesystem.FileMetadata]: @@ -927,7 +924,7 @@ class _RemoveOldDuplicates(beam.DoFn): def process( self, - element: Tuple[str, filesystem.FileMetadata], + element: tuple[str, filesystem.FileMetadata], time_state=beam.DoFn.StateParam(TIME_STATE) ) -> Iterable[filesystem.FileMetadata]: path = element[0] diff --git a/sdks/python/apache_beam/io/filesystem.py b/sdks/python/apache_beam/io/filesystem.py index e67337cf65ee..bfddb42dd912 100644 --- a/sdks/python/apache_beam/io/filesystem.py +++ b/sdks/python/apache_beam/io/filesystem.py @@ -36,9 +36,7 @@ import zlib from typing import BinaryIO # pylint: disable=unused-import from typing import Iterator -from typing import List from typing import Optional -from typing import Tuple import zstandard @@ -486,7 +484,7 @@ class MatchResult(object): """Result from the ``FileSystem`` match operation which contains the list of matched ``FileMetadata``. """ - def __init__(self, pattern: str, metadata_list: List[FileMetadata]) -> None: + def __init__(self, pattern: str, metadata_list: list[FileMetadata]) -> None: self.metadata_list = metadata_list self.pattern = pattern @@ -553,7 +551,7 @@ def join(self, basepath: str, *paths: str) -> str: raise NotImplementedError @abc.abstractmethod - def split(self, path: str) -> Tuple[str, str]: + def split(self, path: str) -> tuple[str, str]: """Splits the given path into two parts. Splits the path into a pair (head, tail) such that tail contains the last @@ -626,7 +624,7 @@ def _url_dirname(self, url_or_path): scheme, path = self._split_scheme(url_or_path) return self._combine_scheme(scheme, posixpath.dirname(path)) - def match_files(self, file_metas: List[FileMetadata], + def match_files(self, file_metas: list[FileMetadata], pattern: str) -> Iterator[FileMetadata]: """Filter :class:`FileMetadata` objects by *pattern* diff --git a/sdks/python/apache_beam/io/flink/flink_streaming_impulse_source.py b/sdks/python/apache_beam/io/flink/flink_streaming_impulse_source.py index 91c76b5d54bf..4b1cf6265a30 100644 --- a/sdks/python/apache_beam/io/flink/flink_streaming_impulse_source.py +++ b/sdks/python/apache_beam/io/flink/flink_streaming_impulse_source.py @@ -24,7 +24,6 @@ import json from typing import Any -from typing import Dict from apache_beam import PTransform from apache_beam import Windowing @@ -35,7 +34,7 @@ class FlinkStreamingImpulseSource(PTransform): URN = "flink:transform:streaming_impulse:v1" - config: Dict[str, Any] = {} + config: dict[str, Any] = {} def expand(self, pbegin): assert isinstance(pbegin, pvalue.PBegin), ( diff --git a/sdks/python/apache_beam/io/gcp/bigquery.py b/sdks/python/apache_beam/io/gcp/bigquery.py index 181c891c1b65..e1bb5583f38b 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery.py +++ b/sdks/python/apache_beam/io/gcp/bigquery.py @@ -366,10 +366,7 @@ def chain_after(result): import uuid import warnings from dataclasses import dataclass -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Union import fastavro @@ -638,7 +635,7 @@ def _BigQuerySource(*args, **kwargs): @dataclass class _BigQueryExportResult: coder: beam.coders.Coder - paths: List[str] + paths: list[str] class _CustomBigQuerySource(BoundedSource): @@ -975,12 +972,12 @@ def __init__( dataset: Optional[str] = None, project: Optional[str] = None, query: Optional[str] = None, - selected_fields: Optional[List[str]] = None, + selected_fields: Optional[list[str]] = None, row_restriction: Optional[str] = None, pipeline_options: Optional[GoogleCloudOptions] = None, unique_id: Optional[uuid.UUID] = None, - bigquery_job_labels: Optional[Dict] = None, - bigquery_dataset_labels: Optional[Dict] = None, + bigquery_job_labels: Optional[dict] = None, + bigquery_dataset_labels: Optional[dict] = None, job_name: Optional[str] = None, step_name: Optional[str] = None, use_standard_sql: Optional[bool] = False, @@ -1607,7 +1604,7 @@ def _create_table_if_needed(self, table_reference, schema=None): additional_create_parameters=self.additional_bq_parameters) _KNOWN_TABLES.add(str_table_reference) - def _check_row_size(self, row_and_insert_id) -> Tuple[int, Optional[str]]: + def _check_row_size(self, row_and_insert_id) -> tuple[int, Optional[str]]: """Returns error string when the row estimated size is too big""" row_byte_size = get_deep_size(row_and_insert_id) @@ -2007,7 +2004,7 @@ def __init__( max_insert_payload_size=MAX_INSERT_PAYLOAD_SIZE, num_streaming_keys=DEFAULT_SHARDS_PER_DESTINATION, use_cdc_writes: bool = False, - primary_key: List[str] = None, + primary_key: list[str] = None, expansion_service=None, big_lake_configuration=None): """Initialize a WriteToBigQuery transform. @@ -2488,13 +2485,13 @@ class WriteResult: def __init__( self, method: str = None, - destination_load_jobid_pairs: PCollection[Tuple[str, + destination_load_jobid_pairs: PCollection[tuple[str, JobReference]] = None, - destination_file_pairs: PCollection[Tuple[str, Tuple[str, int]]] = None, - destination_copy_jobid_pairs: PCollection[Tuple[str, + destination_file_pairs: PCollection[tuple[str, tuple[str, int]]] = None, + destination_copy_jobid_pairs: PCollection[tuple[str, JobReference]] = None, - failed_rows: PCollection[Tuple[str, dict]] = None, - failed_rows_with_errors: PCollection[Tuple[str, dict, list]] = None): + failed_rows: PCollection[tuple[str, dict]] = None, + failed_rows_with_errors: PCollection[tuple[str, dict, list]] = None): self._method = method self._destination_load_jobid_pairs = destination_load_jobid_pairs @@ -2525,7 +2522,7 @@ def validate(self, valid_methods, attribute): @property def destination_load_jobid_pairs( - self) -> PCollection[Tuple[str, JobReference]]: + self) -> PCollection[tuple[str, JobReference]]: """A ``FILE_LOADS`` method attribute Returns: A PCollection of the table destinations that were successfully @@ -2539,7 +2536,7 @@ def destination_load_jobid_pairs( return self._destination_load_jobid_pairs @property - def destination_file_pairs(self) -> PCollection[Tuple[str, Tuple[str, int]]]: + def destination_file_pairs(self) -> PCollection[tuple[str, tuple[str, int]]]: """A ``FILE_LOADS`` method attribute Returns: A PCollection of the table destinations along with the @@ -2553,7 +2550,7 @@ def destination_file_pairs(self) -> PCollection[Tuple[str, Tuple[str, int]]]: @property def destination_copy_jobid_pairs( - self) -> PCollection[Tuple[str, JobReference]]: + self) -> PCollection[tuple[str, JobReference]]: """A ``FILE_LOADS`` method attribute Returns: A PCollection of the table destinations that were successfully @@ -2567,7 +2564,7 @@ def destination_copy_jobid_pairs( return self._destination_copy_jobid_pairs @property - def failed_rows(self) -> PCollection[Tuple[str, dict]]: + def failed_rows(self) -> PCollection[tuple[str, dict]]: """A ``[STREAMING_INSERTS, STORAGE_WRITE_API]`` method attribute Returns: A PCollection of rows that failed when inserting to BigQuery. @@ -2583,7 +2580,7 @@ def failed_rows(self) -> PCollection[Tuple[str, dict]]: return self._failed_rows @property - def failed_rows_with_errors(self) -> PCollection[Tuple[str, dict, list]]: + def failed_rows_with_errors(self) -> PCollection[tuple[str, dict, list]]: """A ``[STREAMING_INSERTS, STORAGE_WRITE_API]`` method attribute Returns: @@ -2642,7 +2639,7 @@ def __init__( with_auto_sharding=False, num_storage_api_streams=0, use_cdc_writes: bool = False, - primary_key: List[str] = None, + primary_key: list[str] = None, big_lake_configuration=None, expansion_service=None): self._table = table @@ -3180,7 +3177,7 @@ def __init__( validate: bool = False, kms_key: str = None, temp_dataset: Union[str, DatasetReference] = None, - bigquery_job_labels: Dict[str, str] = None, + bigquery_job_labels: dict[str, str] = None, query_priority: str = BigQueryQueryPriority.BATCH): if gcs_location: if not isinstance(gcs_location, (str, ValueProvider)): diff --git a/sdks/python/apache_beam/io/gcp/bigquery_avro_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_avro_tools.py index b6c177fc7418..ceab52444bb1 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_avro_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_avro_tools.py @@ -24,7 +24,6 @@ """ from typing import Any -from typing import Dict # BigQuery types as listed in # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types @@ -67,8 +66,8 @@ def get_record_schema_from_dict_table_schema( schema_name: str, - table_schema: Dict[str, Any], - namespace: str = "apache_beam.io.gcp.bigquery") -> Dict[str, Any]: + table_schema: dict[str, Any], + namespace: str = "apache_beam.io.gcp.bigquery") -> dict[str, Any]: # noqa: F821 """Convert a table schema into an Avro schema. @@ -95,8 +94,8 @@ def get_record_schema_from_dict_table_schema( } -def table_field_to_avro_field(table_field: Dict[str, Any], - namespace: str) -> Dict[str, Any]: +def table_field_to_avro_field(table_field: dict[str, Any], + namespace: str) -> dict[str, Any]: # noqa: F821 """Convert a BigQuery field to an avro field. diff --git a/sdks/python/apache_beam/io/gcp/bigquery_change_history.py b/sdks/python/apache_beam/io/gcp/bigquery_change_history.py index dad56d26e499..f0a23ddce02a 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_change_history.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_change_history.py @@ -50,11 +50,8 @@ import time import uuid from typing import Any -from typing import Dict from typing import Iterable -from typing import List from typing import Optional -from typing import Tuple import apache_beam as beam from apache_beam.io.gcp import bigquery_tools @@ -155,7 +152,7 @@ class _StreamRestriction: __slots__ = ('stream_names', 'range') def __init__( - self, stream_names: Tuple[str, ...], start: int, stop: int) -> None: + self, stream_names: tuple[str, ...], start: int, stop: int) -> None: self.stream_names = stream_names # tuple of BQ stream name strings self.range = OffsetRange(start, stop) @@ -201,7 +198,7 @@ def try_claim(self, position: int) -> bool: def try_split( self, fraction_of_remainder: float - ) -> Optional[Tuple[_StreamRestriction, _StreamRestriction]]: + ) -> Optional[tuple[_StreamRestriction, _StreamRestriction]]: result = self._offset_tracker.try_split(fraction_of_remainder) if result is not None: primary, residual = result @@ -228,7 +225,7 @@ class _NonSplittableOffsetTracker(OffsetRestrictionTracker): """ def try_split( self, fraction_of_remainder: float - ) -> Optional[Tuple[OffsetRange, OffsetRange]]: + ) -> Optional[tuple[OffsetRange, OffsetRange]]: if fraction_of_remainder == 0: return super().try_split(fraction_of_remainder) return None @@ -250,7 +247,7 @@ class _PollWatermarkEstimator(WatermarkEstimator): State is checkpointed as (watermark_hold, last_end) so both values survive SDF re-dispatch. """ - def __init__(self, state: Tuple[Timestamp, Timestamp]) -> None: + def __init__(self, state: tuple[Timestamp, Timestamp]) -> None: self._watermark_hold, self._last_end = state def observe_timestamp(self, timestamp: Timestamp) -> None: @@ -259,7 +256,7 @@ def observe_timestamp(self, timestamp: Timestamp) -> None: def current_watermark(self) -> Timestamp: return self._watermark_hold - def get_estimator_state(self) -> Tuple[Timestamp, Timestamp]: + def get_estimator_state(self) -> tuple[Timestamp, Timestamp]: return (self._watermark_hold, self._last_end) def set_watermark(self, timestamp: Timestamp) -> None: @@ -293,11 +290,11 @@ class _PollWatermarkEstimatorProvider(WatermarkEstimatorProvider): """ def initial_estimator_state( self, element: _PollConfig, - restriction: OffsetRange) -> Tuple[Timestamp, Timestamp]: + restriction: OffsetRange) -> tuple[Timestamp, Timestamp]: return (element.start_time, element.start_time) def create_watermark_estimator( - self, estimator_state: Tuple[Timestamp, + self, estimator_state: tuple[Timestamp, Timestamp]) -> _PollWatermarkEstimator: return _PollWatermarkEstimator(estimator_state) @@ -309,7 +306,7 @@ def build_changes_query( change_function: str, change_type_column: str = 'change_type', change_timestamp_column: str = 'change_timestamp', - columns: Optional[List[str]] = None, + columns: Optional[list[str]] = None, row_filter: Optional[str] = None) -> str: """Build a CHANGES() or APPENDS() SQL query. @@ -353,7 +350,7 @@ def build_changes_query( def compute_ranges(start: Timestamp, end: Timestamp, - change_function: str) -> List[Tuple[Timestamp, Timestamp]]: + change_function: str) -> list[tuple[Timestamp, Timestamp]]: """Split [start, end) into query-safe chunks. CHANGES() has a max 1-day range. APPENDS() has no limit. @@ -571,7 +568,7 @@ def __init__( location: Optional[str], change_type_column: str = 'change_type', change_timestamp_column: str = 'change_timestamp', - columns: Optional[List[str]] = None, + columns: Optional[list[str]] = None, row_filter: Optional[str] = None) -> None: self._table = table self._project = project @@ -735,8 +732,8 @@ def setup(self) -> None: self._ensure_client() def _split_all_streams( - self, stream_names: Tuple[str, ...], - max_split_rounds: int) -> Tuple[str, ...]: + self, stream_names: tuple[str, ...], + max_split_rounds: int) -> tuple[str, ...]: """Split each stream at fraction=0.5 for up to max_split_rounds rounds. Each round attempts to split every stream in the current list. A @@ -952,7 +949,7 @@ def _create_read_session(self, table_ref: 'bigquery.TableReference') -> Any: len(session.streams)) return session - def _read_stream(self, stream_name: str) -> Iterable[Dict[str, Any]]: + def _read_stream(self, stream_name: str) -> Iterable[dict[str, Any]]: """Read all rows from a single Storage API stream as dicts. When batch_arrow_read is enabled, converts entire Arrow RecordBatches @@ -966,7 +963,7 @@ def _read_stream(self, stream_name: str) -> Iterable[Dict[str, Any]]: yield from self._read_stream_row_by_row(stream_name) def _read_stream_row_by_row(self, - stream_name: str) -> Iterable[Dict[str, Any]]: + stream_name: str) -> Iterable[dict[str, Any]]: """Row-by-row Arrow conversion (lower memory than batch mode).""" t0 = time.time() row_count = 0 @@ -980,7 +977,7 @@ def _read_stream_row_by_row(self, elapsed, row_count / elapsed if elapsed > 0 else 0) - def _read_stream_batch(self, stream_name: str) -> Iterable[Dict[str, Any]]: + def _read_stream_batch(self, stream_name: str) -> Iterable[dict[str, Any]]: """Batch-convert Arrow RecordBatches for high throughput.""" schema = None row_count = 0 @@ -1002,7 +999,7 @@ def _read_stream_batch(self, stream_name: str) -> Iterable[Dict[str, Any]]: elapsed, row_count / elapsed if elapsed > 0 else 0) - def _read_stream_raw(self, stream_name: str) -> Iterable[Tuple[bytes, bytes]]: + def _read_stream_raw(self, stream_name: str) -> Iterable[tuple[bytes, bytes]]: """Yield raw (schema_bytes, batch_bytes) without decompression. Used when emit_raw_batches is enabled to defer decompression and @@ -1034,7 +1031,7 @@ class _DecompressArrowBatchesFn(beam.DoFn): def __init__(self, change_timestamp_column: str = 'change_timestamp') -> None: self._change_timestamp_column = change_timestamp_column - def process(self, element: Tuple[bytes, bytes]) -> Iterable[Dict[str, Any]]: + def process(self, element: tuple[bytes, bytes]) -> Iterable[dict[str, Any]]: schema_bytes, batch_bytes = element schema = pyarrow.ipc.read_schema(pyarrow.py_buffer(schema_bytes)) batch = pyarrow.ipc.read_record_batch( @@ -1077,7 +1074,7 @@ def setup(self) -> None: def process( self, - element: Tuple[str, Tuple[int, int]], + element: tuple[str, tuple[int, int]], streams_read=beam.DoFn.StateParam(STREAMS_READ) ) -> None: table_key = element[0] @@ -1194,7 +1191,7 @@ def __init__( location: Optional[str] = None, change_type_column: str = 'change_type', change_timestamp_column: str = 'change_timestamp', - columns: Optional[List[str]] = None, + columns: Optional[list[str]] = None, row_filter: Optional[str] = None, batch_arrow_read: bool = True, max_split_rounds: int = 1, @@ -1322,9 +1319,9 @@ def expand(self, pbegin: beam.pvalue.PBegin) -> beam.PCollection: max_split_rounds=self._max_split_rounds, emit_raw_batches=emit_raw)) if emit_raw: - read_sdf = read_sdf.with_output_types(Tuple[bytes, bytes]) + read_sdf = read_sdf.with_output_types(tuple[bytes, bytes]) else: - read_sdf = read_sdf.with_output_types(Dict[str, Any]) + read_sdf = read_sdf.with_output_types(dict[str, Any]) read_outputs = ( query_results diff --git a/sdks/python/apache_beam/io/gcp/bigquery_read_internal.py b/sdks/python/apache_beam/io/gcp/bigquery_read_internal.py index 6432f3b4eeac..136b3cc56b7e 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_read_internal.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_read_internal.py @@ -29,9 +29,7 @@ import uuid from typing import TYPE_CHECKING from typing import Any -from typing import Dict from typing import Iterable -from typing import List from typing import Optional from typing import Union @@ -197,7 +195,7 @@ def __init__( gcs_location: Union[str, ValueProvider] = None, validate: bool = False, use_json_exports: bool = False, - bigquery_job_labels: Dict[str, str] = None, + bigquery_job_labels: dict[str, str] = None, step_name: str = None, job_name: str = None, unique_id: str = None, @@ -462,7 +460,7 @@ def decode(self, value): value = json.loads(value.decode('utf-8')) return self._decode_row(value, self.fields) - def _decode_row(self, row: Dict[str, Any], schema_fields: List[FieldSchema]): + def _decode_row(self, row: dict[str, Any], schema_fields: list[FieldSchema]): for field in schema_fields: if field.name not in row: # The field exists in the schema, but it doesn't exist in this row. diff --git a/sdks/python/apache_beam/io/gcp/bigquery_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_tools.py index 1a7a07706a39..a16d10003047 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_tools.py @@ -40,7 +40,6 @@ from json.decoder import JSONDecodeError from typing import Optional from typing import Sequence -from typing import Tuple from typing import TypeVar from typing import Union @@ -180,7 +179,7 @@ def get_hashable_destination(destination): def to_hashable_table_ref( - table_ref_elem_kv: Tuple[Union[str, TableReference], V]) -> Tuple[str, V]: + table_ref_elem_kv: tuple[Union[str, TableReference], V]) -> tuple[str, V]: """Turns the key of the input tuple to its string representation. The key should be either a string or a TableReference. diff --git a/sdks/python/apache_beam/io/gcp/bigtableio.py b/sdks/python/apache_beam/io/gcp/bigtableio.py index f10039e564d1..38b507aaed29 100644 --- a/sdks/python/apache_beam/io/gcp/bigtableio.py +++ b/sdks/python/apache_beam/io/gcp/bigtableio.py @@ -39,8 +39,6 @@ import logging import struct -from typing import Dict -from typing import List import apache_beam as beam from apache_beam.internal.metrics.metric import ServiceCallMetric @@ -262,7 +260,7 @@ def expand(self, input): input | beam.ParDo(self._DirectRowMutationsToBeamRow()).with_output_types( RowTypeConstraint.from_fields( - [("key", bytes), ("mutations", List[Dict[str, bytes]])])) + [("key", bytes), ("mutations", list[dict[str, bytes]])])) | external_write) else: return ( diff --git a/sdks/python/apache_beam/io/gcp/datastore/v1new/helper.py b/sdks/python/apache_beam/io/gcp/datastore/v1new/helper.py index f66bf2e56405..9c493afe3837 100644 --- a/sdks/python/apache_beam/io/gcp/datastore/v1new/helper.py +++ b/sdks/python/apache_beam/io/gcp/datastore/v1new/helper.py @@ -25,7 +25,6 @@ import os import uuid -from typing import List from typing import Union from cachetools.func import ttl_cache @@ -70,7 +69,7 @@ def retry_on_rpc_error(exception): def create_entities(count, id_or_name=False): """Creates a list of entities with random keys.""" if id_or_name: - ids_or_names: List[Union[str, int]] = [ + ids_or_names: list[Union[str, int]] = [ uuid.uuid4().int & ((1 << 63) - 1) for _ in range(count) ] else: diff --git a/sdks/python/apache_beam/io/gcp/datastore/v1new/types.py b/sdks/python/apache_beam/io/gcp/datastore/v1new/types.py index f7ce69099ca0..3db3d669c73b 100644 --- a/sdks/python/apache_beam/io/gcp/datastore/v1new/types.py +++ b/sdks/python/apache_beam/io/gcp/datastore/v1new/types.py @@ -23,7 +23,6 @@ import copy from typing import Iterable -from typing import List from typing import Optional from typing import Union @@ -154,7 +153,7 @@ def __repr__(self): class Key(object): def __init__( self, - path_elements: List[Union[str, int]], + path_elements: list[Union[str, int]], parent: Optional['Key'] = None, project: Optional[str] = None, namespace: Optional[str] = None): diff --git a/sdks/python/apache_beam/io/gcp/experimental/spannerio.py b/sdks/python/apache_beam/io/gcp/experimental/spannerio.py index 04800ff015c8..c6f786604090 100644 --- a/sdks/python/apache_beam/io/gcp/experimental/spannerio.py +++ b/sdks/python/apache_beam/io/gcp/experimental/spannerio.py @@ -323,7 +323,7 @@ def snapshot_options(self): @with_input_types(ReadOperation, _SPANNER_TRANSACTION) -@with_output_types(typing.List[typing.Any]) +@with_output_types(list[typing.Any]) class _NaiveSpannerReadDoFn(DoFn): def __init__(self, spanner_configuration): """ @@ -439,7 +439,7 @@ def process(self, element, spanner_transaction): @with_input_types(ReadOperation) -@with_output_types(typing.Dict[typing.Any, typing.Any]) +@with_output_types(dict[typing.Any, typing.Any]) class _CreateReadPartitions(DoFn): """ A DoFn to create partitions. Uses the Partitioning API (PartitionRead / @@ -584,8 +584,8 @@ def create_transaction( exact_staleness))) -@with_input_types(typing.Dict[typing.Any, typing.Any]) -@with_output_types(typing.List[typing.Any]) +@with_input_types(dict[typing.Any, typing.Any]) +@with_output_types(list[typing.Any]) class _ReadFromPartitionFn(DoFn): """ A DoFn to perform reads from the partition. diff --git a/sdks/python/apache_beam/io/gcp/pubsub.py b/sdks/python/apache_beam/io/gcp/pubsub.py index 59eadee5538e..276103f52760 100644 --- a/sdks/python/apache_beam/io/gcp/pubsub.py +++ b/sdks/python/apache_beam/io/gcp/pubsub.py @@ -34,10 +34,8 @@ import re from typing import Any -from typing import List from typing import NamedTuple from typing import Optional -from typing import Tuple from typing import Union from apache_beam import coders @@ -467,7 +465,7 @@ def display_data(self): TOPIC_REGEXP = 'projects/([^/]+)/topics/(.+)' -def parse_topic(full_topic: str) -> Tuple[str, str]: +def parse_topic(full_topic: str) -> tuple[str, str]: match = re.match(TOPIC_REGEXP, full_topic) if not match: raise ValueError( @@ -754,7 +752,7 @@ class MultipleReadFromPubSub(PTransform): """ def __init__( self, - pubsub_source_descriptors: List[PubSubSourceDescriptor], + pubsub_source_descriptors: list[PubSubSourceDescriptor], with_attributes: bool = False, ): """Initializes ``PubSubMultipleReader``. diff --git a/sdks/python/apache_beam/io/iobase.py b/sdks/python/apache_beam/io/iobase.py index 67d6cd358a07..afc977406af0 100644 --- a/sdks/python/apache_beam/io/iobase.py +++ b/sdks/python/apache_beam/io/iobase.py @@ -39,7 +39,6 @@ from typing import Any from typing import Iterator from typing import Optional -from typing import Tuple from typing import Union import apache_beam as beam @@ -975,7 +974,7 @@ def display_data(self): def to_runner_api_parameter( self, context: PipelineContext, - ) -> Tuple[str, Any]: + ) -> tuple[str, Any]: from apache_beam.io.gcp.pubsub import _PubSubSource if isinstance(self.source, _PubSubSource): return ( @@ -1120,7 +1119,7 @@ def expand(self, pcoll): def to_runner_api_parameter( self, context: PipelineContext, - ) -> Tuple[str, Any]: + ) -> tuple[str, Any]: # TODO(BEAM-27443): Remove the need for special casing here. # Importing locally to prevent circular dependencies. from apache_beam.io.gcp.pubsub import _PubSubSink diff --git a/sdks/python/apache_beam/io/jdbc.py b/sdks/python/apache_beam/io/jdbc.py index 741507634539..20792fe858e2 100644 --- a/sdks/python/apache_beam/io/jdbc.py +++ b/sdks/python/apache_beam/io/jdbc.py @@ -123,7 +123,7 @@ def default_io_expansion_service(classpath=None): 'Config', [('driver_class_name', str), ('jdbc_url', str), ('username', str), ('password', str), ('connection_properties', typing.Optional[str]), - ('connection_init_sqls', typing.Optional[typing.List[str]]), + ('connection_init_sqls', typing.Optional[list[str]]), ('read_query', typing.Optional[str]), ('write_statement', typing.Optional[str]), ('fetch_size', typing.Optional[np.int16]), diff --git a/sdks/python/apache_beam/io/kafka.py b/sdks/python/apache_beam/io/kafka.py index b1847544d395..f7cdd7f7b876 100644 --- a/sdks/python/apache_beam/io/kafka.py +++ b/sdks/python/apache_beam/io/kafka.py @@ -111,9 +111,9 @@ ReadFromKafkaSchema = typing.NamedTuple( 'ReadFromKafkaSchema', - [('consumer_config', typing.Mapping[str, str]), - ('topics', typing.List[str]), ('key_deserializer', str), - ('value_deserializer', str), ('start_read_time', typing.Optional[int]), + [('consumer_config', typing.Mapping[str, str]), ('topics', list[str]), + ('key_deserializer', str), ('value_deserializer', str), + ('start_read_time', typing.Optional[int]), ('max_num_records', typing.Optional[int]), ('max_read_time', typing.Optional[int]), ('commit_offset_in_finalize', bool), ('timestamp_policy', str), diff --git a/sdks/python/apache_beam/io/requestresponse.py b/sdks/python/apache_beam/io/requestresponse.py index e53fa07471af..9fdf33e2299d 100644 --- a/sdks/python/apache_beam/io/requestresponse.py +++ b/sdks/python/apache_beam/io/requestresponse.py @@ -26,12 +26,9 @@ import time from datetime import timedelta from typing import Any -from typing import Dict from typing import Generic -from typing import List from typing import Mapping from typing import Optional -from typing import Tuple from typing import TypeVar from typing import Union @@ -294,7 +291,7 @@ class _FilterCacheReadFn(beam.DoFn): It emits to main output for successful cache read requests or to the tagged output - `cache_misses` - otherwise.""" - def process(self, element: Tuple[RequestT, ResponseT], *args, **kwargs): + def process(self, element: tuple[RequestT, ResponseT], *args, **kwargs): if not element[1]: yield pvalue.TaggedOutput('cache_misses', element[0]) else: @@ -455,7 +452,7 @@ def __init__( *, request_coder: Optional[coders.Coder], response_coder: Optional[coders.Coder], - kwargs: Optional[Dict[str, Any]] = None, + kwargs: Optional[dict[str, Any]] = None, source_caller: Optional[Caller] = None, mode: _RedisMode, ): @@ -538,13 +535,13 @@ def _write_cache(self, element): def __call__(self, element, *args, **kwargs): if self.mode == _RedisMode.READ: - if isinstance(element, List): + if isinstance(element, list): responses = [self._read_cache(e) for e in element] return responses else: return self._read_cache(element) else: - if isinstance(element, List): + if isinstance(element, list): responses = [self._write_cache(e) for e in element] return responses else: @@ -563,7 +560,7 @@ def __init__( port: int, time_to_live: Union[int, timedelta], *, - kwargs: Optional[Dict[str, Any]] = None, + kwargs: Optional[dict[str, Any]] = None, request_coder: Optional[coders.Coder], response_coder: Optional[coders.Coder], source_caller: Optional[Caller[RequestT, ResponseT]] = None, @@ -602,7 +599,7 @@ def expand( return requests | RequestResponseIO(self.redis_caller) -class _WriteToRedis(beam.PTransform[beam.PCollection[Tuple[RequestT, +class _WriteToRedis(beam.PTransform[beam.PCollection[tuple[RequestT, ResponseT]], beam.PCollection[ResponseT]]): """A `PTransfrom` that performs write to Redis cache.""" @@ -612,7 +609,7 @@ def __init__( port: int, time_to_live: Union[int, timedelta], *, - kwargs: Optional[Dict[str, Any]] = None, + kwargs: Optional[dict[str, Any]] = None, request_coder: Optional[coders.Coder], response_coder: Optional[coders.Coder], source_caller: Optional[Caller[RequestT, ResponseT]] = None, @@ -646,7 +643,7 @@ def __init__( mode=_RedisMode.WRITE) def expand( - self, elements: beam.PCollection[Tuple[RequestT, ResponseT]] + self, elements: beam.PCollection[tuple[RequestT, ResponseT]] ) -> beam.PCollection[ResponseT]: return elements | RequestResponseIO(self.redis_caller) diff --git a/sdks/python/apache_beam/io/requestresponse_it_test.py b/sdks/python/apache_beam/io/requestresponse_it_test.py index 8703653b266e..072a1dc27b9f 100644 --- a/sdks/python/apache_beam/io/requestresponse_it_test.py +++ b/sdks/python/apache_beam/io/requestresponse_it_test.py @@ -21,7 +21,6 @@ import typing import unittest from dataclasses import dataclass -from typing import Tuple from typing import Union import pytest @@ -155,7 +154,7 @@ def setUpClass(cls) -> None: cls.client = EchoHTTPCaller(http_endpoint_address) @classmethod - def _get_client_and_options(cls) -> Tuple[EchoHTTPCaller, EchoITOptions]: + def _get_client_and_options(cls) -> tuple[EchoHTTPCaller, EchoITOptions]: assert cls.options is not None assert cls.client is not None return cls.client, cls.options diff --git a/sdks/python/apache_beam/io/restriction_trackers.py b/sdks/python/apache_beam/io/restriction_trackers.py index 4b819e87a8d6..7f4cf14747fa 100644 --- a/sdks/python/apache_beam/io/restriction_trackers.py +++ b/sdks/python/apache_beam/io/restriction_trackers.py @@ -18,8 +18,6 @@ """`iobase.RestrictionTracker` implementations provided with Apache Beam.""" # pytype: skip-file -from typing import Tuple - from apache_beam.io.iobase import RestrictionProgress from apache_beam.io.iobase import RestrictionTracker from apache_beam.io.range_trackers import OffsetRangeTracker @@ -62,7 +60,7 @@ def split(self, desired_num_offsets_per_split, min_num_offsets_per_split=1): yield OffsetRange(current_split_start, current_split_stop) current_split_start = current_split_stop - def split_at(self, split_pos) -> Tuple['OffsetRange', 'OffsetRange']: + def split_at(self, split_pos) -> tuple['OffsetRange', 'OffsetRange']: return OffsetRange(self.start, split_pos), OffsetRange(split_pos, self.stop) def new_tracker(self): diff --git a/sdks/python/apache_beam/io/textio.py b/sdks/python/apache_beam/io/textio.py index ba28fc608a0c..5b2e6fc47360 100644 --- a/sdks/python/apache_beam/io/textio.py +++ b/sdks/python/apache_beam/io/textio.py @@ -24,7 +24,6 @@ from functools import partial from typing import TYPE_CHECKING from typing import Any -from typing import Dict from typing import Optional from typing import Union @@ -1041,7 +1040,7 @@ def ReadFromJson( *, orient: str = 'records', lines: bool = True, - dtype: Union[bool, Dict[str, Any]] = False, + dtype: Union[bool, dict[str, Any]] = False, **kwargs): """A PTransform for reading json values from files into a PCollection. diff --git a/sdks/python/apache_beam/metrics/cells.py b/sdks/python/apache_beam/metrics/cells.py index 0eb0e53e1d84..8300d7f063e9 100644 --- a/sdks/python/apache_beam/metrics/cells.py +++ b/sdks/python/apache_beam/metrics/cells.py @@ -23,6 +23,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import copy import logging import threading diff --git a/sdks/python/apache_beam/metrics/execution.py b/sdks/python/apache_beam/metrics/execution.py index ede0975ddb65..e304658e09ab 100644 --- a/sdks/python/apache_beam/metrics/execution.py +++ b/sdks/python/apache_beam/metrics/execution.py @@ -32,6 +32,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import threading from typing import TYPE_CHECKING from typing import Any diff --git a/sdks/python/apache_beam/metrics/metric.py b/sdks/python/apache_beam/metrics/metric.py index bfe023901bd2..b15237cae020 100644 --- a/sdks/python/apache_beam/metrics/metric.py +++ b/sdks/python/apache_beam/metrics/metric.py @@ -30,14 +30,9 @@ import logging import re from typing import TYPE_CHECKING -from typing import Dict -from typing import FrozenSet from typing import Iterable from typing import Iterator -from typing import List from typing import Optional -from typing import Set -from typing import Type from typing import Union from apache_beam.metrics import cells @@ -66,7 +61,7 @@ class Metrics(object): """Lets users create/access metric objects during pipeline execution.""" @staticmethod - def get_namespace(namespace: Union[Type, str]) -> str: + def get_namespace(namespace: Union[type, str]) -> str: if isinstance(namespace, type): return '{}.{}'.format(namespace.__module__, namespace.__name__) elif isinstance(namespace, str): @@ -76,7 +71,7 @@ def get_namespace(namespace: Union[Type, str]) -> str: @staticmethod def counter( - namespace: Union[Type, str], name: str) -> 'Metrics.DelegatingCounter': + namespace: Union[type, str], name: str) -> 'Metrics.DelegatingCounter': """Obtains or creates a Counter metric. Args: @@ -91,7 +86,7 @@ def counter( @staticmethod def distribution( - namespace: Union[Type, str], + namespace: Union[type, str], name: str, process_wide: bool = False) -> 'Metrics.DelegatingDistribution': """Obtains or creates a Distribution metric. @@ -111,7 +106,7 @@ def distribution( @staticmethod def gauge( - namespace: Union[Type, str], + namespace: Union[type, str], name: str, process_wide: bool = False) -> 'Metrics.DelegatingGauge': """Obtains or creates a Gauge metric. @@ -133,7 +128,7 @@ def gauge( @staticmethod def string_set( - namespace: Union[Type, str], name: str) -> 'Metrics.DelegatingStringSet': + namespace: Union[type, str], name: str) -> 'Metrics.DelegatingStringSet': """Obtains or creates a String set metric. String set metrics are restricted to string values. @@ -150,7 +145,7 @@ def string_set( @staticmethod def bounded_trie( - namespace: Union[Type, str], + namespace: Union[type, str], name: str) -> 'Metrics.DelegatingBoundedTrie': """Obtains or creates a Bounded Trie metric. @@ -166,7 +161,7 @@ def bounded_trie( @staticmethod def histogram( - namespace: Union[Type, str], + namespace: Union[type, str], name: str, bucket_type: 'BucketType', logger: Optional['MetricLogger'] = None) -> 'Metrics.DelegatingHistogram': @@ -263,7 +258,7 @@ def _matches_name(filter: 'MetricsFilter', metric_key: 'MetricKey') -> bool: return True @staticmethod - def _is_sub_list(needle: List[str], haystack: List[str]) -> bool: + def _is_sub_list(needle: list[str], haystack: list[str]) -> bool: """True iff `needle` is a sub-list of `haystack` (i.e. a contiguous slice of `haystack` exactly matches `needle`""" needle_len = len(needle) @@ -307,7 +302,7 @@ def matches( def query( self, filter: Optional['MetricsFilter'] = None - ) -> Dict[str, List['MetricResult']]: + ) -> dict[str, list['MetricResult']]: """Queries the runner for existing user metrics that match the filter. It should return a dictionary, with lists of each kind of metric, and @@ -338,20 +333,20 @@ class MetricsFilter(object): Note: This class only supports user defined metrics. """ def __init__(self) -> None: - self._names: Set[str] = set() - self._namespaces: Set[str] = set() - self._steps: Set[str] = set() + self._names: set[str] = set() + self._namespaces: set[str] = set() + self._steps: set[str] = set() @property - def steps(self) -> FrozenSet[str]: + def steps(self) -> frozenset[str]: return frozenset(self._steps) @property - def names(self) -> FrozenSet[str]: + def names(self) -> frozenset[str]: return frozenset(self._names) @property - def namespaces(self) -> FrozenSet[str]: + def namespaces(self) -> frozenset[str]: return frozenset(self._namespaces) def with_metric(self, metric: 'Metric') -> 'MetricsFilter': @@ -369,11 +364,11 @@ def with_names(self, names: Iterable[str]) -> 'MetricsFilter': self._names.update(names) return self - def with_namespace(self, namespace: Union[Type, str]) -> 'MetricsFilter': + def with_namespace(self, namespace: Union[type, str]) -> 'MetricsFilter': return self.with_namespaces([namespace]) def with_namespaces( - self, namespaces: Iterable[Union[Type, str]]) -> 'MetricsFilter': + self, namespaces: Iterable[Union[type, str]]) -> 'MetricsFilter': if isinstance(namespaces, str): raise ValueError('Namespaces must be an iterable, not a string') @@ -515,7 +510,7 @@ def add_raw(self, *rollup_segments: str) -> None: @staticmethod def query(results: MetricResults, label: str, - truncated_marker: str = '*') -> Set[str]: + truncated_marker: str = '*') -> set[str]: if not label in Lineage._METRICS: raise ValueError("Label {} does not exist for Lineage", label) response = results.query( diff --git a/sdks/python/apache_beam/metrics/metricbase.py b/sdks/python/apache_beam/metrics/metricbase.py index 9b35bb24f895..addbf14153cf 100644 --- a/sdks/python/apache_beam/metrics/metricbase.py +++ b/sdks/python/apache_beam/metrics/metricbase.py @@ -34,7 +34,6 @@ # pytype: skip-file -from typing import Dict from typing import Optional __all__ = [ @@ -61,7 +60,7 @@ def __init__( namespace: Optional[str], name: Optional[str], urn: Optional[str] = None, - labels: Optional[Dict[str, str]] = None) -> None: + labels: Optional[dict[str, str]] = None) -> None: """Initializes ``MetricName``. Note: namespace and name should be set for user metrics, diff --git a/sdks/python/apache_beam/metrics/monitoring_infos.py b/sdks/python/apache_beam/metrics/monitoring_infos.py index 294bcef039a8..ad24d244993c 100644 --- a/sdks/python/apache_beam/metrics/monitoring_infos.py +++ b/sdks/python/apache_beam/metrics/monitoring_infos.py @@ -20,9 +20,7 @@ import collections import time from functools import reduce -from typing import FrozenSet from typing import Hashable -from typing import List from typing import Union from apache_beam.coders import coder_impl @@ -483,12 +481,12 @@ def get_step_name(monitoring_info_proto): def to_key( - monitoring_info_proto: metrics_pb2.MonitoringInfo) -> FrozenSet[Hashable]: + monitoring_info_proto: metrics_pb2.MonitoringInfo) -> frozenset[Hashable]: """Returns a key based on the URN and labels. This is useful in maps to prevent reporting the same MonitoringInfo twice. """ - key_items: List[Hashable] = list(monitoring_info_proto.labels.items()) + key_items: list[Hashable] = list(monitoring_info_proto.labels.items()) key_items.append(monitoring_info_proto.urn) return frozenset(key_items) diff --git a/sdks/python/apache_beam/ml/gcp/cloud_dlp.py b/sdks/python/apache_beam/ml/gcp/cloud_dlp.py index cb33ef60ef2c..baad9330ee06 100644 --- a/sdks/python/apache_beam/ml/gcp/cloud_dlp.py +++ b/sdks/python/apache_beam/ml/gcp/cloud_dlp.py @@ -20,7 +20,6 @@ """ import logging -from typing import List from google.cloud import dlp_v2 @@ -128,7 +127,7 @@ def expand(self, pcoll): @typehints.with_input_types(str) -@typehints.with_output_types(List[dlp_v2.types.dlp.Finding]) +@typehints.with_output_types(list[dlp_v2.types.dlp.Finding]) class InspectForDetails(PTransform): """Inspects input text for sensitive information. the ``PTransform`` returns a ``PCollection`` of diff --git a/sdks/python/apache_beam/ml/gcp/naturallanguageml.py b/sdks/python/apache_beam/ml/gcp/naturallanguageml.py index f46b8d61639b..ad3173b9974b 100644 --- a/sdks/python/apache_beam/ml/gcp/naturallanguageml.py +++ b/sdks/python/apache_beam/ml/gcp/naturallanguageml.py @@ -18,7 +18,6 @@ from typing import Mapping from typing import Optional from typing import Sequence -from typing import Tuple from typing import Union import apache_beam as beam @@ -83,7 +82,7 @@ def AnnotateText( features: Union[Mapping[str, bool], language_v1.AnnotateTextRequest.Features], timeout: Optional[float] = None, - metadata: Optional[Sequence[Tuple[str, str]]] = None): + metadata: Optional[Sequence[tuple[str, str]]] = None): """A :class:`~apache_beam.transforms.ptransform.PTransform` for annotating text using the Google Cloud Natural Language API: https://cloud.google.com/natural-language/docs. @@ -113,7 +112,7 @@ def __init__( features: Union[Mapping[str, bool], language_v1.AnnotateTextRequest.Features], timeout: Optional[float], - metadata: Optional[Sequence[Tuple[str, str]]] = None): + metadata: Optional[Sequence[tuple[str, str]]] = None): self.features = features self.timeout = timeout self.metadata = metadata diff --git a/sdks/python/apache_beam/ml/gcp/recommendations_ai.py b/sdks/python/apache_beam/ml/gcp/recommendations_ai.py index 9730a6b2b1d9..d3f89e5adfbc 100644 --- a/sdks/python/apache_beam/ml/gcp/recommendations_ai.py +++ b/sdks/python/apache_beam/ml/gcp/recommendations_ai.py @@ -22,7 +22,6 @@ from __future__ import absolute_import from typing import Sequence -from typing import Tuple from cachetools.func import ttl_cache from google.api_core.retry import Retry @@ -98,7 +97,7 @@ def __init__( project: str = None, retry: Retry = None, timeout: float = 120, - metadata: Sequence[Tuple[str, str]] = (), + metadata: Sequence[tuple[str, str]] = (), catalog_name: str = "default_catalog"): """Initializes a :class:`CreateCatalogItem` transform. @@ -144,7 +143,7 @@ def __init__( project: str = None, retry: Retry = None, timeout: float = 120, - metadata: Sequence[Tuple[str, str]] = (), + metadata: Sequence[tuple[str, str]] = (), catalog_name: str = None): self._client = None self.retry = retry @@ -201,7 +200,7 @@ def __init__( project: str = None, retry: Retry = None, timeout: float = 120, - metadata: Sequence[Tuple[str, str]] = (), + metadata: Sequence[tuple[str, str]] = (), catalog_name: str = "default_catalog"): """Initializes a :class:`ImportCatalogItems` transform @@ -301,7 +300,7 @@ def __init__( project: str = None, retry: Retry = None, timeout: float = 120, - metadata: Sequence[Tuple[str, str]] = (), + metadata: Sequence[tuple[str, str]] = (), catalog_name: str = "default_catalog", event_store: str = "default_event_store"): """Initializes a :class:`WriteUserEvent` transform. @@ -400,7 +399,7 @@ def __init__( project: str = None, retry: Retry = None, timeout: float = 120, - metadata: Sequence[Tuple[str, str]] = (), + metadata: Sequence[tuple[str, str]] = (), catalog_name: str = "default_catalog", event_store: str = "default_event_store"): """Initializes a :class:`WriteUserEvent` transform. @@ -506,7 +505,7 @@ def __init__( project: str = None, retry: Retry = None, timeout: float = 120, - metadata: Sequence[Tuple[str, str]] = (), + metadata: Sequence[tuple[str, str]] = (), catalog_name: str = "default_catalog", event_store: str = "default_event_store", placement_id: str = None): diff --git a/sdks/python/apache_beam/ml/gcp/videointelligenceml.py b/sdks/python/apache_beam/ml/gcp/videointelligenceml.py index 25fc258b35a1..2e328a326e59 100644 --- a/sdks/python/apache_beam/ml/gcp/videointelligenceml.py +++ b/sdks/python/apache_beam/ml/gcp/videointelligenceml.py @@ -18,7 +18,6 @@ """A connector for sending API requests to the GCP Video Intelligence API.""" from typing import Optional -from typing import Tuple from typing import Union from cachetools.func import ttl_cache @@ -203,7 +202,7 @@ def expand(self, pvalue): @typehints.with_input_types( - Tuple[Union[str, bytes], Optional[videointelligence.VideoContext]]) + tuple[Union[str, bytes], Optional[videointelligence.VideoContext]]) class _VideoAnnotateFnWithContext(_VideoAnnotateFn): """A DoFn that unpacks each input tuple to element, video_context variables and sends these to the GCP Video Intelligence API service and outputs diff --git a/sdks/python/apache_beam/ml/gcp/visionml.py b/sdks/python/apache_beam/ml/gcp/visionml.py index c4ef30710d58..e8b55304ce77 100644 --- a/sdks/python/apache_beam/ml/gcp/visionml.py +++ b/sdks/python/apache_beam/ml/gcp/visionml.py @@ -20,9 +20,7 @@ A connector for sending API requests to the GCP Vision API. """ -from typing import List from typing import Optional -from typing import Tuple from typing import Union from cachetools.func import ttl_cache @@ -154,7 +152,7 @@ def expand(self, pvalue): metadata=self.metadata))) @typehints.with_input_types(Union[str, bytes], Optional[vision.ImageContext]) - @typehints.with_output_types(List[vision.AnnotateImageRequest]) + @typehints.with_output_types(list[vision.AnnotateImageRequest]) def _create_image_annotation_pairs(self, element, context_side_input): if context_side_input: # If we have a side input image context, use that image_context = context_side_input.get(element) @@ -249,8 +247,8 @@ def expand(self, pvalue): metadata=self.metadata))) @typehints.with_input_types( - Tuple[Union[str, bytes], Optional[vision.ImageContext]]) - @typehints.with_output_types(List[vision.AnnotateImageRequest]) + tuple[Union[str, bytes], Optional[vision.ImageContext]]) + @typehints.with_output_types(list[vision.AnnotateImageRequest]) def _create_image_annotation_pairs(self, element, **kwargs): element, image_context = element # Unpack (image, image_context) tuple if isinstance(element, str): @@ -267,7 +265,7 @@ def _create_image_annotation_pairs(self, element, **kwargs): yield request -@typehints.with_input_types(List[vision.AnnotateImageRequest]) +@typehints.with_input_types(list[vision.AnnotateImageRequest]) class _ImageAnnotateFn(DoFn): """A DoFn that sends each input element to the GCP Vision API. Returns ``google.cloud.vision.BatchAnnotateImagesResponse``. diff --git a/sdks/python/apache_beam/ml/inference/model_manager.py b/sdks/python/apache_beam/ml/inference/model_manager.py index bae18f492351..9c0d90d7a02f 100644 --- a/sdks/python/apache_beam/ml/inference/model_manager.py +++ b/sdks/python/apache_beam/ml/inference/model_manager.py @@ -37,9 +37,7 @@ from collections import deque from typing import Any from typing import Callable -from typing import Dict from typing import Optional -from typing import Tuple import numpy as np import torch @@ -122,7 +120,7 @@ def reset_peak(self): self._memory_history.append((now, self._current_usage)) self._peak_usage = self._current_usage - def get_stats(self) -> Tuple[float, float, float]: + def get_stats(self) -> tuple[float, float, float]: with self._lock: return self._current_usage, self._peak_usage, self._total_memory @@ -186,7 +184,7 @@ def __init__( self.smoothing_factor = smoothing_factor self.min_data_points = min_data_points self.verbose_logging = verbose_logging - self.estimates: Dict[str, float] = {} + self.estimates: dict[str, float] = {} self.history = defaultdict(lambda: deque(maxlen=20)) self.known_models = set() self._lock = threading.Lock() @@ -214,7 +212,7 @@ def set_initial_estimate(self, model_tag: str, cost: float): self.logging_info("Initial Profile for %s: %s MB", model_tag, cost) def add_observation( - self, active_snapshot: Dict[str, int], peak_memory: float): + self, active_snapshot: dict[str, int], peak_memory: float): if active_snapshot: model_list = "\n".join( f"\t- {model}: {count}" diff --git a/sdks/python/apache_beam/ml/rag/chunking/base.py b/sdks/python/apache_beam/ml/rag/chunking/base.py index 626a6ea8abbe..0286e02a81f6 100644 --- a/sdks/python/apache_beam/ml/rag/chunking/base.py +++ b/sdks/python/apache_beam/ml/rag/chunking/base.py @@ -19,7 +19,6 @@ import functools from collections.abc import Callable from typing import Any -from typing import Dict from typing import Optional import apache_beam as beam @@ -71,7 +70,7 @@ def __init__(self, chunk_id_fn: Optional[ChunkIdFn] = None): @abc.abstractmethod def get_splitter_transform( self - ) -> beam.PTransform[beam.PCollection[Dict[str, Any]], + ) -> beam.PTransform[beam.PCollection[dict[str, Any]], beam.PCollection[Chunk]]: """Creates transforms that emits splits for given content.""" raise NotImplementedError( @@ -79,7 +78,7 @@ def get_splitter_transform( def get_ptransform_for_processing( self, **kwargs - ) -> beam.PTransform[beam.PCollection[Dict[str, Any]], + ) -> beam.PTransform[beam.PCollection[dict[str, Any]], beam.PCollection[Chunk]]: """Creates transform for processing documents into chunks.""" ptransform = ( diff --git a/sdks/python/apache_beam/ml/rag/chunking/base_test.py b/sdks/python/apache_beam/ml/rag/chunking/base_test.py index 54e25591c348..36d6428695a4 100644 --- a/sdks/python/apache_beam/ml/rag/chunking/base_test.py +++ b/sdks/python/apache_beam/ml/rag/chunking/base_test.py @@ -18,7 +18,6 @@ import unittest from typing import Any -from typing import Dict from typing import Optional import pytest @@ -54,7 +53,7 @@ def __init__(self, chunk_id_fn: Optional[ChunkIdFn] = None): def get_splitter_transform( self - ) -> beam.PTransform[beam.PCollection[Dict[str, Any]], + ) -> beam.PTransform[beam.PCollection[dict[str, Any]], beam.PCollection[Chunk]]: return beam.ParDo(WordSplitter()) diff --git a/sdks/python/apache_beam/ml/rag/chunking/langchain.py b/sdks/python/apache_beam/ml/rag/chunking/langchain.py index 9e3b6b0c8ef9..c89bc15d2da1 100644 --- a/sdks/python/apache_beam/ml/rag/chunking/langchain.py +++ b/sdks/python/apache_beam/ml/rag/chunking/langchain.py @@ -16,8 +16,6 @@ # from typing import Any -from typing import Dict -from typing import List from typing import Optional import apache_beam as beam @@ -37,7 +35,7 @@ def __init__( self, text_splitter: TextSplitter, document_field: str, - metadata_fields: List[str], + metadata_fields: list[str], chunk_id_fn: Optional[ChunkIdFn] = None): """A ChunkingTransformProvider that uses LangChain text splitters. @@ -94,7 +92,7 @@ def __init__( def get_splitter_transform( self - ) -> beam.PTransform[beam.PCollection[Dict[str, Any]], + ) -> beam.PTransform[beam.PCollection[dict[str, Any]], beam.PCollection[Chunk]]: return "Langchain text split" >> beam.ParDo( _LangChainTextSplitter( @@ -108,7 +106,7 @@ def __init__( self, text_splitter: TextSplitter, document_field: str, - metadata_fields: List[str]): + metadata_fields: list[str]): self.text_splitter = text_splitter self.document_field = document_field self.metadata_fields = metadata_fields diff --git a/sdks/python/apache_beam/ml/rag/embeddings/base.py b/sdks/python/apache_beam/ml/rag/embeddings/base.py index 0128d6a6d6fc..1f85f1d89daf 100644 --- a/sdks/python/apache_beam/ml/rag/embeddings/base.py +++ b/sdks/python/apache_beam/ml/rag/embeddings/base.py @@ -22,7 +22,6 @@ """ from collections.abc import Sequence -from typing import List from apache_beam.ml.rag.types import EmbeddableItem from apache_beam.ml.rag.types import Embedding @@ -48,7 +47,7 @@ def create_text_adapter( create_rag_adapter = create_text_adapter -def _extract_text(items: Sequence[EmbeddableItem]) -> List[str]: +def _extract_text(items: Sequence[EmbeddableItem]) -> list[str]: """Extract text from items for embedding.""" texts = [] for item in items: @@ -62,7 +61,7 @@ def _extract_text(items: Sequence[EmbeddableItem]) -> List[str]: def _add_embedding_fn( items: Sequence[EmbeddableItem], - embeddings: Sequence[List[float]]) -> List[EmbeddableItem]: + embeddings: Sequence[list[float]]) -> list[EmbeddableItem]: """Create Embeddings from items and embedding vectors.""" for item, embedding in zip(items, embeddings): item.embedding = Embedding(dense_embedding=embedding) diff --git a/sdks/python/apache_beam/ml/rag/enrichment/bigquery_vector_search.py b/sdks/python/apache_beam/ml/rag/enrichment/bigquery_vector_search.py index e9269af27bd4..220b41598b07 100644 --- a/sdks/python/apache_beam/ml/rag/enrichment/bigquery_vector_search.py +++ b/sdks/python/apache_beam/ml/rag/enrichment/bigquery_vector_search.py @@ -20,10 +20,7 @@ from dataclasses import dataclass from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Union from google.cloud import bigquery @@ -166,13 +163,13 @@ class BigQueryVectorSearchParameters: project: str table_name: str embedding_column: str - columns: List[str] + columns: list[str] neighbor_count: int metadata_restriction_template: Optional[Union[str, Callable[[EmbeddableItem], str]]] = None distance_type: Optional[str] = None - options: Optional[Dict[str, Any]] = None + options: Optional[dict[str, Any]] = None include_distance: bool = False def _format_restrict(self, item: EmbeddableItem) -> str: @@ -185,7 +182,7 @@ def _format_restrict(self, item: EmbeddableItem) -> str: return self.metadata_restriction_template(item) return self.metadata_restriction_template.format(**item.metadata) - def format_query(self, items: List[EmbeddableItem]) -> str: + def format_query(self, items: list[EmbeddableItem]) -> str: """Format the vector search query template.""" base_columns_str = ", ".join(f"base.{col}" for col in self.columns) columns_str = ", ".join(self.columns) @@ -267,8 +264,8 @@ def format_query(self, items: List[EmbeddableItem]) -> str: class BigQueryVectorSearchEnrichmentHandler( - EnrichmentSourceHandler[Union[EmbeddableItem, List[EmbeddableItem]], - List[Tuple[EmbeddableItem, Dict[str, Any]]]]): + EnrichmentSourceHandler[Union[EmbeddableItem, list[EmbeddableItem]], + list[tuple[EmbeddableItem, dict[str, Any]]]]): """Enrichment handler that performs vector similarity search using BigQuery. This handler enriches EmbeddableItems by finding similar vectors in a @@ -348,9 +345,9 @@ def __enter__(self): def __call__( self, - request: Union[EmbeddableItem, List[EmbeddableItem]], + request: Union[EmbeddableItem, list[EmbeddableItem]], *args, - **kwargs) -> List[Tuple[EmbeddableItem, Dict[str, Any]]]: + **kwargs) -> list[tuple[EmbeddableItem, dict[str, Any]]]: """Process request(s) using BigQuery vector search. Args: @@ -389,11 +386,11 @@ def __call__( def __exit__(self, exc_type, exc_val, exc_tb): self.client.close() - def batch_elements_kwargs(self) -> Dict[str, int]: + def batch_elements_kwargs(self) -> dict[str, int]: """Returns kwargs for beam.BatchElements.""" return self._batching_kwargs -def join_fn(left: Embedding, right: Dict[str, Any]) -> Embedding: +def join_fn(left: Embedding, right: dict[str, Any]) -> Embedding: left.metadata['enrichment_data'] = right return left diff --git a/sdks/python/apache_beam/ml/rag/enrichment/milvus_search.py b/sdks/python/apache_beam/ml/rag/enrichment/milvus_search.py index 85a63cfba21e..cd310b784703 100644 --- a/sdks/python/apache_beam/ml/rag/enrichment/milvus_search.py +++ b/sdks/python/apache_beam/ml/rag/enrichment/milvus_search.py @@ -20,10 +20,7 @@ from dataclasses import field from enum import Enum from typing import Any -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Union from google.protobuf.json_format import MessageToDict @@ -130,7 +127,7 @@ class BaseSearchParameters: anns_field: str limit: int = 3 filter: str = field(default_factory=str) - search_params: Dict[str, Any] = field(default_factory=dict) + search_params: dict[str, Any] = field(default_factory=dict) consistency_level: Optional[str] = None def __post_init__(self): @@ -156,7 +153,7 @@ class VectorSearchParameters(BaseSearchParameters): Note: For inherited parameters documentation, see BaseSearchParameters. """ - kwargs: Dict[str, Any] = field(default_factory=dict) + kwargs: dict[str, Any] = field(default_factory=dict) @dataclass @@ -174,7 +171,7 @@ class KeywordSearchParameters(BaseSearchParameters): Note: For inherited parameters documentation, see BaseSearchParameters. """ - kwargs: Dict[str, Any] = field(default_factory=dict) + kwargs: dict[str, Any] = field(default_factory=dict) @dataclass @@ -195,7 +192,7 @@ class HybridSearchParameters: keyword: KeywordSearchParameters ranker: MilvusBaseRanker limit: int = 3 - kwargs: Dict[str, Any] = field(default_factory=dict) + kwargs: dict[str, Any] = field(default_factory=dict) def __post_init__(self): if not self.vector or not self.keyword: @@ -236,8 +233,8 @@ class MilvusSearchParameters: """ collection_name: str search_strategy: SearchStrategyType - partition_names: List[str] = field(default_factory=list) - output_fields: List[str] = field(default_factory=list) + partition_names: list[str] = field(default_factory=list) + output_fields: list[str] = field(default_factory=list) timeout: Optional[float] = None round_decimal: int = -1 @@ -271,10 +268,10 @@ class MilvusCollectionLoadParameters: parameters. Enables forward compatibility. """ refresh: bool = field(default_factory=bool) - resource_groups: List[str] = field(default_factory=list) - load_fields: List[str] = field(default_factory=list) + resource_groups: list[str] = field(default_factory=list) + load_fields: list[str] = field(default_factory=list) skip_load_dynamic_field: bool = field(default_factory=bool) - kwargs: Dict[str, Any] = field(default_factory=dict) + kwargs: dict[str, Any] = field(default_factory=dict) @dataclass @@ -288,13 +285,13 @@ class MilvusSearchResult: fields: List of dictionaries containing additional field values for each entity. Each dictionary corresponds to one returned entity. """ - id: List[Union[str, int]] = field(default_factory=list) - distance: List[float] = field(default_factory=list) - fields: List[Dict[str, Any]] = field(default_factory=list) + id: list[Union[str, int]] = field(default_factory=list) + distance: list[float] = field(default_factory=list) + fields: list[dict[str, Any]] = field(default_factory=list) -InputT, OutputT = (Union[EmbeddableItem, List[EmbeddableItem]], - List[Tuple[EmbeddableItem, Dict[str, Any]]]) +InputT, OutputT = (Union[EmbeddableItem, list[EmbeddableItem]], + list[tuple[EmbeddableItem, dict[str, Any]]]) class MilvusSearchEnrichmentHandler(EnrichmentSourceHandler[InputT, OutputT]): @@ -415,9 +412,9 @@ def connect_and_load(): def __call__( self, - request: Union[EmbeddableItem, List[EmbeddableItem]], + request: Union[EmbeddableItem, list[EmbeddableItem]], *args, - **kwargs) -> List[Tuple[EmbeddableItem, Dict[str, Any]]]: + **kwargs) -> list[tuple[EmbeddableItem, dict[str, Any]]]: reqs = request if isinstance(request, list) else [request] # Early return for empty requests to avoid unnecessary connection attempts if not reqs: @@ -425,7 +422,7 @@ def __call__( search_result = self._search_documents(reqs) return self._get_call_response(reqs, search_result) - def _search_documents(self, embeddable_items: List[EmbeddableItem]): + def _search_documents(self, embeddable_items: list[EmbeddableItem]): if isinstance(self.search_strategy, HybridSearchParameters): data = self._get_hybrid_search_data(embeddable_items) return self._client.hybrid_search( @@ -464,7 +461,7 @@ def _search_documents(self, embeddable_items: List[EmbeddableItem]): raise ValueError( f"Not supported search strategy yet: {self.search_strategy}") - def _get_hybrid_search_data(self, embeddable_items: List[EmbeddableItem]): + def _get_hybrid_search_data(self, embeddable_items: list[EmbeddableItem]): vector_search_data = list( map(self._get_vector_search_data, embeddable_items)) keyword_search_data = list( @@ -507,7 +504,7 @@ def _get_keyword_search_data(self, embeddable_item: EmbeddableItem): def _get_call_response( self, - embeddable_items: List[EmbeddableItem], + embeddable_items: list[EmbeddableItem], search_result: SearchResult[Hits]): response = [] for i in range(len(embeddable_items)): @@ -523,7 +520,7 @@ def _get_call_response( response.append((embeddable_item, result.__dict__)) return response - def _normalize_milvus_fields(self, fields: Dict[str, Any]): + def _normalize_milvus_fields(self, fields: dict[str, Any]): normalized_fields = {} for field, value in fields.items(): value = self._normalize_milvus_value(value) @@ -543,7 +540,7 @@ def _normalize_milvus_value(self, value: Any): return value def convert_sparse_embedding_to_milvus_format( - self, sparse_vector: Tuple[List[int], List[float]]) -> Dict[int, float]: + self, sparse_vector: tuple[list[int], list[float]]) -> dict[int, float]: if not sparse_vector: return None # Converts sparse embedding from (indices, values) tuple format to @@ -586,11 +583,11 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._client.close() self._client = None - def batch_elements_kwargs(self) -> Dict[str, int]: + def batch_elements_kwargs(self) -> dict[str, int]: """Returns kwargs for beam.BatchElements.""" return self._batching_kwargs -def join_fn(left: Embedding, right: Dict[str, Any]) -> Embedding: +def join_fn(left: Embedding, right: dict[str, Any]) -> Embedding: left.metadata['enrichment_data'] = right return left diff --git a/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py b/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py index 307563868fe8..f37fc4931487 100644 --- a/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py +++ b/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py @@ -19,7 +19,6 @@ import unittest from dataclasses import dataclass from dataclasses import field -from typing import Dict import pytest @@ -129,7 +128,7 @@ class MilvusITDataConstruct: tags: list[str] dense_embedding: list[float] sparse_embedding: dict - vocabulary: Dict[str, int] = field(default_factory=dict) + vocabulary: dict[str, int] = field(default_factory=dict) def __getitem__(self, key): return getattr(self, key) diff --git a/sdks/python/apache_beam/ml/rag/ingestion/alloydb.py b/sdks/python/apache_beam/ml/rag/ingestion/alloydb.py index 333c259f9b86..39e501af7810 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/alloydb.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/alloydb.py @@ -16,8 +16,6 @@ from dataclasses import dataclass from typing import Any -from typing import Dict -from typing import List from typing import Optional from apache_beam.ml.rag.ingestion.jdbc_common import ConnectionConfig @@ -64,11 +62,11 @@ class AlloyDBLanguageConnectorConfig: ip_type: str = "PRIVATE" enable_iam_auth: bool = False target_principal: Optional[str] = None - delegates: Optional[List[str]] = None + delegates: Optional[list[str]] = None admin_service_endpoint: Optional[str] = None quota_project: Optional[str] = None - connection_properties: Optional[Dict[str, str]] = None - additional_properties: Optional[Dict[str, Any]] = None + connection_properties: Optional[dict[str, str]] = None + additional_properties: Optional[dict[str, Any]] = None def to_jdbc_url(self) -> str: """Convert options to a properly formatted JDBC URL. @@ -115,7 +113,7 @@ def to_connection_config(self): connection_properties=self.connection_properties, additional_jdbc_args=self.additional_jdbc_args()) - def additional_jdbc_args(self) -> Dict[str, List[Any]]: + def additional_jdbc_args(self) -> dict[str, list[Any]]: return { 'classpath': [ "org.postgresql:postgresql:42.2.16", @@ -132,7 +130,7 @@ def __init__( *, # pylint: disable=dangerous-default-value write_config: WriteConfig = WriteConfig(), - column_specs: List[ColumnSpec] = ColumnSpecsBuilder.with_defaults().build( + column_specs: list[ColumnSpec] = ColumnSpecsBuilder.with_defaults().build( ), conflict_resolution: Optional[ConflictResolution] = ConflictResolution( on_conflict_fields=[], action='IGNORE')): diff --git a/sdks/python/apache_beam/ml/rag/ingestion/alloydb_it_test.py b/sdks/python/apache_beam/ml/rag/ingestion/alloydb_it_test.py index ce98de19a1de..35f58d0b071a 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/alloydb_it_test.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/alloydb_it_test.py @@ -21,7 +21,6 @@ import secrets import time import unittest -from typing import List from typing import NamedTuple import psycopg2 @@ -52,7 +51,7 @@ 'CustomSpecsRow', [ ('custom_id', str), # For id_spec test - ('embedding_vec', List[float]), # For embedding_spec test + ('embedding_vec', list[float]), # For embedding_spec test ('content_col', str), # For content_spec test ('metadata', str) ]) @@ -65,7 +64,7 @@ ('source', str), # For metadata_spec and composite key ('timestamp', str), # For metadata_spec and composite key ('content', str), - ('embedding', List[float]), + ('embedding', list[float]), ('metadata', str) ]) registry.register_coder(MetadataConflictRow, RowCoder) diff --git a/sdks/python/apache_beam/ml/rag/ingestion/bigquery.py b/sdks/python/apache_beam/ml/rag/ingestion/bigquery.py index 2a7111c0d35f..20f7febe78f1 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/bigquery.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/bigquery.py @@ -17,7 +17,6 @@ import warnings from collections.abc import Callable from typing import Any -from typing import Dict from typing import Optional import apache_beam as beam @@ -27,7 +26,7 @@ from apache_beam.ml.rag.types import EmbeddableItem from apache_beam.typehints.row_type import RowTypeConstraint -EmbeddableToDictFn = Callable[[EmbeddableItem], Dict[str, any]] +EmbeddableToDictFn = Callable[[EmbeddableItem], dict[str, any]] # Backward compatibility alias. ChunkToDictFn = EmbeddableToDictFn @@ -35,7 +34,7 @@ class SchemaConfig: def __init__( self, - schema: Dict, + schema: dict, embeddable_to_dict_fn: Optional[EmbeddableToDictFn] = None, **kwargs): """Configuration for custom BigQuery schema and row conversion. @@ -83,7 +82,7 @@ def __init__( class BigQueryVectorWriterConfig(VectorDatabaseWriteConfig): def __init__( self, - write_config: Dict[str, Any], + write_config: dict[str, Any], *, # Force keyword arguments schema_config: Optional[SchemaConfig] = None): """Configuration for writing vectors to BigQuery using managed transforms. diff --git a/sdks/python/apache_beam/ml/rag/ingestion/cloudsql.py b/sdks/python/apache_beam/ml/rag/ingestion/cloudsql.py index 4cd6474ba348..4307ac2d94e4 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/cloudsql.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/cloudsql.py @@ -17,8 +17,6 @@ from dataclasses import asdict from dataclasses import dataclass from typing import Any -from typing import Dict -from typing import List from typing import Optional from apache_beam.ml.rag.ingestion import mysql @@ -61,15 +59,15 @@ class LanguageConnectorConfig: password: str database_name: str instance_name: str - ip_types: Optional[List[str]] = None + ip_types: Optional[list[str]] = None enable_iam_auth: bool = False target_principal: Optional[str] = None - delegates: Optional[List[str]] = None + delegates: Optional[list[str]] = None quota_project: Optional[str] = None - connection_properties: Optional[Dict[str, str]] = None - additional_properties: Optional[Dict[str, Any]] = None + connection_properties: Optional[dict[str, str]] = None + additional_properties: Optional[dict[str, Any]] = None - def _base_jdbc_properties(self) -> Dict[str, Any]: + def _base_jdbc_properties(self) -> dict[str, Any]: properties = {"cloudSqlInstance": self.instance_name} if self.ip_types: @@ -109,7 +107,7 @@ def to_connection_config(self): connection_properties=self.connection_properties, additional_jdbc_args=self.additional_jdbc_args()) - def additional_jdbc_args(self) -> Dict[str, List[Any]]: + def additional_jdbc_args(self) -> dict[str, list[Any]]: return {} @@ -125,7 +123,7 @@ def to_jdbc_url(self) -> str: socketFactory="com.google.cloud.sql.postgres.SocketFactory", database_type="postgresql") - def additional_jdbc_args(self) -> Dict[str, List[Any]]: + def additional_jdbc_args(self) -> dict[str, list[Any]]: return { 'classpath': [ "org.postgresql:postgresql:42.2.16", @@ -146,7 +144,7 @@ def __init__( *, # pylint: disable=dangerous-default-value write_config: WriteConfig = WriteConfig(), - column_specs: List[postgres_common.ColumnSpec] = postgres_common. + column_specs: list[postgres_common.ColumnSpec] = postgres_common. ColumnSpecsBuilder.with_defaults().build(), conflict_resolution: Optional[ postgres_common.ConflictResolution] = postgres_common. @@ -229,7 +227,7 @@ def to_jdbc_url(self) -> str: socketFactory="com.google.cloud.sql.mysql.SocketFactory", database_type="mysql") - def additional_jdbc_args(self) -> Dict[str, List[Any]]: + def additional_jdbc_args(self) -> dict[str, list[Any]]: return { 'classpath': [ "mysql:mysql-connector-java:8.0.22", @@ -250,7 +248,7 @@ def __init__( *, write_config: WriteConfig = WriteConfig(), # pylint: disable=dangerous-default-value - column_specs: List[mysql_common.ColumnSpec] = mysql_common. + column_specs: list[mysql_common.ColumnSpec] = mysql_common. ColumnSpecsBuilder.with_defaults().build(), conflict_resolution: Optional[mysql_common.ConflictResolution] = None): self.connector_config = _MySQLConnectorConfig.from_base_config( diff --git a/sdks/python/apache_beam/ml/rag/ingestion/cloudsql_it_test.py b/sdks/python/apache_beam/ml/rag/ingestion/cloudsql_it_test.py index 7ae49ba51823..1d4b988a5db0 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/cloudsql_it_test.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/cloudsql_it_test.py @@ -23,7 +23,6 @@ import unittest from dataclasses import dataclass from typing import Any -from typing import List from typing import Literal from typing import Optional @@ -318,7 +317,7 @@ def build_jdbc_params(helper: DatabaseTestHelper, table_name: str) -> dict: @staticmethod def verify_standard_operations( - pipeline, jdbc_params: dict, expected_chunks: List[Chunk]): + pipeline, jdbc_params: dict, expected_chunks: list[Chunk]): num_records = len(expected_chunks) sample_size = min(500, num_records // 2) diff --git a/sdks/python/apache_beam/ml/rag/ingestion/jdbc_common.py b/sdks/python/apache_beam/ml/rag/ingestion/jdbc_common.py index 586bb7a4aa65..35fd5b35ef3c 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/jdbc_common.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/jdbc_common.py @@ -17,8 +17,6 @@ from dataclasses import dataclass from dataclasses import field from typing import Any -from typing import Dict -from typing import List from typing import Optional @@ -55,9 +53,9 @@ class ConnectionConfig: jdbc_url: str username: str password: str - connection_properties: Optional[Dict[str, str]] = None - connection_init_sqls: Optional[List[str]] = None - additional_jdbc_args: Dict[str, Any] = field(default_factory=dict) + connection_properties: Optional[dict[str, str]] = None + connection_init_sqls: Optional[list[str]] = None + additional_jdbc_args: dict[str, Any] = field(default_factory=dict) @dataclass diff --git a/sdks/python/apache_beam/ml/rag/ingestion/milvus_search.py b/sdks/python/apache_beam/ml/rag/ingestion/milvus_search.py index 7d7c554cc68e..b7cad3796b13 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/milvus_search.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/milvus_search.py @@ -19,8 +19,6 @@ from dataclasses import field from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional from pymilvus import MilvusClient @@ -65,7 +63,7 @@ class MilvusWriteConfig: partition_name: str = "" timeout: Optional[float] = None write_config: WriteConfig = field(default_factory=WriteConfig) - kwargs: Dict[str, Any] = field(default_factory=dict) + kwargs: dict[str, Any] = field(default_factory=dict) def __post_init__(self): if not self.collection_name: @@ -113,10 +111,10 @@ class MilvusVectorWriterConfig(VectorDatabaseWriteConfig): """ connection_params: MilvusConnectionParameters write_config: MilvusWriteConfig - column_specs: List[ColumnSpec] = field( + column_specs: list[ColumnSpec] = field( default_factory=lambda: MilvusVectorWriterConfig.default_column_specs()) - def create_converter(self) -> Callable[[EmbeddableItem], Dict[str, Any]]: + def create_converter(self) -> Callable[[EmbeddableItem], dict[str, Any]]: """Creates a function to convert EmbeddableItem objects to Milvus records. Returns: @@ -124,7 +122,7 @@ def create_converter(self) -> Callable[[EmbeddableItem], Dict[str, Any]]: dictionary representing a Milvus record with fields mapped according to column_specs. """ - def convert(chunk: EmbeddableItem) -> Dict[str, Any]: + def convert(chunk: EmbeddableItem) -> dict[str, Any]: result = {} for col in self.column_specs: result[col.column_name] = col.value_fn(chunk) @@ -143,7 +141,7 @@ def create_write_transform(self) -> beam.PTransform: return _WriteToMilvusVectorDatabase(self) @staticmethod - def default_column_specs() -> List[ColumnSpec]: + def default_column_specs() -> list[ColumnSpec]: """Returns default column specifications for RAG use cases. Creates column mappings for standard RAG fields: id, dense embedding, diff --git a/sdks/python/apache_beam/ml/rag/ingestion/mysql.py b/sdks/python/apache_beam/ml/rag/ingestion/mysql.py index 45f33ea2bad5..75117441ccf6 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/mysql.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/mysql.py @@ -19,7 +19,6 @@ from abc import ABC from abc import abstractmethod from typing import Callable -from typing import List from typing import NamedTuple from typing import Optional @@ -41,23 +40,23 @@ class _ConflictResolutionStrategy(ABC): """Abstract base class for conflict resolution strategies.""" @abstractmethod - def get_conflict_clause(self, all_columns: List[str]) -> str: + def get_conflict_clause(self, all_columns: list[str]) -> str: """Generate the MySQL conflict clause.""" pass class _NoConflictStrategy(_ConflictResolutionStrategy): """Strategy for when no conflict resolution is needed.""" - def get_conflict_clause(self, all_columns: List[str]) -> str: + def get_conflict_clause(self, all_columns: list[str]) -> str: return "" class _UpdateStrategy(_ConflictResolutionStrategy): """Strategy for UPDATE action on conflict.""" - def __init__(self, update_fields: Optional[List[str]] = None): + def __init__(self, update_fields: Optional[list[str]] = None): self.update_fields = update_fields - def get_conflict_clause(self, all_columns: List[str]) -> str: + def get_conflict_clause(self, all_columns: list[str]) -> str: # Use provided fields or default to all columns fields_to_update = self.update_fields or all_columns assert len(fields_to_update) > 0 @@ -71,7 +70,7 @@ class _IgnoreStrategy(_ConflictResolutionStrategy): def __init__(self, primary_key_field: str): self.primary_key_field = primary_key_field - def get_conflict_clause(self, all_columns: List[str]) -> str: + def get_conflict_clause(self, all_columns: list[str]) -> str: return f"ON DUPLICATE KEY UPDATE {self.primary_key_field}"\ f" = {self.primary_key_field}" @@ -94,7 +93,7 @@ def __init__( self, table_name: str, *, - column_specs: List[ColumnSpec], + column_specs: list[ColumnSpec], conflict_resolution: Optional[ConflictResolution] = None): """Builds SQL queries for writing EmbeddableItems with Embeddings to MySQL. """ @@ -150,7 +149,7 @@ def __init__( *, # pylint: disable=dangerous-default-value write_config: WriteConfig = WriteConfig(), - column_specs: List[ColumnSpec] = ColumnSpecsBuilder.with_defaults().build( + column_specs: list[ColumnSpec] = ColumnSpecsBuilder.with_defaults().build( ), conflict_resolution: Optional[ConflictResolution] = None): """Configuration for writing vectors to MySQL using jdbc. diff --git a/sdks/python/apache_beam/ml/rag/ingestion/mysql_common.py b/sdks/python/apache_beam/ml/rag/ingestion/mysql_common.py index 829e95de9f2f..ccda96681347 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/mysql_common.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/mysql_common.py @@ -18,11 +18,8 @@ from dataclasses import dataclass from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Literal from typing import Optional -from typing import Type from apache_beam.ml.rag.types import EmbeddableItem @@ -97,7 +94,7 @@ class ColumnSpec: json: Creates a JSON column specification. """ column_name: str - python_type: Type + python_type: type value_fn: Callable[[EmbeddableItem], Any] placeholder: str = '?' @@ -139,7 +136,7 @@ def json( return cls(column_name, str, value_fn) -def embedding_to_string(embedding: List[float]) -> str: +def embedding_to_string(embedding: list[float]) -> str: """Convert embedding to MySQL vector string format.""" return '[' + ','.join(str(x) for x in embedding) + ']' @@ -147,7 +144,7 @@ def embedding_to_string(embedding: List[float]) -> str: class ColumnSpecsBuilder: """Builder for :class:`.ColumnSpec`'s with chainable methods.""" def __init__(self): - self._specs: List[ColumnSpec] = [] + self._specs: list[ColumnSpec] = [] @staticmethod def with_defaults() -> 'ColumnSpecsBuilder': @@ -159,7 +156,7 @@ def with_defaults() -> 'ColumnSpecsBuilder': def with_id_spec( self, column_name: str = "id", - python_type: Type = str, + python_type: type = str, convert_fn: Optional[Callable[[str], Any]] = None) -> 'ColumnSpecsBuilder': """Add ID :class:`.ColumnSpec` with optional type and conversion. @@ -193,7 +190,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_content_spec( self, column_name: str = "content", - python_type: Type = str, + python_type: type = str, convert_fn: Optional[Callable[[str], Any]] = None) -> 'ColumnSpecsBuilder': """Add content :class:`.ColumnSpec` with optional type and conversion. @@ -227,8 +224,8 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_metadata_spec( self, column_name: str = "metadata", - python_type: Type = str, - convert_fn: Optional[Callable[[Dict[str, Any]], Any]] = None + python_type: type = str, + convert_fn: Optional[Callable[[dict[str, Any]], Any]] = None ) -> 'ColumnSpecsBuilder': """Add metadata :class:`.ColumnSpec` with optional type and conversion. @@ -263,7 +260,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_embedding_spec( self, column_name: str = "embedding", - convert_fn: Callable[[List[float]], Any] = embedding_to_string + convert_fn: Callable[[list[float]], Any] = embedding_to_string ) -> 'ColumnSpecsBuilder': """Add embedding :class:`.ColumnSpec` with optional conversion. @@ -295,7 +292,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def add_metadata_field( self, field: str, - python_type: Type, + python_type: type, column_name: Optional[str] = None, convert_fn: Optional[Callable[[Any], Any]] = None, default: Any = None) -> 'ColumnSpecsBuilder': @@ -380,7 +377,7 @@ def add_custom_column_spec(self, spec: ColumnSpec) -> 'ColumnSpecsBuilder': self._specs.append(spec) return self - def build(self) -> List[ColumnSpec]: + def build(self) -> list[ColumnSpec]: """Build the final list of column specifications.""" return self._specs.copy() @@ -427,7 +424,7 @@ class ConflictResolution: ... ) """ action: Literal["UPDATE", "IGNORE"] = "UPDATE" - update_fields: Optional[List[str]] = None + update_fields: Optional[list[str]] = None primary_key_field: Optional[str] = None def __post_init__(self): diff --git a/sdks/python/apache_beam/ml/rag/ingestion/postgres.py b/sdks/python/apache_beam/ml/rag/ingestion/postgres.py index b01e450e9bec..3d41b0b11b11 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/postgres.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/postgres.py @@ -16,8 +16,6 @@ import logging from typing import Callable -from typing import Dict -from typing import List from typing import NamedTuple from typing import Optional from typing import Union @@ -36,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -MetadataSpec = Union[ColumnSpec, Dict[str, ColumnSpec]] +MetadataSpec = Union[ColumnSpec, dict[str, ColumnSpec]] class _PostgresQueryBuilder: @@ -44,7 +42,7 @@ def __init__( self, table_name: str, *, - column_specs: List[ColumnSpec], + column_specs: list[ColumnSpec], conflict_resolution: Optional[ConflictResolution] = None): """Builds SQL queries for writing EmbeddableItems to Postgres. """ @@ -111,7 +109,7 @@ def __init__( *, # pylint: disable=dangerous-default-value write_config: WriteConfig = WriteConfig(), - column_specs: List[ColumnSpec] = ColumnSpecsBuilder.with_defaults().build( + column_specs: list[ColumnSpec] = ColumnSpecsBuilder.with_defaults().build( ), conflict_resolution: Optional[ConflictResolution] = ConflictResolution( on_conflict_fields=[], action='IGNORE')): diff --git a/sdks/python/apache_beam/ml/rag/ingestion/postgres_common.py b/sdks/python/apache_beam/ml/rag/ingestion/postgres_common.py index 4aa08fc7c494..2456f78eba9b 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/postgres_common.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/postgres_common.py @@ -18,12 +18,8 @@ from dataclasses import dataclass from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Literal from typing import Optional -from typing import Tuple -from typing import Type from typing import Union from apache_beam.ml.rag.types import EmbeddableItem @@ -108,7 +104,7 @@ class ColumnSpec: jsonb: Creates a JSONB column specification with jsonb casting. """ column_name: str - python_type: Type + python_type: type value_fn: Callable[[EmbeddableItem], Any] sql_typecast: Optional[str] = None @@ -158,7 +154,7 @@ def jsonb( class ColumnSpecsBuilder: """Builder for :class:`.ColumnSpec`'s with chainable methods.""" def __init__(self): - self._specs: List[ColumnSpec] = [] + self._specs: list[ColumnSpec] = [] @staticmethod def with_defaults() -> 'ColumnSpecsBuilder': @@ -170,7 +166,7 @@ def with_defaults() -> 'ColumnSpecsBuilder': def with_id_spec( self, column_name: str = "id", - python_type: Type = str, + python_type: type = str, convert_fn: Optional[Callable[[str], Any]] = None, sql_typecast: Optional[str] = None) -> 'ColumnSpecsBuilder': """Add ID :class:`.ColumnSpec` with optional type and conversion. @@ -207,7 +203,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_content_spec( self, column_name: str = "content", - python_type: Type = str, + python_type: type = str, convert_fn: Optional[Callable[[str], Any]] = None, sql_typecast: Optional[str] = None) -> 'ColumnSpecsBuilder': """Add content :class:`.ColumnSpec` with optional type and conversion. @@ -244,8 +240,8 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_metadata_spec( self, column_name: str = "metadata", - python_type: Type = str, - convert_fn: Optional[Callable[[Dict[str, Any]], Any]] = None, + python_type: type = str, + convert_fn: Optional[Callable[[dict[str, Any]], Any]] = None, sql_typecast: Optional[str] = "::jsonb") -> 'ColumnSpecsBuilder': """Add metadata :class:`.ColumnSpec` with optional type and conversion. @@ -284,7 +280,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_embedding_spec( self, column_name: str = "embedding", - convert_fn: Optional[Callable[[List[float]], Any]] = None + convert_fn: Optional[Callable[[list[float]], Any]] = None ) -> 'ColumnSpecsBuilder': """Add embedding :class:`.ColumnSpec` with optional conversion. @@ -318,7 +314,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def with_sparse_embedding_spec( self, column_name: str = "sparse_embedding", - conv_fn: Optional[Callable[[Tuple[List[int], List[float]]], Any]] = None + conv_fn: Optional[Callable[[tuple[list[int], list[float]]], Any]] = None ) -> 'ColumnSpecsBuilder': """Add sparse embedding :class:`.ColumnSpec` with optional conversion. @@ -354,7 +350,7 @@ def value_fn(chunk: EmbeddableItem) -> Any: def add_metadata_field( self, field: str, - python_type: Type, + python_type: type, column_name: Optional[str] = None, convert_fn: Optional[Callable[[Any], Any]] = None, default: Any = None, @@ -450,7 +446,7 @@ def add_custom_column_spec(self, spec: ColumnSpec) -> 'ColumnSpecsBuilder': self._specs.append(spec) return self - def build(self) -> List[ColumnSpec]: + def build(self) -> list[ColumnSpec]: """Build the final list of column specifications.""" return self._specs.copy() @@ -491,11 +487,11 @@ class ConflictResolution: ... action="IGNORE" ... ) """ - on_conflict_fields: Union[str, List[str]] + on_conflict_fields: Union[str, list[str]] action: Literal["UPDATE", "IGNORE"] = "UPDATE" - update_fields: Optional[List[str]] = None + update_fields: Optional[list[str]] = None - def maybe_set_default_update_fields(self, columns: List[str]): + def maybe_set_default_update_fields(self, columns: list[str]): if self.action != "UPDATE": return if self.update_fields is not None: diff --git a/sdks/python/apache_beam/ml/rag/ingestion/postgres_it_test.py b/sdks/python/apache_beam/ml/rag/ingestion/postgres_it_test.py index adbe28b5d086..2da715bbd804 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/postgres_it_test.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/postgres_it_test.py @@ -21,7 +21,6 @@ import secrets import time import unittest -from typing import List from typing import NamedTuple import psycopg2 @@ -50,7 +49,7 @@ 'CustomSpecsRow', [ ('custom_id', str), # For id_spec test - ('embedding_vec', List[float]), # For embedding_spec test + ('embedding_vec', list[float]), # For embedding_spec test ('content_col', str), # For content_spec test ('metadata', str) ]) @@ -63,7 +62,7 @@ ('source', str), # For metadata_spec and composite key ('timestamp', str), # For metadata_spec and composite key ('content', str), - ('embedding', List[float]), + ('embedding', list[float]), ('metadata', str) ]) registry.register_coder(MetadataConflictRow, RowCoder) diff --git a/sdks/python/apache_beam/ml/rag/ingestion/spanner.py b/sdks/python/apache_beam/ml/rag/ingestion/spanner.py index 8e108759721e..c3e29f4a6e33 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/spanner.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/spanner.py @@ -65,11 +65,9 @@ from dataclasses import dataclass from typing import Any from typing import Callable -from typing import List from typing import Literal from typing import NamedTuple from typing import Optional -from typing import Type import apache_beam as beam from apache_beam.coders import registry @@ -108,7 +106,7 @@ class SpannerColumnSpec: ... ) """ column_name: str - python_type: Type + python_type: type value_fn: Callable[[EmbeddableItem], Any] @@ -137,7 +135,7 @@ class SpannerColumnSpecsBuilder: ... ) """ def __init__(self): - self._specs: List[SpannerColumnSpec] = [] + self._specs: list[SpannerColumnSpec] = [] @staticmethod def with_defaults() -> 'SpannerColumnSpecsBuilder': @@ -159,7 +157,7 @@ def with_defaults() -> 'SpannerColumnSpecsBuilder': def with_id_spec( self, column_name: str = "id", - python_type: Type = str, + python_type: type = str, convert_fn: Optional[Callable[[str], Any]] = None ) -> 'SpannerColumnSpecsBuilder': """Add ID column specification. @@ -195,7 +193,7 @@ def with_id_spec( def with_embedding_spec( self, column_name: str = "embedding", - convert_fn: Optional[Callable[[List[float]], List[float]]] = None + convert_fn: Optional[Callable[[list[float]], list[float]]] = None ) -> 'SpannerColumnSpecsBuilder': """Add embedding array column (ARRAY or ARRAY). @@ -221,7 +219,7 @@ def with_embedding_spec( ... convert_fn=lambda vec: [round(x, 4) for x in vec] ... ) """ - def extract_fn(embeddable: EmbeddableItem) -> List[float]: + def extract_fn(embeddable: EmbeddableItem) -> list[float]: if not embeddable.dense_embedding: raise ValueError(f'EmbeddableItem must contain embedding: {embeddable}') return embeddable.dense_embedding @@ -229,7 +227,7 @@ def extract_fn(embeddable: EmbeddableItem) -> List[float]: self._specs.append( SpannerColumnSpec( column_name=column_name, - python_type=List[float], + python_type=list[float], value_fn=functools.partial( _extract_and_convert, extract_fn, convert_fn))) return self @@ -237,7 +235,7 @@ def extract_fn(embeddable: EmbeddableItem) -> List[float]: def with_content_spec( self, column_name: str = "content", - python_type: Type = str, + python_type: type = str, convert_fn: Optional[Callable[[str], Any]] = None ) -> 'SpannerColumnSpecsBuilder': """Add content column. @@ -301,7 +299,7 @@ def with_metadata_spec( def add_metadata_field( self, field: str, - python_type: Type, + python_type: type, column_name: Optional[str] = None, convert_fn: Optional[Callable[[Any], Any]] = None, default: Any = None) -> 'SpannerColumnSpecsBuilder': @@ -369,7 +367,7 @@ def value_fn(embeddable: EmbeddableItem) -> Any: def add_column( self, column_name: str, - python_type: Type, + python_type: type, value_fn: Callable[[EmbeddableItem], Any]) -> 'SpannerColumnSpecsBuilder': """Add a custom column with full control. @@ -402,7 +400,7 @@ def add_column( value_fn=value_fn)) return self - def build(self) -> List[SpannerColumnSpec]: + def build(self) -> list[SpannerColumnSpec]: """Build the final list of column specifications. Returns: @@ -417,7 +415,7 @@ class _SpannerSchemaBuilder: Creates a NamedTuple type from column specifications and registers it with Beam's RowCoder for serialization. """ - def __init__(self, table_name: str, column_specs: List[SpannerColumnSpec]): + def __init__(self, table_name: str, column_specs: list[SpannerColumnSpec]): """Initialize schema builder. Args: @@ -511,7 +509,7 @@ def __init__( table_name: str, *, # Schema configuration - column_specs: Optional[List[SpannerColumnSpec]] = None, + column_specs: Optional[list[SpannerColumnSpec]] = None, # Write operation type write_mode: Literal["INSERT", "UPDATE", "REPLACE", "INSERT_OR_UPDATE"] = "INSERT_OR_UPDATE", diff --git a/sdks/python/apache_beam/ml/rag/ingestion/test_utils.py b/sdks/python/apache_beam/ml/rag/ingestion/test_utils.py index 0373874c09d2..29d9ab1c7e37 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/test_utils.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/test_utils.py @@ -17,7 +17,6 @@ import hashlib import json -from typing import List import apache_beam as beam from apache_beam.ml.rag.types import Chunk @@ -57,7 +56,7 @@ def get_expected_values( range_start: int, range_end: int, content_prefix: str = "Testval", - seed_multiplier: int = 1) -> List[Chunk]: + seed_multiplier: int = 1) -> list[Chunk]: """Returns a range of test Chunks.""" return [ ChunkTestUtils.from_seed(i, content_prefix, seed_multiplier) diff --git a/sdks/python/apache_beam/ml/rag/test_utils.py b/sdks/python/apache_beam/ml/rag/test_utils.py index 6abe65d560b3..72f7bde5d80e 100644 --- a/sdks/python/apache_beam/ml/rag/test_utils.py +++ b/sdks/python/apache_beam/ml/rag/test_utils.py @@ -23,7 +23,6 @@ import unittest from dataclasses import dataclass from typing import Callable -from typing import List from typing import Optional from typing import cast @@ -244,7 +243,7 @@ def create_client(): exception_types=(MilvusException, )) # Configure schema. - field_schemas: List[FieldSchema] = cast(List[FieldSchema], config["fields"]) + field_schemas: list[FieldSchema] = cast(list[FieldSchema], config["fields"]) schema = CollectionSchema( fields=field_schemas, functions=config["functions"]) @@ -345,7 +344,7 @@ def create_user_yaml(service_port: int, max_vector_field_num=5): @staticmethod def assert_chunks_equivalent( - actual_chunks: List[Chunk], expected_chunks: List[Chunk]): + actual_chunks: list[Chunk], expected_chunks: list[Chunk]): """assert_chunks_equivalent checks for presence rather than exact match""" # Sort both lists by ID to ensure consistent ordering. actual_sorted = sorted(actual_chunks, key=lambda c: c.id) diff --git a/sdks/python/apache_beam/ml/rag/types.py b/sdks/python/apache_beam/ml/rag/types.py index 0de93a35306e..0128b1ecd0db 100644 --- a/sdks/python/apache_beam/ml/rag/types.py +++ b/sdks/python/apache_beam/ml/rag/types.py @@ -32,10 +32,7 @@ from dataclasses import dataclass from dataclasses import field from typing import Any -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Union @@ -60,8 +57,8 @@ class Embedding: dense_embedding: Dense vector representation. sparse_embedding: Optional sparse vector representation for hybrid search. """ - dense_embedding: Optional[List[float]] = None - sparse_embedding: Optional[Tuple[List[int], List[float]]] = None + dense_embedding: Optional[list[float]] = None + sparse_embedding: Optional[tuple[list[int], list[float]]] = None @dataclass @@ -95,7 +92,7 @@ class EmbeddableItem: content: Content id: str = field(default_factory=lambda: str(uuid.uuid4())) index: int = 0 - metadata: Dict[str, Any] = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) embedding: Optional[Embedding] = None @classmethod @@ -105,7 +102,7 @@ def from_text( *, id: Optional[str] = None, index: int = 0, - metadata: Optional[Dict[str, Any]] = None, + metadata: Optional[dict[str, Any]] = None, ) -> 'EmbeddableItem': """Create an EmbeddableItem with text content. @@ -128,7 +125,7 @@ def from_image( image: Union[bytes, str], *, id: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, + metadata: Optional[dict[str, Any]] = None, ) -> 'EmbeddableItem': """Create an EmbeddableItem with image content. @@ -145,11 +142,11 @@ def from_image( ) @property - def dense_embedding(self) -> Optional[List[float]]: + def dense_embedding(self) -> Optional[list[float]]: return self.embedding.dense_embedding if self.embedding else None @property - def sparse_embedding(self) -> Optional[Tuple[List[int], List[float]]]: + def sparse_embedding(self) -> Optional[tuple[list[int], list[float]]]: return self.embedding.sparse_embedding if self.embedding else None @property diff --git a/sdks/python/apache_beam/ml/rag/utils.py b/sdks/python/apache_beam/ml/rag/utils.py index e2d9962467a1..f56bfe518485 100644 --- a/sdks/python/apache_beam/ml/rag/utils.py +++ b/sdks/python/apache_beam/ml/rag/utils.py @@ -24,11 +24,7 @@ from dataclasses import field from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple -from typing import Type from apache_beam.ml.rag.types import Chunk from apache_beam.ml.rag.types import Content @@ -65,7 +61,7 @@ class MilvusConnectionParameters: db_name: str = "default" token: str = field(default_factory=str) timeout: Optional[float] = None - kwargs: Dict[str, Any] = field(default_factory=dict) + kwargs: dict[str, Any] = field(default_factory=dict) def __post_init__(self): if not self.uri: @@ -82,8 +78,8 @@ class MilvusHelpers: """Utility class providing helper methods for Milvus vector db operations.""" @staticmethod def sparse_embedding( - sparse_vector: Optional[Tuple[List[int], List[float]]] - ) -> Optional[Dict[int, float]]: + sparse_vector: Optional[tuple[list[int], list[float]]] + ) -> Optional[dict[int, float]]: if not sparse_vector: return None # Converts sparse embedding from (indices, values) tuple format to @@ -92,7 +88,7 @@ def sparse_embedding( return {int(idx): float(val) for idx, val in zip(indices, values)} -def parse_chunk_strings(chunk_str_list: List[str]) -> List[Chunk]: +def parse_chunk_strings(chunk_str_list: list[str]) -> list[Chunk]: parsed_chunks = [] # Define safe globals and disable built-in functions for safety. @@ -149,7 +145,7 @@ def retry_with_backoff( retry_delay: float = 1.0, retry_backoff_factor: float = 2.0, operation_name: str = "operation", - exception_types: Tuple[Type[BaseException], ...] = (Exception, ) + exception_types: tuple[type[BaseException], ...] = (Exception, ) ) -> Any: """Executes an operation with retry logic and exponential backoff. diff --git a/sdks/python/apache_beam/ml/transforms/base.py b/sdks/python/apache_beam/ml/transforms/base.py index 4031777ce152..f52e9df40566 100644 --- a/sdks/python/apache_beam/ml/transforms/base.py +++ b/sdks/python/apache_beam/ml/transforms/base.py @@ -25,10 +25,8 @@ from collections.abc import Sequence from dataclasses import dataclass from typing import Any -from typing import Dict from typing import Generic from typing import Iterable -from typing import List from typing import Optional from typing import TypeVar from typing import Union @@ -85,9 +83,9 @@ class EmbeddingTypeAdapter(Generic[EmbeddingTypeAdapterInputT, input_fn: Function to extract text for embedding from input type output_fn: Function to create output type from input and embeddings """ - input_fn: Callable[[Sequence[EmbeddingTypeAdapterInputT]], List[str]] + input_fn: Callable[[Sequence[EmbeddingTypeAdapterInputT]], list[str]] output_fn: Callable[[Sequence[EmbeddingTypeAdapterInputT], Sequence[Any]], - List[EmbeddingTypeAdapterOutputT]] + list[EmbeddingTypeAdapterOutputT]] def __reduce__(self): """Custom serialization that preserves type information during @@ -184,8 +182,8 @@ def append_transform(self, transform: BaseOperation): def _dict_input_fn( - columns: Sequence[str], batch: Sequence[Union[Dict[str, Any], - beam.Row]]) -> List[str]: + columns: Sequence[str], batch: Sequence[Union[dict[str, Any], + beam.Row]]) -> list[str]: """Extract text from specified columns in batch.""" if batch and hasattr(batch[0], '_asdict'): batch = [row._asdict() if hasattr(row, '_asdict') else row for row in batch] @@ -222,7 +220,7 @@ def _dict_input_fn( def _dict_output_fn( columns: Sequence[str], - batch: Sequence[Union[Dict[str, Any], beam.Row]], + batch: Sequence[Union[dict[str, Any], beam.Row]], embeddings: Sequence[Any]) -> list[Union[dict[str, Any], beam.Row]]: """Map embeddings back to columns in batch.""" is_beam_row = False @@ -244,15 +242,15 @@ def _dict_output_fn( def _create_dict_adapter( - columns: List[str]) -> EmbeddingTypeAdapter[Dict[str, Any], Dict[str, Any]]: + columns: list[str]) -> EmbeddingTypeAdapter[dict[str, Any], dict[str, Any]]: """Create adapter for dict-based processing.""" - return EmbeddingTypeAdapter[Dict[str, Any], Dict[str, Any]]( + return EmbeddingTypeAdapter[dict[str, Any], dict[str, Any]]( input_fn=cast( - Callable[[Sequence[Dict[str, Any]]], List[str]], + Callable[[Sequence[dict[str, Any]]], list[str]], functools.partial(_dict_input_fn, columns)), output_fn=cast( - Callable[[Sequence[Dict[str, Any]], Sequence[Any]], - List[Dict[str, Any]]], + Callable[[Sequence[dict[str, Any]], Sequence[Any]], + list[dict[str, Any]]], functools.partial(_dict_output_fn, columns))) diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index d60d75283eab..a79bddb21ab9 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -26,11 +26,8 @@ import os from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional from typing import Sequence -from typing import Type from typing import TypeVar import apache_beam as beam @@ -488,7 +485,7 @@ def get_all_options( retain_unknown_options=False, display_warnings=False, current_only=False, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Returns a dictionary of all defined arguments. Returns a dictionary of all defined arguments into a dictionary. @@ -629,7 +626,7 @@ def from_urn(key): def display_data(self): return self.get_all_options(drop_default=True, retain_unknown_options=True) - def view_as(self, cls: Type[PipelineOptionsT]) -> PipelineOptionsT: + def view_as(self, cls: type[PipelineOptionsT]) -> PipelineOptionsT: """Returns a view of current object as provided PipelineOption subclass. Example Usage:: @@ -687,11 +684,11 @@ def is_compat_version_prior_to(self, breaking_change_version): v2_parts = (breaking_change_version.split('.') + ['0', '0', '0'])[:3] return tuple(map(int, v1_parts)) < tuple(map(int, v2_parts)) - def _visible_option_list(self) -> List[str]: + def _visible_option_list(self) -> list[str]: return sorted( option for option in dir(self._visible_options) if option[0] != '_') - def __dir__(self) -> List[str]: + def __dir__(self) -> list[str]: return sorted( dir(type(self)) + list(self.__dict__) + self._visible_option_list()) @@ -853,7 +850,7 @@ def additional_option_ptransform_fn(): # Optional type checks that aren't enabled by default. -additional_type_checks: Dict[str, Callable[[], None]] = { +additional_type_checks: dict[str, Callable[[], None]] = { 'ptransform_fn': additional_option_ptransform_fn, } @@ -2169,7 +2166,7 @@ class OptionsContext(object): Can also be used as a decorator. """ - overrides: List[Dict[str, Any]] = [] + overrides: list[dict[str, Any]] = [] def __init__(self, **options): self.options = options diff --git a/sdks/python/apache_beam/options/value_provider.py b/sdks/python/apache_beam/options/value_provider.py index fa1649beed26..e6cddafc5cb9 100644 --- a/sdks/python/apache_beam/options/value_provider.py +++ b/sdks/python/apache_beam/options/value_provider.py @@ -25,7 +25,6 @@ # pytype: skip-file from functools import wraps -from typing import Set from apache_beam import error @@ -95,7 +94,7 @@ class RuntimeValueProvider(ValueProvider): at graph construction time. """ runtime_options = None - experiments: Set[str] = set() + experiments: set[str] = set() def __init__(self, option_name, value_type, default_value): self.option_name = option_name diff --git a/sdks/python/apache_beam/pipeline.py b/sdks/python/apache_beam/pipeline.py index 3cce2c5bb773..750868f7443a 100644 --- a/sdks/python/apache_beam/pipeline.py +++ b/sdks/python/apache_beam/pipeline.py @@ -65,7 +65,6 @@ from typing import TYPE_CHECKING from typing import Any from typing import Optional -from typing import Type from typing import Union from google.protobuf import message @@ -644,7 +643,7 @@ def __enter__(self) -> 'Pipeline': def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional['TracebackType']) -> None: @@ -1021,7 +1020,7 @@ def expand(self, pcoll): if pcoll.element_type is None: pcoll.element_type = typehints.Any - def __reduce__(self) -> tuple[Type, tuple[str, ...]]: + def __reduce__(self) -> tuple[type, tuple[str, ...]]: # Some transforms contain a reference to their enclosing pipeline, # which in turn reference all other transforms (resulting in quadratic # time/space to pickle each transform individually). As we don't diff --git a/sdks/python/apache_beam/pvalue.py b/sdks/python/apache_beam/pvalue.py index 1cd220cc2566..ee885af13367 100644 --- a/sdks/python/apache_beam/pvalue.py +++ b/sdks/python/apache_beam/pvalue.py @@ -30,7 +30,6 @@ import itertools from typing import TYPE_CHECKING from typing import Any -from typing import Dict from typing import Generic from typing import Iterator from typing import NamedTuple @@ -255,7 +254,7 @@ def __init__( # gets applied. self.producer: Optional[AppliedPTransform] = None # Dictionary of PCollections already associated with tags. - self._pcolls: Dict[Optional[str], PCollection] = {} + self._pcolls: dict[Optional[str], PCollection] = {} def __str__(self): return '<%s>' % self._str_internal() diff --git a/sdks/python/apache_beam/runners/common.py b/sdks/python/apache_beam/runners/common.py index 034090cf7bdc..c22072dbf8b9 100644 --- a/sdks/python/apache_beam/runners/common.py +++ b/sdks/python/apache_beam/runners/common.py @@ -22,6 +22,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import logging import sys import threading diff --git a/sdks/python/apache_beam/runners/dask/dask_runner.py b/sdks/python/apache_beam/runners/dask/dask_runner.py index bc915d300857..b14449ab2fad 100644 --- a/sdks/python/apache_beam/runners/dask/dask_runner.py +++ b/sdks/python/apache_beam/runners/dask/dask_runner.py @@ -59,7 +59,7 @@ def _parse_timeout(candidate): return dask.config.no_default @staticmethod - def _extract_bag_kwargs(dask_options: t.Dict) -> t.Dict: + def _extract_bag_kwargs(dask_options: dict) -> dict: """Parse keyword arguments for `dask.Bag`s; used in graph translation.""" out = {} @@ -174,7 +174,7 @@ def to_dask_bag_visitor(bag_kwargs=None) -> PipelineVisitor: @dataclasses.dataclass class DaskBagVisitor(PipelineVisitor): - bags: t.Dict[AppliedPTransform, db.Bag] = dataclasses.field( + bags: dict[AppliedPTransform, db.Bag] = dataclasses.field( default_factory=collections.OrderedDict) def visit_transform(self, transform_node: AppliedPTransform) -> None: diff --git a/sdks/python/apache_beam/runners/dask/overrides.py b/sdks/python/apache_beam/runners/dask/overrides.py index b952834f12d7..357e51208775 100644 --- a/sdks/python/apache_beam/runners/dask/overrides.py +++ b/sdks/python/apache_beam/runners/dask/overrides.py @@ -32,7 +32,7 @@ @dataclasses.dataclass class _Create(beam.PTransform): - values: t.Tuple[t.Any] + values: tuple[t.Any] def expand(self, input_or_inputs): return beam.pvalue.PCollection.from_(input_or_inputs) @@ -56,8 +56,8 @@ def expand(self, input_or_inputs): return beam.pvalue.PCollection.from_(input_or_inputs) -@typehints.with_input_types(t.Tuple[K, V]) -@typehints.with_output_types(t.Tuple[K, t.Iterable[V]]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, t.Iterable[V]]) class _GroupByKeyOnly(beam.PTransform): def expand(self, input_or_inputs): return beam.pvalue.PCollection.from_(input_or_inputs) @@ -70,8 +70,8 @@ def infer_output_type(self, input_type): return typehints.KV[key_type, typehints.Iterable[value_type]] -@typehints.with_input_types(t.Tuple[K, t.Iterable[V]]) -@typehints.with_output_types(t.Tuple[K, t.Iterable[V]]) +@typehints.with_input_types(tuple[K, t.Iterable[V]]) +@typehints.with_output_types(tuple[K, t.Iterable[V]]) class _GroupAlsoByWindow(beam.ParDo): def __init__(self, windowing): super().__init__(_GroupAlsoByWindowDoFn(windowing)) @@ -81,8 +81,8 @@ def expand(self, input_or_inputs): return beam.pvalue.PCollection.from_(input_or_inputs) -@typehints.with_input_types(t.Tuple[K, V]) -@typehints.with_output_types(t.Tuple[K, t.Iterable[V]]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, t.Iterable[V]]) class _GroupByKey(beam.PTransform): def expand(self, input_or_inputs): return ( @@ -105,7 +105,7 @@ def expand(self, input_or_inputs): return beam.pvalue.PCollection(self.pipeline, is_bounded=is_bounded) -def dask_overrides() -> t.List[PTransformOverride]: +def dask_overrides() -> list[PTransformOverride]: class CreateOverride(PTransformOverride): def matches(self, applied_ptransform: AppliedPTransform) -> bool: return applied_ptransform.transform.__class__ == beam.Create diff --git a/sdks/python/apache_beam/runners/dask/transform_evaluator.py b/sdks/python/apache_beam/runners/dask/transform_evaluator.py index 6fd216fadb53..dbf55a3cee0d 100644 --- a/sdks/python/apache_beam/runners/dask/transform_evaluator.py +++ b/sdks/python/apache_beam/runners/dask/transform_evaluator.py @@ -100,7 +100,7 @@ def __iter__(self): class TaggingReceiver(Receiver): """A Receiver that handles tagged `WindowValue`s.""" tag: str - values: t.List[PCollVal] + values: list[PCollVal] def receive(self, windowed_value: WindowedValue): if self.tag: @@ -113,7 +113,7 @@ def receive(self, windowed_value: WindowedValue): @dataclasses.dataclass class OneReceiver(dict): """A Receiver that tags value via dictionary lookup key.""" - values: t.List[PCollVal] = field(default_factory=list) + values: list[PCollVal] = field(default_factory=list) def __missing__(self, key): if key not in self: @@ -135,7 +135,7 @@ class DaskBagOp(abc.ABC): from the pipeline's `DaskOptions`. """ applied: AppliedPTransform - bag_kwargs: t.Dict = dataclasses.field(default_factory=dict) + bag_kwargs: dict = dataclasses.field(default_factory=dict) @property def transform(self): @@ -263,7 +263,7 @@ def value(item): class Flatten(DaskBagOp): """Produces a flattened bag from a collection of bags.""" def apply( - self, input_bag: t.List[db.Bag], side_inputs: OpSide = None) -> db.Bag: + self, input_bag: list[db.Bag], side_inputs: OpSide = None) -> db.Bag: assert isinstance(input_bag, list), 'Must take a sequence of bags!' return db.concat(input_bag) diff --git a/sdks/python/apache_beam/runners/direct/bundle_factory.py b/sdks/python/apache_beam/runners/direct/bundle_factory.py index 95d8c06111a2..ef87b168d93e 100644 --- a/sdks/python/apache_beam/runners/direct/bundle_factory.py +++ b/sdks/python/apache_beam/runners/direct/bundle_factory.py @@ -21,7 +21,6 @@ from typing import Iterable from typing import Iterator -from typing import List from typing import Union from typing import cast @@ -124,7 +123,7 @@ def __init__( stacked: bool = True) -> None: assert isinstance(pcollection, (pvalue.PBegin, pvalue.PCollection)) self._pcollection = pcollection - self._elements: List[Union[WindowedValue, + self._elements: list[Union[WindowedValue, _Bundle._StackedWindowedValues]] = [] self._stacked = stacked self._committed = False @@ -144,7 +143,7 @@ def get_elements_iterable(self, """ if not self._stacked: # we can safely assume self._elements contains only WindowedValues - elements = cast('List[WindowedValue]', self._elements) + elements = cast('list[WindowedValue]', self._elements) if self._committed and not make_copy: return elements return list(elements) diff --git a/sdks/python/apache_beam/runners/direct/consumer_tracking_pipeline_visitor.py b/sdks/python/apache_beam/runners/direct/consumer_tracking_pipeline_visitor.py index 91085274f32a..d4559607ae94 100644 --- a/sdks/python/apache_beam/runners/direct/consumer_tracking_pipeline_visitor.py +++ b/sdks/python/apache_beam/runners/direct/consumer_tracking_pipeline_visitor.py @@ -19,9 +19,6 @@ # pytype: skip-file -from typing import Dict -from typing import Set - from apache_beam import pvalue from apache_beam.pipeline import AppliedPTransform from apache_beam.pipeline import PipelineVisitor @@ -37,9 +34,9 @@ class ConsumerTrackingPipelineVisitor(PipelineVisitor): transform has produced and committed output. """ def __init__(self): - self.value_to_consumers: Dict[pvalue.PValue, Set[AppliedPTransform]] = {} - self.root_transforms: Set[AppliedPTransform] = set() - self.step_names: Dict[AppliedPTransform, str] = {} + self.value_to_consumers: dict[pvalue.PValue, set[AppliedPTransform]] = {} + self.root_transforms: set[AppliedPTransform] = set() + self.step_names: dict[AppliedPTransform, str] = {} self._num_transforms = 0 self._views = set() diff --git a/sdks/python/apache_beam/runners/direct/direct_runner.py b/sdks/python/apache_beam/runners/direct/direct_runner.py index 73b0321b5de4..443dbd63063d 100644 --- a/sdks/python/apache_beam/runners/direct/direct_runner.py +++ b/sdks/python/apache_beam/runners/direct/direct_runner.py @@ -250,8 +250,8 @@ def visit_transform(self, applied_ptransform): V = typing.TypeVar('V') -@typehints.with_input_types(typing.Tuple[K, V]) -@typehints.with_output_types(typing.Tuple[K, typing.Iterable[V]]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, typing.Iterable[V]]) class _GroupByKeyOnly(PTransform): """A group by key transform, ignoring windows.""" def infer_output_type(self, input_type): @@ -263,8 +263,8 @@ def expand(self, pcoll): return PCollection.from_(pcoll) -@typehints.with_input_types(typing.Tuple[K, typing.Iterable[V]]) -@typehints.with_output_types(typing.Tuple[K, typing.Iterable[V]]) +@typehints.with_input_types(tuple[K, typing.Iterable[V]]) +@typehints.with_output_types(tuple[K, typing.Iterable[V]]) class _GroupAlsoByWindow(ParDo): """The GroupAlsoByWindow transform.""" def __init__(self, windowing): @@ -301,8 +301,8 @@ def process(self, element): return self.driver.process_entire_key(k, vs) -@typehints.with_input_types(typing.Tuple[K, V]) -@typehints.with_output_types(typing.Tuple[K, typing.Iterable[V]]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, typing.Iterable[V]]) class _StreamingGroupByKeyOnly(_GroupByKeyOnly): """Streaming GroupByKeyOnly placeholder for overriding in DirectRunner.""" urn = "direct_runner:streaming_gbko:v0.1" @@ -318,8 +318,8 @@ def from_runner_api_parameter( return _StreamingGroupByKeyOnly() -@typehints.with_input_types(typing.Tuple[K, typing.Iterable[V]]) -@typehints.with_output_types(typing.Tuple[K, typing.Iterable[V]]) +@typehints.with_input_types(tuple[K, typing.Iterable[V]]) +@typehints.with_output_types(tuple[K, typing.Iterable[V]]) class _StreamingGroupAlsoByWindow(_GroupAlsoByWindow): """Streaming GroupAlsoByWindow placeholder for overriding in DirectRunner.""" urn = "direct_runner:streaming_gabw:v0.1" @@ -338,8 +338,8 @@ def from_runner_api_parameter(unused_ptransform, payload, context): context.windowing_strategies.get_by_id(payload.value)) -@typehints.with_input_types(typing.Tuple[K, typing.Iterable[V]]) -@typehints.with_output_types(typing.Tuple[K, typing.Iterable[V]]) +@typehints.with_input_types(tuple[K, typing.Iterable[V]]) +@typehints.with_output_types(tuple[K, typing.Iterable[V]]) class _GroupByKey(PTransform): """The DirectRunner GroupByKey implementation.""" def expand(self, pcoll): diff --git a/sdks/python/apache_beam/runners/direct/evaluation_context.py b/sdks/python/apache_beam/runners/direct/evaluation_context.py index 6138577bb91d..a6618f000d29 100644 --- a/sdks/python/apache_beam/runners/direct/evaluation_context.py +++ b/sdks/python/apache_beam/runners/direct/evaluation_context.py @@ -23,12 +23,8 @@ import threading from typing import TYPE_CHECKING from typing import Any -from typing import DefaultDict -from typing import Dict from typing import Iterable -from typing import List from typing import Optional -from typing import Tuple from typing import Union from apache_beam import pvalue @@ -91,10 +87,10 @@ class _SideInputsContainer(object): """ def __init__(self, side_inputs: Iterable['pvalue.AsSideInput']) -> None: self._lock = threading.Lock() - self._views: Dict[pvalue.AsSideInput, _SideInputView] = {} - self._transform_to_side_inputs: DefaultDict[ + self._views: dict[pvalue.AsSideInput, _SideInputView] = {} + self._transform_to_side_inputs: collections.defaultdict[ Optional[AppliedPTransform], - List[pvalue.AsSideInput]] = collections.defaultdict(list) + list[pvalue.AsSideInput]] = collections.defaultdict(list) # this appears unused: self._side_input_to_blocked_tasks = collections.defaultdict(list) # type: ignore @@ -139,7 +135,7 @@ def add_values(self, side_input, values): view.elements.extend(values) def update_watermarks_for_transform_and_unblock_tasks( - self, ptransform, watermark) -> List[Tuple[TransformExecutor, Timestamp]]: + self, ptransform, watermark) -> list[tuple[TransformExecutor, Timestamp]]: """Updates _SideInputsContainer after a watermark update and unbloks tasks. It traverses the list of side inputs per PTransform and calls @@ -160,7 +156,7 @@ def update_watermarks_for_transform_and_unblock_tasks( return unblocked_tasks def _update_watermarks_for_side_input_and_unblock_tasks( - self, side_input, watermark) -> List[Tuple[TransformExecutor, Timestamp]]: + self, side_input, watermark) -> list[tuple[TransformExecutor, Timestamp]]: """Helps update _SideInputsContainer after a watermark update. For each view of the side input, it updates the value of the watermark @@ -241,9 +237,9 @@ def __init__( self._value_to_consumers = value_to_consumers self._step_names = step_names self.views = views - self._pcollection_to_views: DefaultDict[ + self._pcollection_to_views: collections.defaultdict[ pvalue.PValue, - List[pvalue.AsSideInput]] = collections.defaultdict(list) + list[pvalue.AsSideInput]] = collections.defaultdict(list) for view in views: self._pcollection_to_views[view.pvalue].append(view) self._transform_keyed_states = self._initialize_keyed_states( @@ -254,7 +250,7 @@ def __init__( root_transforms, value_to_consumers, self._transform_keyed_states) - self._pending_unblocked_tasks: List[Tuple[TransformExecutor, + self._pending_unblocked_tasks: list[tuple[TransformExecutor, Timestamp]] = [] self._counter_factory = counters.CounterFactory() self._metrics = DirectMetrics() @@ -370,7 +366,7 @@ def _commit_bundles( self, uncommitted_bundles: Iterable['_Bundle'], unprocessed_bundles: Iterable['_Bundle'] - ) -> Tuple[Tuple['_Bundle', ...], Tuple['_Bundle', ...]]: + ) -> tuple[tuple['_Bundle', ...], tuple['_Bundle', ...]]: """Commits bundles and returns a immutable set of committed bundles.""" for in_progress_bundle in uncommitted_bundles: producing_applied_ptransform = in_progress_bundle.pcollection.producer @@ -401,7 +397,7 @@ def create_empty_committed_bundle( output_pcollection) def extract_all_timers( - self) -> Tuple[List[Tuple[AppliedPTransform, List['TimerFiring']]], bool]: + self) -> tuple[list[tuple[AppliedPTransform, list['TimerFiring']]], bool]: return self._watermark_manager.extract_all_timers() def is_done(self, transform: Optional[AppliedPTransform] = None) -> bool: diff --git a/sdks/python/apache_beam/runners/direct/executor.py b/sdks/python/apache_beam/runners/direct/executor.py index e8be9d64f993..6bc2b0e4c3f1 100644 --- a/sdks/python/apache_beam/runners/direct/executor.py +++ b/sdks/python/apache_beam/runners/direct/executor.py @@ -27,10 +27,7 @@ import traceback from typing import TYPE_CHECKING from typing import Any -from typing import Dict -from typing import FrozenSet from typing import Optional -from typing import Set from weakref import WeakValueDictionary from apache_beam.metrics.execution import MetricsContainer @@ -145,7 +142,7 @@ def shutdown(self): class _TransformEvaluationState(object): - def __init__(self, executor_service, scheduled: Set['TransformExecutor']): + def __init__(self, executor_service, scheduled: set['TransformExecutor']): self.executor_service = executor_service self.scheduled = scheduled @@ -212,7 +209,7 @@ class _TransformExecutorServices(object): """ def __init__(self, executor_service: _ExecutorService) -> None: self._executor_service = executor_service - self._scheduled: Set[TransformExecutor] = set() + self._scheduled: set[TransformExecutor] = set() self._parallel = _ParallelEvaluationState( self._executor_service, self._scheduled) self._serial_cache: WeakValueDictionary[ @@ -229,7 +226,7 @@ def serial(self, step: Any) -> _SerialEvaluationState: return cached @property - def executors(self) -> FrozenSet['TransformExecutor']: + def executors(self) -> frozenset['TransformExecutor']: return frozenset(self._scheduled) @@ -305,7 +302,7 @@ def __init__( self._applied_ptransform = applied_ptransform self._completion_callback = completion_callback self._transform_evaluation_state = transform_evaluation_state - self._side_input_values: Dict[pvalue.AsSideInput, Any] = {} + self._side_input_values: dict[pvalue.AsSideInput, Any] = {} self.blocked = False self._call_count = 0 self._retry_count = 0 diff --git a/sdks/python/apache_beam/runners/direct/transform_evaluator.py b/sdks/python/apache_beam/runners/direct/transform_evaluator.py index 49e7d9d02106..2349a0881e40 100644 --- a/sdks/python/apache_beam/runners/direct/transform_evaluator.py +++ b/sdks/python/apache_beam/runners/direct/transform_evaluator.py @@ -27,10 +27,6 @@ from collections import abc from typing import TYPE_CHECKING from typing import Any -from typing import Dict -from typing import List -from typing import Tuple -from typing import Type from apache_beam import coders from apache_beam import io @@ -89,13 +85,13 @@ class TransformEvaluatorRegistry(object): Creates instances of TransformEvaluator for the application of a transform. """ - _test_evaluators_overrides: Dict[Type[core.PTransform], - Type['_TransformEvaluator']] = {} + _test_evaluators_overrides: dict[type[core.PTransform], + type['_TransformEvaluator']] = {} def __init__(self, evaluation_context: 'EvaluationContext') -> None: assert evaluation_context self._evaluation_context = evaluation_context - self._evaluators: Dict[Type[core.PTransform], Type[_TransformEvaluator]] = { + self._evaluators: dict[type[core.PTransform], type[_TransformEvaluator]] = { io.Read: _BoundedReadEvaluator, _DirectReadFromPubSub: _PubSubReadEvaluator, core.Flatten: _FlattenEvaluator, @@ -587,7 +583,7 @@ class _PubSubReadEvaluator(_TransformEvaluator): # A mapping of transform to _PubSubSubscriptionWrapper. # TODO(https://github.com/apache/beam/issues/19751): Prevents garbage # collection of pipeline instances. - _subscription_cache: Dict[AppliedPTransform, str] = {} + _subscription_cache: dict[AppliedPTransform, str] = {} def __init__( self, @@ -651,7 +647,7 @@ def process_element(self, element): pass def _read_from_pubsub( - self, timestamp_attribute) -> List[Tuple[Timestamp, 'PubsubMessage']]: + self, timestamp_attribute) -> list[tuple[Timestamp, 'PubsubMessage']]: from google.cloud import pubsub from apache_beam.io.gcp.pubsub import PubsubMessage diff --git a/sdks/python/apache_beam/runners/direct/watermark_manager.py b/sdks/python/apache_beam/runners/direct/watermark_manager.py index 666ade6cf82d..90970ce7f669 100644 --- a/sdks/python/apache_beam/runners/direct/watermark_manager.py +++ b/sdks/python/apache_beam/runners/direct/watermark_manager.py @@ -21,11 +21,7 @@ import threading from typing import TYPE_CHECKING -from typing import Dict from typing import Iterable -from typing import List -from typing import Set -from typing import Tuple from apache_beam import pipeline from apache_beam import pvalue @@ -55,7 +51,7 @@ def __init__( self._value_to_consumers = value_to_consumers self._transform_keyed_states = transform_keyed_states # AppliedPTransform -> TransformWatermarks - self._transform_to_watermarks: Dict[AppliedPTransform, + self._transform_to_watermarks: dict[AppliedPTransform, _TransformWatermarks] = {} for root_transform in root_transforms: @@ -179,10 +175,10 @@ def _refresh_watermarks(self, applied_ptransform, side_inputs_container): return unblocked_tasks def extract_all_timers( - self) -> Tuple[List[Tuple[AppliedPTransform, List[TimerFiring]]], bool]: + self) -> tuple[list[tuple[AppliedPTransform, list[TimerFiring]]], bool]: """Extracts fired timers for all transforms and reports if there are any timers set.""" - all_timers: List[Tuple[AppliedPTransform, List[TimerFiring]]] = [] + all_timers: list[tuple[AppliedPTransform, list[TimerFiring]]] = [] has_realtime_timer = False for applied_ptransform, tw in self._transform_to_watermarks.items(): fired_timers, had_realtime_timer = tw.extract_transform_timers() @@ -201,19 +197,19 @@ class _TransformWatermarks(object): def __init__(self, clock, keyed_states, transform): self._clock = clock self._keyed_states = keyed_states - self._input_transform_watermarks: List[_TransformWatermarks] = [] + self._input_transform_watermarks: list[_TransformWatermarks] = [] self._input_watermark = WatermarkManager.WATERMARK_NEG_INF self._output_watermark = WatermarkManager.WATERMARK_NEG_INF self._keyed_earliest_holds = {} # Scheduled bundles targeted for this transform. - self._pending: Set['_Bundle'] = set() + self._pending: set['_Bundle'] = set() self._fired_timers = set() self._lock = threading.Lock() self._label = str(transform) def update_input_transform_watermarks( - self, input_transform_watermarks: List['_TransformWatermarks']) -> None: + self, input_transform_watermarks: list['_TransformWatermarks']) -> None: with self._lock: self._input_transform_watermarks = input_transform_watermarks @@ -300,7 +296,7 @@ def refresh(self) -> bool: def synchronized_processing_output_time(self): return self._clock.time() - def extract_transform_timers(self) -> Tuple[List[TimerFiring], bool]: + def extract_transform_timers(self) -> tuple[list[TimerFiring], bool]: """Extracts fired timers and reports of any timers set per transform.""" with self._lock: fired_timers = [] diff --git a/sdks/python/apache_beam/runners/interactive/augmented_pipeline.py b/sdks/python/apache_beam/runners/interactive/augmented_pipeline.py index 519bf3514c53..2b64f44755d4 100644 --- a/sdks/python/apache_beam/runners/interactive/augmented_pipeline.py +++ b/sdks/python/apache_beam/runners/interactive/augmented_pipeline.py @@ -22,9 +22,7 @@ # pytype: skip-file import copy -from typing import Dict from typing import Optional -from typing import Set import apache_beam as beam from apache_beam.portability.api import beam_runner_api_pb2 @@ -43,7 +41,7 @@ class AugmentedPipeline: def __init__( self, user_pipeline: beam.Pipeline, - pcolls: Optional[Set[beam.pvalue.PCollection]] = None): + pcolls: Optional[set[beam.pvalue.PCollection]] = None): """ Initializes a pipelilne for augmenting interactive flavor. @@ -77,7 +75,7 @@ def augmented_pipeline(self) -> beam_runner_api_pb2.Pipeline: def background_recording_pipeline(self) -> beam_runner_api_pb2.Pipeline: raise NotImplementedError - def cacheables(self) -> Dict[beam.pvalue.PCollection, Cacheable]: + def cacheables(self) -> dict[beam.pvalue.PCollection, Cacheable]: """Finds all the cacheable intermediate PCollections in the pipeline with their metadata. """ diff --git a/sdks/python/apache_beam/runners/interactive/caching/read_cache.py b/sdks/python/apache_beam/runners/interactive/caching/read_cache.py index ac7ef5cae561..2b4b3bbef1ba 100644 --- a/sdks/python/apache_beam/runners/interactive/caching/read_cache.py +++ b/sdks/python/apache_beam/runners/interactive/caching/read_cache.py @@ -21,8 +21,6 @@ """ # pytype: skip-file -from typing import Tuple - import apache_beam as beam from apache_beam.portability.api import beam_runner_api_pb2 from apache_beam.runners.interactive import cache_manager as cache @@ -47,7 +45,7 @@ def __init__( self._cacheable = cacheable self._key = repr(cacheable.to_key()) - def read_cache(self) -> Tuple[str, str]: + def read_cache(self) -> tuple[str, str]: """Reads cache of the cacheable PCollection and wires the cache into the pipeline proto. Returns the pipeline-scoped ids of the cacheable PCollection and the cache reading output PCollection that replaces it. @@ -118,7 +116,7 @@ def read_cache(self) -> Tuple[str, str]: return source_id, output_id def _build_runner_api_template( - self) -> Tuple[beam_runner_api_pb2.Pipeline, beam.pvalue.PCollection]: + self) -> tuple[beam_runner_api_pb2.Pipeline, beam.pvalue.PCollection]: transform = _ReadCacheTransform(self._cache_manager, self._key) tmp_pipeline = beam.Pipeline() tmp_pipeline.component_id_map = self._context.component_id_map diff --git a/sdks/python/apache_beam/runners/interactive/caching/write_cache.py b/sdks/python/apache_beam/runners/interactive/caching/write_cache.py index e56073b009cb..0e1c58bb342e 100644 --- a/sdks/python/apache_beam/runners/interactive/caching/write_cache.py +++ b/sdks/python/apache_beam/runners/interactive/caching/write_cache.py @@ -21,8 +21,6 @@ """ # pytype: skip-file -from typing import Tuple - import apache_beam as beam from apache_beam.portability.api import beam_runner_api_pb2 from apache_beam.runners.interactive import cache_manager as cache @@ -124,7 +122,7 @@ def write_cache(self) -> None: inputs[key] = input_id def _build_runner_api_template( - self) -> Tuple[beam_runner_api_pb2.Pipeline, '_PCollectionPlaceHolder']: + self) -> tuple[beam_runner_api_pb2.Pipeline, '_PCollectionPlaceHolder']: pph = _PCollectionPlaceHolder(self._cacheable.pcoll, self._context) transform = _WriteCacheTransform(self._cache_manager, self._key) _ = pph.placeholder_pcoll | 'sink_cache_' + self._key >> transform diff --git a/sdks/python/apache_beam/runners/interactive/dataproc/dataproc_cluster_manager.py b/sdks/python/apache_beam/runners/interactive/dataproc/dataproc_cluster_manager.py index f15541d423ac..817d4f6b583b 100644 --- a/sdks/python/apache_beam/runners/interactive/dataproc/dataproc_cluster_manager.py +++ b/sdks/python/apache_beam/runners/interactive/dataproc/dataproc_cluster_manager.py @@ -21,7 +21,6 @@ import re import time from typing import Optional -from typing import Tuple from apache_beam import version as beam_version from apache_beam.options.pipeline_options import PipelineOptions @@ -314,7 +313,7 @@ def get_staging_location(self) -> str: self.cluster_metadata.cluster_name) raise e - def parse_master_url_and_dashboard(self, line: str) -> Tuple[str, str]: + def parse_master_url_and_dashboard(self, line: str) -> tuple[str, str]: """Parses the master_url and YARN application_id of the Flink process from an input line. The line containing both the master_url and application id is always formatted as such: @@ -340,7 +339,7 @@ def parse_master_url_and_dashboard(self, line: str) -> Tuple[str, str]: yarn_endpoint) return master_url, dashboard - def get_master_url_and_dashboard(self) -> Tuple[Optional[str], Optional[str]]: + def get_master_url_and_dashboard(self) -> tuple[Optional[str], Optional[str]]: """Returns the master_url of the current cluster.""" startup_logs = [] for file in self._fs._list(self._staging_directory): diff --git a/sdks/python/apache_beam/runners/interactive/display/pipeline_graph.py b/sdks/python/apache_beam/runners/interactive/display/pipeline_graph.py index 10058351938e..e1d66cd10600 100644 --- a/sdks/python/apache_beam/runners/interactive/display/pipeline_graph.py +++ b/sdks/python/apache_beam/runners/interactive/display/pipeline_graph.py @@ -25,11 +25,7 @@ import collections import logging import threading -from typing import DefaultDict -from typing import Dict from typing import Iterator -from typing import List -from typing import Tuple from typing import Union import apache_beam as beam @@ -93,9 +89,10 @@ def __init__( (beam_runner_api_pb2.Pipeline, beam.Pipeline, type(pipeline))) # A dict from PCollection ID to a list of its consuming Transform IDs - self._consumers: DefaultDict[str, List[str]] = collections.defaultdict(list) + self._consumers: collections.defaultdict[ + str, list[str]] = collections.defaultdict(list) # A dict from PCollection ID to its producing Transform ID - self._producers: Dict[str, str] = {} + self._producers: dict[str, str] = {} for transform_id, transform_proto in self._top_level_transforms(): for pcoll_id in transform_proto.inputs.values(): @@ -132,7 +129,7 @@ def display_graph(self): 'pipeline graph.') def _top_level_transforms( - self) -> Iterator[Tuple[str, beam_runner_api_pb2.PTransform]]: + self) -> Iterator[tuple[str, beam_runner_api_pb2.PTransform]]: """Yields all top level PTransforms (subtransforms of the root PTransform). Yields: (str, PTransform proto) ID, proto pair of top level PTransforms. diff --git a/sdks/python/apache_beam/runners/interactive/display/pipeline_graph_renderer.py b/sdks/python/apache_beam/runners/interactive/display/pipeline_graph_renderer.py index ad46f5d65ea3..0cc0cfb7de92 100644 --- a/sdks/python/apache_beam/runners/interactive/display/pipeline_graph_renderer.py +++ b/sdks/python/apache_beam/runners/interactive/display/pipeline_graph_renderer.py @@ -27,7 +27,6 @@ import subprocess from typing import TYPE_CHECKING from typing import Optional -from typing import Type from apache_beam.utils.plugin import BeamPlugin @@ -95,7 +94,7 @@ def render_pipeline_graph(self, pipeline_graph: 'PipelineGraph') -> str: return pipeline_graph._get_graph().create_svg().decode("utf-8") # pylint: disable=protected-access -def get_renderer(option: Optional[str] = None) -> Type[PipelineGraphRenderer]: +def get_renderer(option: Optional[str] = None) -> type[PipelineGraphRenderer]: """Get an instance of PipelineGraphRenderer given rendering option. Args: diff --git a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py index aebca7b85d65..438a3c4f4e43 100644 --- a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py +++ b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py @@ -14,8 +14,6 @@ import json from dataclasses import dataclass from typing import Any -from typing import Dict -from typing import List from typing import TypedDict import yaml @@ -50,8 +48,8 @@ def __post_init__(self): class FlowGraph(TypedDict): - nodes: List[Dict[str, Any]] - edges: List[Dict[str, Any]] + nodes: list[dict[str, Any]] + edges: list[dict[str, Any]] # ======================== Main Function ======================== @@ -96,8 +94,8 @@ def parse_beam_yaml(yaml_str: str, isDryRunMode: bool = False) -> str: pipeline = parsed_yaml['pipeline'] transforms = pipeline.get('transforms', []) - nodes: List[NodeData] = [] - edges: List[EdgeData] = [] + nodes: list[NodeData] = [] + edges: list[EdgeData] = [] nodes.append(NodeData(id='0', label='Input', type='input')) nodes.append(NodeData(id='1', label='Output', type='output')) @@ -144,7 +142,7 @@ def to_dict(node): def build_success_response( - nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]) -> str: + nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> str: """Build success response""" return json.dumps({'data': {'nodes': nodes, 'edges': edges}, 'error': None}) diff --git a/sdks/python/apache_beam/runners/interactive/options/capture_control.py b/sdks/python/apache_beam/runners/interactive/options/capture_control.py index 826b596bbc6d..fad73e6d15c0 100644 --- a/sdks/python/apache_beam/runners/interactive/options/capture_control.py +++ b/sdks/python/apache_beam/runners/interactive/options/capture_control.py @@ -25,7 +25,6 @@ import logging from datetime import timedelta -from typing import List from apache_beam.io.gcp.pubsub import ReadFromPubSub from apache_beam.runners.interactive import interactive_environment as ie @@ -46,7 +45,7 @@ def __init__(self): self._capture_size_limit = 1e9 self._test_limiters = None - def limiters(self) -> List['capture_limiters.Limiter']: + def limiters(self) -> list['capture_limiters.Limiter']: # noqa: F821 if self._test_limiters: return self._test_limiters @@ -56,7 +55,7 @@ def limiters(self) -> List['capture_limiters.Limiter']: ] def set_limiters_for_test( - self, limiters: List['capture_limiters.Limiter']) -> None: + self, limiters: list['capture_limiters.Limiter']) -> None: # noqa: F821 self._test_limiters = limiters diff --git a/sdks/python/apache_beam/runners/interactive/pipeline_instrument.py b/sdks/python/apache_beam/runners/interactive/pipeline_instrument.py index 07e35f96877c..ceb53e3eb766 100644 --- a/sdks/python/apache_beam/runners/interactive/pipeline_instrument.py +++ b/sdks/python/apache_beam/runners/interactive/pipeline_instrument.py @@ -24,7 +24,6 @@ # pytype: skip-file import logging -from typing import Dict import apache_beam as beam from apache_beam.pipeline import PipelineVisitor @@ -332,7 +331,7 @@ def set_proto_map(proto_map, new_value): return pipeline_to_execute @property - def cacheables(self) -> Dict[str, Cacheable]: + def cacheables(self) -> dict[str, Cacheable]: """Returns the Cacheables by PCollection ids. If you're already working with user defined pipelines and PCollections, @@ -372,7 +371,7 @@ def runner_pcoll_to_user_pcoll(self): pipeline to instances in the user pipeline.""" return self._runner_pcoll_to_user_pcoll - def find_cacheables(self) -> Dict[str, Cacheable]: + def find_cacheables(self) -> dict[str, Cacheable]: """Finds PCollections that need to be cached for analyzed pipeline. There might be multiple pipelines defined and watched, this will only find diff --git a/sdks/python/apache_beam/runners/interactive/sql/beam_sql_magics.py b/sdks/python/apache_beam/runners/interactive/sql/beam_sql_magics.py index 3dc866907a40..ade417e39690 100644 --- a/sdks/python/apache_beam/runners/interactive/sql/beam_sql_magics.py +++ b/sdks/python/apache_beam/runners/interactive/sql/beam_sql_magics.py @@ -25,10 +25,7 @@ import keyword import logging import traceback -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Union from IPython.core.magic import Magics @@ -126,7 +123,7 @@ def __init__(self): 'names and the SQL keywords, such as "SELECT", "FROM", "WHERE" and ' 'etc.')) - def parse(self, args: List[str]) -> Optional[argparse.Namespace]: + def parse(self, args: list[str]) -> Optional[argparse.Namespace]: """Parses a list of string inputs. The parsed namespace contains these attributes: @@ -246,7 +243,7 @@ def beam_sql(self, line: str, cell: Optional[str] = None) -> Optional[PValue]: @progress_indicated -def collect_data_for_local_run(query: str, found: Dict[str, beam.PCollection]): +def collect_data_for_local_run(query: str, found: dict[str, beam.PCollection]): from apache_beam.runners.interactive import interactive_beam as ib for name, pcoll in found.items(): try: @@ -269,8 +266,8 @@ def collect_data_for_local_run(query: str, found: Dict[str, beam.PCollection]): def apply_sql( query: str, output_name: Optional[str], - found: Dict[str, beam.PCollection], - run: bool = True) -> Tuple[str, Union[PValue, SqlNode], SqlChain]: + found: dict[str, beam.PCollection], + run: bool = True) -> tuple[str, Union[PValue, SqlNode], SqlChain]: """Applies a SqlTransform with the given sql and queried PCollections. Args: @@ -312,7 +309,7 @@ def apply_sql( def pcolls_from_streaming_cache( user_pipeline: beam.Pipeline, query_pipeline: beam.Pipeline, - name_to_pcoll: Dict[str, beam.PCollection]) -> Dict[str, beam.PCollection]: + name_to_pcoll: dict[str, beam.PCollection]) -> dict[str, beam.PCollection]: """Reads PCollection cache through the TestStream. Args: @@ -364,7 +361,7 @@ def exception_handler(e): def _generate_output_name( output_name: Optional[str], query: str, - found: Dict[str, beam.PCollection]) -> str: + found: dict[str, beam.PCollection]) -> str: """Generates a unique output name if None is provided. Otherwise, returns the given output name directly. @@ -379,11 +376,11 @@ def _generate_output_name( def _build_query_components( query: str, - found: Dict[str, beam.PCollection], + found: dict[str, beam.PCollection], output_name: str, run: bool = True -) -> Tuple[str, - Union[Dict[str, beam.PCollection], beam.PCollection, beam.Pipeline], +) -> tuple[str, + Union[dict[str, beam.PCollection], beam.PCollection, beam.Pipeline], SqlChain]: """Builds necessary components needed to apply the SqlTransform. diff --git a/sdks/python/apache_beam/runners/interactive/sql/sql_chain.py b/sdks/python/apache_beam/runners/interactive/sql/sql_chain.py index 9cafbb6c9039..30f9f4bdff53 100644 --- a/sdks/python/apache_beam/runners/interactive/sql/sql_chain.py +++ b/sdks/python/apache_beam/runners/interactive/sql/sql_chain.py @@ -26,9 +26,7 @@ import logging from dataclasses import dataclass from typing import Any -from typing import Dict from typing import Optional -from typing import Set from typing import Union import apache_beam as beam @@ -61,10 +59,10 @@ class SqlNode: execution_count: the execution count if in an IPython env. """ output_name: str - source: Union[beam.Pipeline, Set[str]] + source: Union[beam.Pipeline, set[str]] query: str - schemas: Set[Any] = None - evaluated: Set[beam.Pipeline] = None + schemas: set[Any] = None + evaluated: set[beam.Pipeline] = None next: Optional['SqlNode'] = None execution_count: int = 0 @@ -193,7 +191,7 @@ class SqlChain: Otherwise, at least some of the nodes in chain has queried against PCollections. """ - nodes: Dict[str, SqlNode] = None + nodes: dict[str, SqlNode] = None root: Optional[SqlNode] = None current: Optional[SqlNode] = None user_pipeline: Optional[beam.Pipeline] = None diff --git a/sdks/python/apache_beam/runners/interactive/sql/utils.py b/sdks/python/apache_beam/runners/interactive/sql/utils.py index 2e46c0f23a7a..c2c0f4258d83 100644 --- a/sdks/python/apache_beam/runners/interactive/sql/utils.py +++ b/sdks/python/apache_beam/runners/interactive/sql/utils.py @@ -28,10 +28,8 @@ from dataclasses import dataclass from typing import Any from typing import Callable -from typing import Dict from typing import NamedTuple from typing import Optional -from typing import Type from typing import Union import apache_beam as beam @@ -72,8 +70,8 @@ def register_coder_for_schema( def find_pcolls( sql: str, - pcolls: Dict[str, beam.PCollection], - verbose: bool = False) -> Dict[str, beam.PCollection]: + pcolls: dict[str, beam.PCollection], + verbose: bool = False) -> dict[str, beam.PCollection]: """Finds all PCollections used in the given sql query. It does a simple word by word match and calls ib.collect for each PCollection @@ -120,7 +118,7 @@ def pformat_namedtuple(schema: NamedTuple) -> str: ])) -def pformat_dict(raw_input: Dict[str, Any]) -> str: +def pformat_dict(raw_input: dict[str, Any]) -> str: return '{{\n{}\n}}'.format( ',\n'.join(['{}: {}'.format(k, v) for k, v in raw_input.items()])) @@ -146,8 +144,8 @@ class OptionsEntry: """ label: str help: str - cls: Type[PipelineOptions] - arg_builder: Union[str, Dict[str, Optional[Callable]]] + cls: type[PipelineOptions] + arg_builder: Union[str, dict[str, Optional[Callable]]] default: Optional[str] = None def __post_init__(self): diff --git a/sdks/python/apache_beam/runners/interactive/testing/mock_env.py b/sdks/python/apache_beam/runners/interactive/testing/mock_env.py index 9b8f349d785a..d6f7f318a4f0 100644 --- a/sdks/python/apache_beam/runners/interactive/testing/mock_env.py +++ b/sdks/python/apache_beam/runners/interactive/testing/mock_env.py @@ -21,7 +21,6 @@ import unittest import uuid -from typing import Type from unittest.mock import patch from apache_beam.runners.interactive import interactive_environment as ie @@ -30,7 +29,7 @@ from apache_beam.runners.interactive.testing.mock_ipython import mock_get_ipython -def isolated_env(cls: Type[unittest.TestCase]): +def isolated_env(cls: type[unittest.TestCase]): """A class decorator for unittest.TestCase to set up an isolated test environment for Interactive Beam.""" class IsolatedInteractiveEnvironmentTest(cls): diff --git a/sdks/python/apache_beam/runners/interactive/utils.py b/sdks/python/apache_beam/runners/interactive/utils.py index 136fe372c214..399161fd3370 100644 --- a/sdks/python/apache_beam/runners/interactive/utils.py +++ b/sdks/python/apache_beam/runners/interactive/utils.py @@ -25,10 +25,7 @@ import logging from typing import Any from typing import Callable -from typing import Dict from typing import Iterator -from typing import List -from typing import Tuple from typing import Union import pandas as pd @@ -66,7 +63,7 @@ def to_element_list( include_window_info: bool, n: int = None, include_time_events: bool = False, -) -> List[WindowedValue]: +) -> list[WindowedValue]: """Returns an iterator that properly decodes the elements from the reader. """ @@ -107,7 +104,7 @@ def elements(): def elements_to_df( - elements: List[WindowedValue], + elements: list[WindowedValue], include_window_info: bool = False, element_type: Any = None) -> 'DataFrame': # noqa: F821 """Parses the given elements into a Dataframe. @@ -313,7 +310,7 @@ def deferred_df_to_pcollection(df): return to_pcollection(df, yield_elements='pandas', label=str(df._expr)), proxy -def pcoll_by_name() -> Dict[str, beam.PCollection]: +def pcoll_by_name() -> dict[str, beam.PCollection]: """Finds all PCollections by their variable names defined in the notebook.""" from apache_beam.runners.interactive import interactive_environment as ie @@ -340,7 +337,7 @@ def find_pcoll_name(pcoll: beam.PCollection) -> str: return None -def cacheables() -> Dict[CacheKey, Cacheable]: +def cacheables() -> dict[CacheKey, Cacheable]: """Finds all Cacheables with their CacheKeys.""" from apache_beam.runners.interactive import interactive_environment as ie @@ -422,7 +419,7 @@ def visit_transform(self, transform_node): def create_var_in_main(name: str, value: Any, - watch: bool = True) -> Tuple[str, Any]: + watch: bool = True) -> tuple[str, Any]: """Declares a variable in the main module. Args: diff --git a/sdks/python/apache_beam/runners/pipeline_context.py b/sdks/python/apache_beam/runners/pipeline_context.py index f367598f9293..1f06a842fbb7 100644 --- a/sdks/python/apache_beam/runners/pipeline_context.py +++ b/sdks/python/apache_beam/runners/pipeline_context.py @@ -24,13 +24,10 @@ # mypy: disallow-untyped-defs from typing import Any -from typing import Dict -from typing import FrozenSet from typing import Generic from typing import Iterable from typing import Mapping from typing import Optional -from typing import Type from typing import TypeVar from typing import Union @@ -72,14 +69,14 @@ class _PipelineContextMap(Generic[PortableObjectT]): def __init__( self, context: 'PipelineContext', - obj_type: Type[PortableObjectT], + obj_type: type[PortableObjectT], namespace: str, proto_map: Optional[Mapping[str, message.Message]] = None) -> None: self._pipeline_context = context self._obj_type = obj_type self._namespace = namespace - self._obj_to_id: Dict[Any, str] = {} - self._id_to_obj: Dict[str, Any] = {} + self._obj_to_id: dict[Any, str] = {} + self._id_to_obj: dict[str, Any] = {} self._id_to_proto = dict(proto_map) if proto_map else {} def populate_map(self, proto_map: Mapping[str, message.Message]) -> None: @@ -131,7 +128,7 @@ def get_by_proto( obj=obj, obj_type=self._obj_type, label=label), maybe_new_proto) - def get_id_to_proto_map(self) -> Dict[str, message.Message]: + def get_id_to_proto_map(self) -> dict[str, message.Message]: return self._id_to_proto def get_proto_from_id(self, id: str) -> message.Message: @@ -232,7 +229,7 @@ def __init__( def add_requirement(self, requirement: str) -> None: self._requirements.add(requirement) - def requirements(self) -> FrozenSet[str]: + def requirements(self) -> frozenset[str]: return frozenset(self._requirements) # If fake coders are requested, return a pickled version of the element type @@ -288,14 +285,14 @@ def default_environment_id(self) -> str: return self._default_environment_id def get_environment_id_for_resource_hints( - self, hints: Dict[str, bytes]) -> str: + self, hints: dict[str, bytes]) -> str: """Returns an environment id that has necessary resource hints.""" if not hints: return self.default_environment_id() def get_or_create_environment_with_resource_hints( template_env_id: str, - resource_hints: Dict[str, bytes], + resource_hints: dict[str, bytes], ) -> str: """Creates an environment that has necessary hints and returns its id.""" template_env = self.environments.get_proto_from_id(template_env_id) diff --git a/sdks/python/apache_beam/runners/portability/abstract_job_service.py b/sdks/python/apache_beam/runners/portability/abstract_job_service.py index 3a3ad1507813..24aee6f619ab 100644 --- a/sdks/python/apache_beam/runners/portability/abstract_job_service.py +++ b/sdks/python/apache_beam/runners/portability/abstract_job_service.py @@ -26,10 +26,8 @@ import zipfile from concurrent import futures from typing import BinaryIO -from typing import Dict from typing import Iterator from typing import Optional -from typing import Tuple from typing import Union import grpc @@ -47,7 +45,7 @@ _LOGGER = logging.getLogger(__name__) -StateEvent = Tuple[int, Union[timestamp_pb2.Timestamp, Timestamp]] +StateEvent = tuple[int, Union[timestamp_pb2.Timestamp, Timestamp]] def make_state_event(state, timestamp): @@ -70,7 +68,7 @@ class AbstractJobServiceServicer(beam_job_api_pb2_grpc.JobServiceServicer): Servicer for the Beam Job API. """ def __init__(self): - self._jobs: Dict[str, AbstractBeamJob] = {} + self._jobs: dict[str, AbstractBeamJob] = {} def create_beam_job( self, @@ -272,7 +270,7 @@ def __init__(self, jar_path, root): def close(self): self._zipfile_handle.close() - def file_writer(self, path: str) -> Tuple[BinaryIO, str]: + def file_writer(self, path: str) -> tuple[BinaryIO, str]: """Given a relative path, returns an open handle that can be written to and an reference that can later be used to read this file.""" full_path = '%s/%s' % (self._root, path) diff --git a/sdks/python/apache_beam/runners/portability/artifact_service.py b/sdks/python/apache_beam/runners/portability/artifact_service.py index 60b89f3a424a..bbb69ada3e10 100644 --- a/sdks/python/apache_beam/runners/portability/artifact_service.py +++ b/sdks/python/apache_beam/runners/portability/artifact_service.py @@ -32,11 +32,8 @@ from typing import Any from typing import BinaryIO # pylint: disable=unused-import from typing import Callable -from typing import Dict -from typing import List from typing import MutableMapping from typing import Optional -from typing import Tuple from urllib.request import urlopen import grpc @@ -97,12 +94,12 @@ class ArtifactStagingService( beam_artifact_api_pb2_grpc.ArtifactStagingServiceServicer): def __init__( self, - file_writer: Callable[[str, Optional[str]], Tuple[BinaryIO, str]], + file_writer: Callable[[str, Optional[str]], tuple[BinaryIO, str]], ): self._lock = threading.Lock() - self._jobs_to_stage: Dict[ + self._jobs_to_stage: dict[ str, - Tuple[Dict[Any, List[beam_runner_api_pb2.ArtifactInformation]], + tuple[dict[Any, list[beam_runner_api_pb2.ArtifactInformation]], threading.Event]] = {} self._file_writer = file_writer @@ -110,7 +107,7 @@ def register_job( self, staging_token: str, dependency_sets: MutableMapping[ - Any, List[beam_runner_api_pb2.ArtifactInformation]]): + Any, list[beam_runner_api_pb2.ArtifactInformation]]): if staging_token in self._jobs_to_stage: raise ValueError('Already staging %s' % staging_token) with self._lock: diff --git a/sdks/python/apache_beam/runners/portability/expansion_service_test.py b/sdks/python/apache_beam/runners/portability/expansion_service_test.py index 7aa2e5f16e5b..b0b0b2dd2bfa 100644 --- a/sdks/python/apache_beam/runners/portability/expansion_service_test.py +++ b/sdks/python/apache_beam/runners/portability/expansion_service_test.py @@ -159,7 +159,7 @@ def expand(self, pcoll): return pcoll \ | beam.CoGroupByKey() \ | beam.ParDo(self.ConcatFn()).with_output_types( - typing.Tuple[int, typing.Iterable[str]]) + tuple[int, typing.Iterable[str]]) def to_runner_api_parameter(self, unused_context): return TEST_CGBK_URN, None diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/execution.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/execution.py index ec9f5dd15ba8..d2f0f97a8a37 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/execution.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/execution.py @@ -24,23 +24,17 @@ import itertools import logging import struct -import typing import uuid import weakref from typing import TYPE_CHECKING from typing import Any from typing import Callable -from typing import DefaultDict -from typing import Dict from typing import Generic from typing import Iterable from typing import Iterator -from typing import List from typing import MutableMapping from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple from typing import TypeVar from typing import Union @@ -108,7 +102,7 @@ class PartitionableBuffer(Buffer, Protocol): def copy(self) -> 'PartitionableBuffer': pass - def partition(self, n: int) -> List[List[bytes]]: + def partition(self, n: int) -> list[list[bytes]]: pass @property @@ -126,8 +120,8 @@ class ListBuffer: """Used to support parititioning of a list.""" def __init__(self, coder_impl: Optional[CoderImpl]) -> None: self._coder_impl = coder_impl or CoderImpl() - self._inputs: List[bytes] = [] - self._grouped_output: Optional[List[List[bytes]]] = None + self._inputs: list[bytes] = [] + self._grouped_output: Optional[list[list[bytes]]] = None self.cleared = False def copy(self) -> 'ListBuffer': @@ -150,7 +144,7 @@ def append(self, element: bytes) -> None: raise RuntimeError('ListBuffer append after read.') self._inputs.append(element) - def partition(self, n: int) -> List[List[bytes]]: + def partition(self, n: int) -> list[list[bytes]]: if self.cleared: raise RuntimeError('Trying to partition a cleared ListBuffer.') if len(self._inputs) >= n or len(self._inputs) == 0: @@ -198,9 +192,11 @@ def __init__( self._key_coder = pre_grouped_coder.key_coder() self._pre_grouped_coder = pre_grouped_coder self._post_grouped_coder = post_grouped_coder - self._table: DefaultDict[bytes, List[Any]] = collections.defaultdict(list) + self._table: collections.defaultdict[bytes, + list[Any]] = collections.defaultdict( + list) self._windowing = windowing - self._grouped_output: Optional[List[List[bytes]]] = None + self._grouped_output: Optional[list[list[bytes]]] = None def copy(self) -> 'GroupingBuffer': # This is a silly temporary optimization. This class must be removed once @@ -234,7 +230,7 @@ def extend(self, input_buffer: Buffer) -> None: for key, values in input_buffer._table.items(): self._table[key].extend(values) - def partition(self, n: int) -> List[List[bytes]]: + def partition(self, n: int) -> list[list[bytes]]: """ It is used to partition _GroupingBuffer to N parts. Once it is partitioned, it would not be re-partitioned with diff N. Re-partition is not supported now. @@ -311,9 +307,8 @@ def __init__( raise ValueError("Unknown access pattern: '%s'" % access_pattern.urn) self._windowed_value_coder = coder self._window_coder = coder.window_coder - self._values_by_window: DefaultDict[Tuple[str, BoundedWindow], - List[Any]] = collections.defaultdict( - list) + self._values_by_window: collections.defaultdict[ + tuple[str, BoundedWindow], list[Any]] = collections.defaultdict(list) def append(self, elements_data: bytes) -> None: input_stream = create_InputStream(elements_data) @@ -326,7 +321,7 @@ def append(self, elements_data: bytes) -> None: for window in windowed_value.windows: self._values_by_window[key, window].append(value) - def encoded_items(self) -> Iterator[Tuple[bytes, bytes, bytes, int]]: + def encoded_items(self) -> Iterator[tuple[bytes, bytes, bytes, int]]: value_coder_impl = self._value_coder.get_impl() key_coder_impl = self._key_coder.get_impl() for (key, window), values in self._values_by_window.items(): @@ -385,13 +380,13 @@ class _ProcessingQueueManager(object): """ class KeyedQueue(Generic[QUEUE_KEY_TYPE]): def __init__(self) -> None: - self._q: typing.Deque[Tuple[QUEUE_KEY_TYPE, - DataInput]] = collections.deque() + self._q: collections.deque[tuple[QUEUE_KEY_TYPE, + DataInput]] = collections.deque() self._keyed_elements: MutableMapping[QUEUE_KEY_TYPE, - Tuple[QUEUE_KEY_TYPE, + tuple[QUEUE_KEY_TYPE, DataInput]] = {} - def enque(self, elm: Tuple[QUEUE_KEY_TYPE, DataInput]) -> None: + def enque(self, elm: tuple[QUEUE_KEY_TYPE, DataInput]) -> None: key = elm[0] incoming_inputs: DataInput = elm[1] if not incoming_inputs: @@ -415,7 +410,7 @@ def enque(self, elm: Tuple[QUEUE_KEY_TYPE, DataInput]) -> None: self._keyed_elements[key] = elm self._q.appendleft(elm) - def deque(self) -> Tuple[QUEUE_KEY_TYPE, DataInput]: + def deque(self) -> tuple[QUEUE_KEY_TYPE, DataInput]: elm = self._q.pop() key = elm[0] del self._keyed_elements[key] @@ -434,9 +429,9 @@ def __str__(self) -> str: def __init__(self) -> None: # For time-pending and watermark-pending inputs, the key type is # STAGE+TIMESTAMP, while for the ready inputs, the key type is only STAGE. - self.time_pending_inputs = _ProcessingQueueManager.KeyedQueue[Tuple[ + self.time_pending_inputs = _ProcessingQueueManager.KeyedQueue[tuple[ str, Timestamp]]() - self.watermark_pending_inputs = _ProcessingQueueManager.KeyedQueue[Tuple[ + self.watermark_pending_inputs = _ProcessingQueueManager.KeyedQueue[tuple[ str, Timestamp]]() self.ready_inputs = _ProcessingQueueManager.KeyedQueue[str]() @@ -451,7 +446,7 @@ class GenericMergingWindowFn(window.WindowFn): TO_SDK_TRANSFORM = 'read' FROM_SDK_TRANSFORM = 'write' - _HANDLES: Dict[str, 'GenericMergingWindowFn'] = {} + _HANDLES: dict[str, 'GenericMergingWindowFn'] = {} def __init__( self, @@ -664,14 +659,14 @@ class FnApiRunnerExecutionContext(object): """ def __init__( self, - stages: List[translations.Stage], + stages: list[translations.Stage], worker_handler_manager: 'worker_handlers.WorkerHandlerManager', pipeline_components: beam_runner_api_pb2.Components, safe_coders: translations.SafeCoderMapping, - data_channel_coders: Dict[str, str], + data_channel_coders: dict[str, str], num_workers: int, uses_teststream: bool = False, - split_managers: Sequence[Tuple[str, Callable[[int], + split_managers: Sequence[tuple[str, Callable[[int], Iterable[float]]]] = () ) -> None: """ @@ -705,7 +700,7 @@ def __init__( Optional[str]] = {} # Map of buffer_id to its consumers. A consumer is the pair of # Stage name + Ptransform name that consume that buffer. - self.buffer_id_to_consumer_pairs: Dict[bytes, Set[Tuple[str, str]]] = {} + self.buffer_id_to_consumer_pairs: dict[bytes, set[tuple[str, str]]] = {} self._compute_pipeline_dictionaries() self.watermark_manager = WatermarkManager(stages) @@ -721,7 +716,7 @@ def __init__( for id in self.pipeline_components.windowing_strategies.keys() } - self._stage_managers: Dict[str, BundleContextManager] = {} + self._stage_managers: dict[str, BundleContextManager] = {} def bundle_manager_for( self, @@ -834,14 +829,14 @@ def _build_data_side_inputs_map( patterns for all of the outputs of a stage that will be consumed as a side input. """ - transform_consumers: DefaultDict[ + transform_consumers: collections.defaultdict[ str, - List[beam_runner_api_pb2.PTransform]] = collections.defaultdict(list) - stage_consumers: DefaultDict[ - str, List[translations.Stage]] = collections.defaultdict(list) + list[beam_runner_api_pb2.PTransform]] = collections.defaultdict(list) + stage_consumers: collections.defaultdict[ + str, list[translations.Stage]] = collections.defaultdict(list) - def get_all_side_inputs() -> Set[str]: - all_side_inputs: Set[str] = set() + def get_all_side_inputs() -> set[str]: + all_side_inputs: set[str] = set() for stage in stages: for transform in stage.transforms: for input in transform.inputs.values(): @@ -852,7 +847,7 @@ def get_all_side_inputs() -> Set[str]: return all_side_inputs all_side_inputs = frozenset(get_all_side_inputs()) - data_side_inputs_by_producing_stage: Dict[str, DataSideInput] = {} + data_side_inputs_by_producing_stage: dict[str, DataSideInput] = {} producing_stages_by_pcoll = {} @@ -996,7 +991,7 @@ def __init__( execution_context: FnApiRunnerExecutionContext, stage: translations.Stage, num_workers: int, - split_managers: Sequence[Tuple[str, Callable[[int], Iterable[float]]]], + split_managers: Sequence[tuple[str, Callable[[int], Iterable[float]]]], ) -> None: self.execution_context = execution_context self.stage = stage @@ -1007,11 +1002,11 @@ def __init__( # Properties that are lazily initialized self._process_bundle_descriptor: Optional[ beam_fn_api_pb2.ProcessBundleDescriptor] = None - self._worker_handlers: Optional[List[worker_handlers.WorkerHandler]] = None + self._worker_handlers: Optional[list[worker_handlers.WorkerHandler]] = None # a mapping of {(transform_id, timer_family_id): timer_coder_id}. The map # is built after self._process_bundle_descriptor is initialized. # This field can be used to tell whether current bundle has timers. - self._timer_coder_ids: Optional[Dict[Tuple[str, str], str]] = None + self._timer_coder_ids: Optional[dict[tuple[str, str], str]] = None # A mapping from transform_name to Buffer ID self.stage_data_outputs: DataOutput = {} @@ -1033,7 +1028,7 @@ def _compute_expected_outputs(self) -> None: create_buffer_id(timer_family_id, 'timers'), time_domain) @property - def worker_handlers(self) -> List['worker_handlers.WorkerHandler']: + def worker_handlers(self) -> list['worker_handlers.WorkerHandler']: if self._worker_handlers is None: self._worker_handlers = ( self.execution_context.worker_handler_manager.get_worker_handlers( @@ -1088,7 +1083,7 @@ def get_input_coder_impl(self, transform_id: str) -> CoderImpl: assert coder_id return self.get_coder_impl(coder_id) - def _build_timer_coders_id_map(self) -> Dict[Tuple[str, str], str]: + def _build_timer_coders_id_map(self) -> dict[tuple[str, str], str]: assert self._process_bundle_descriptor is not None timer_coder_ids = {} for transform_id, transform_proto in (self._process_bundle_descriptor diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py index fdf291cb6f12..32997441a48e 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py @@ -32,16 +32,11 @@ import threading import time from typing import Callable -from typing import Dict from typing import Iterable from typing import Iterator -from typing import List from typing import Mapping from typing import MutableMapping from typing import Optional -from typing import Set -from typing import Tuple -from typing import Type from typing import TypeVar from typing import Union @@ -133,7 +128,7 @@ def __init__( retrieval_token='unused-retrieval-token')) @staticmethod - def supported_requirements() -> Tuple[str, ...]: + def supported_requirements() -> tuple[str, ...]: return ( common_urns.requirements.REQUIRES_STATEFUL_PROCESSING.urn, common_urns.requirements.REQUIRES_BUNDLE_FINALIZATION.urn, @@ -380,7 +375,7 @@ def _check_requirements( def create_stages( self, pipeline_proto: beam_runner_api_pb2.Pipeline - ) -> Tuple[translations.TransformContext, List[translations.Stage]]: + ) -> tuple[translations.TransformContext, list[translations.Stage]]: return translations.create_and_optimize_stages( copy.deepcopy(pipeline_proto), phases=[ @@ -409,7 +404,7 @@ def create_stages( def run_stages( self, stage_context: translations.TransformContext, - stages: List[translations.Stage]) -> 'RunnerResult': + stages: list[translations.Stage]) -> 'RunnerResult': """Run a list of topologically-sorted stages in batch mode. Args: @@ -607,7 +602,7 @@ def _run_bundle_multiple_times_for_testing( @staticmethod def _collect_written_timers( bundle_context_manager: execution.BundleContextManager - ) -> Tuple[Dict[translations.TimerFamilyId, timestamp.Timestamp], + ) -> tuple[dict[translations.TimerFamilyId, timestamp.Timestamp], OutputTimerData]: """Review output buffers, and collect written timers. @@ -670,7 +665,7 @@ def _add_sdk_delayed_applications_to_deferred_inputs( bundle_context_manager: execution.BundleContextManager, bundle_result: beam_fn_api_pb2.InstructionResponse, deferred_inputs: MutableMapping[str, execution.PartitionableBuffer] - ) -> Set[str]: + ) -> set[str]: """Returns a set of PCollection IDs of PColls having delayed applications. This transform inspects the bundle_context_manager, and bundle_result @@ -697,11 +692,11 @@ def _add_sdk_delayed_applications_to_deferred_inputs( def _add_residuals_and_channel_splits_to_deferred_inputs( self, - splits: List[beam_fn_api_pb2.ProcessBundleSplitResponse], + splits: list[beam_fn_api_pb2.ProcessBundleSplitResponse], bundle_context_manager: execution.BundleContextManager, last_sent: MutableMapping[str, execution.PartitionableBuffer], deferred_inputs: MutableMapping[str, execution.PartitionableBuffer] - ) -> Tuple[Set[str], Set[str]]: + ) -> tuple[set[str], set[str]]: """Returns a two sets representing PCollections with watermark holds. The first set represents PCollections with delayed root applications. @@ -710,7 +705,7 @@ def _add_residuals_and_channel_splits_to_deferred_inputs( pcolls_with_delayed_apps = set() transforms_with_channel_splits = set() - prev_stops: Dict[str, int] = {} + prev_stops: dict[str, int] = {} for split in splits: for delayed_application in split.residual_roots: producer_name = bundle_context_manager.input_for( @@ -927,8 +922,8 @@ def _get_bundle_manager( cache_token_generator = FnApiRunner.get_cache_token_generator(static=False) if bundle_context_manager.num_workers == 1: # Avoid thread/processor pools for increased performance and debugability. - bundle_manager_type: Union[Type[BundleManager], - Type[ParallelBundleManager]] = BundleManager + bundle_manager_type: Union[type[BundleManager], + type[ParallelBundleManager]] = BundleManager elif bundle_context_manager.stage.is_stateful(): # State is keyed, and a single key cannot be processed concurrently. # Alternatively, we could arrange to partition work by key. @@ -946,11 +941,11 @@ def _build_watermark_updates( runner_execution_context: execution.FnApiRunnerExecutionContext, stage_inputs: Iterable[str], expected_timers: Iterable[translations.TimerFamilyId], - pcolls_with_da: Set[str], - transforms_w_splits: Set[str], - watermarks_by_transform_and_timer_family: Dict[translations.TimerFamilyId, + pcolls_with_da: set[str], + transforms_w_splits: set[str], + watermarks_by_transform_and_timer_family: dict[translations.TimerFamilyId, timestamp.Timestamp] - ) -> Dict[Union[str, translations.TimerFamilyId], timestamp.Timestamp]: + ) -> dict[Union[str, translations.TimerFamilyId], timestamp.Timestamp]: """Builds a dictionary of PCollection (or TimerFamilyId) to timestamp. Args: @@ -965,7 +960,7 @@ def _build_watermark_updates( watermarks_by_transform_and_timer_family: represent the set of watermark holds to be added for each timer family. """ - updates: Dict[Union[str, translations.TimerFamilyId], + updates: dict[Union[str, translations.TimerFamilyId], timestamp.Timestamp] = {} def get_pcoll_id(transform_id): @@ -1016,10 +1011,10 @@ def _run_bundle( data_output: DataOutput, expected_timer_output: OutputTimers, bundle_manager: 'BundleManager' - ) -> Tuple[beam_fn_api_pb2.InstructionResponse, - Dict[str, execution.PartitionableBuffer], + ) -> tuple[beam_fn_api_pb2.InstructionResponse, + dict[str, execution.PartitionableBuffer], OutputTimerData, - Dict[Union[str, translations.TimerFamilyId], timestamp.Timestamp]]: + dict[Union[str, translations.TimerFamilyId], timestamp.Timestamp]]: """Execute a bundle, and return a result object, and deferred inputs.""" data_input = bundle_input.data input_timers = bundle_input.timers @@ -1038,7 +1033,7 @@ def _run_bundle( # - timers # - SDK-initiated deferred applications of root elements # - Runner-initiated deferred applications of root elements - deferred_inputs: Dict[str, execution.PartitionableBuffer] = {} + deferred_inputs: dict[str, execution.PartitionableBuffer] = {} watermarks_by_transform_and_timer_family, newly_set_timers = ( self._collect_written_timers(bundle_context_manager)) @@ -1252,8 +1247,8 @@ def _generate_splits_for_testing( self, split_manager, inputs: Mapping[str, execution.PartitionableBuffer], - process_bundle_id) -> List[beam_fn_api_pb2.ProcessBundleSplitResponse]: - split_results: List[beam_fn_api_pb2.ProcessBundleSplitResponse] = [] + process_bundle_id) -> list[beam_fn_api_pb2.ProcessBundleSplitResponse]: + split_results: list[beam_fn_api_pb2.ProcessBundleSplitResponse] = [] read_transform_id, buffer_data = only_element(inputs.items()) byte_stream = b''.join(buffer_data or []) num_elements = len( @@ -1359,7 +1354,7 @@ def process_bundle( cache_tokens=[next(self._cache_token_generator)])) result_future = self._worker_handler.control_conn.push(process_bundle_req) - split_results: List[beam_fn_api_pb2.ProcessBundleSplitResponse] = [] + split_results: list[beam_fn_api_pb2.ProcessBundleSplitResponse] = [] with ProgressRequester(self._worker_handler, process_bundle_id, self._progress_frequency): @@ -1368,8 +1363,8 @@ def process_bundle( split_results = self._generate_splits_for_testing( split_manager, inputs, process_bundle_id) - expect_reads: List[Union[str, - Tuple[str, + expect_reads: list[Union[str, + tuple[str, str]]] = list(expected_outputs.keys()) expect_reads.extend(list(expected_output_timers.keys())) @@ -1433,8 +1428,8 @@ def process_bundle( expected_output_timers: OutputTimers, dry_run: bool = False, ) -> BundleProcessResult: - part_inputs: List[Dict[str, - List[bytes]]] = [{} + part_inputs: list[dict[str, + list[bytes]]] = [{} for _ in range(self._num_workers)] # Timers are only executed on the first worker # TODO(BEAM-9741): Split timers to multiple workers @@ -1446,7 +1441,7 @@ def process_bundle( part_inputs[ix][name] = part merged_result: Optional[beam_fn_api_pb2.InstructionResponse] = None - split_result_list: List[beam_fn_api_pb2.ProcessBundleSplitResponse] = [] + split_result_list: list[beam_fn_api_pb2.ProcessBundleSplitResponse] = [] def execute(part_map_input_timers) -> BundleProcessResult: part_map, input_timers = part_map_input_timers @@ -1581,7 +1576,7 @@ def query(self, filter=None): self.BOUNDED_TRIES: bounded_tries, } - def monitoring_infos(self) -> List[metrics_pb2.MonitoringInfo]: + def monitoring_infos(self) -> list[metrics_pb2.MonitoringInfo]: return [ item for sublist in self._monitoring_infos.values() for item in sublist ] diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py index 8bae82f0aaaf..72a5ed4412ca 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py @@ -31,10 +31,7 @@ import unittest import uuid from typing import Any -from typing import Dict from typing import Iterator -from typing import List -from typing import Tuple from typing import no_type_check import hamcrest # pylint: disable=ungrouped-imports @@ -945,7 +942,7 @@ def is_buffered_correctly(actual): | beam.WindowInto( window.FixedWindows(1) if windowed else window.GlobalWindows()) | beam.Map(lambda x: (key, x)).with_output_types( - Tuple[key_type if key_type else type(key), Any]) + tuple[key_type if key_type else type(key), Any]) | beam.ParDo(BufferDoFn())) assert_that(actual, is_buffered_correctly) @@ -2440,7 +2437,7 @@ def __reduce__(self): return _unpickle_element_counter, (name, ) -_pickled_element_counters: Dict[str, ElementCounter] = {} +_pickled_element_counters: dict[str, ElementCounter] = {} def _unpickle_element_counter(name): @@ -2679,8 +2676,8 @@ def infer_output_type(self, input_type): class ListPlusOneDoFn(beam.DoFn): - def process_batch(self, batch: List[np.int64], *unused_args, - **unused_kwargs) -> Iterator[List[np.int64]]: + def process_batch(self, batch: list[np.int64], *unused_args, + **unused_kwargs) -> Iterator[list[np.int64]]: assert isinstance(batch, list) yield [element + 1 for element in batch] diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/translations.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/translations.py index aadf5cfaa866..7261a815f7e4 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/translations.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/translations.py @@ -33,16 +33,11 @@ from typing import Collection from typing import Container from typing import DefaultDict -from typing import Dict -from typing import FrozenSet from typing import Iterable from typing import Iterator -from typing import List from typing import MutableMapping from typing import NamedTuple from typing import Optional -from typing import Set -from typing import Tuple from typing import TypeVar from typing import Union @@ -59,7 +54,6 @@ from apache_beam.utils import timestamp if TYPE_CHECKING: - from apache_beam.runners.portability.fn_api_runner.execution import ListBuffer from apache_beam.runners.portability.fn_api_runner.execution import PartitionableBuffer T = TypeVar('T') @@ -90,39 +84,39 @@ # TimerFamilyId is identified by transform name + timer family # TODO(pabloem): Rename this type to express this name is unique per pipeline. -TimerFamilyId = Tuple[str, str] +TimerFamilyId = tuple[str, str] BufferId = bytes # SideInputId is identified by a consumer ParDo + tag. -SideInputId = Tuple[str, str] +SideInputId = tuple[str, str] SideInputAccessPattern = beam_runner_api_pb2.FunctionSpec # A map from a PCollection coder ID to a Safe Coder ID # A safe coder is a coder that can be used on the runner-side of the FnApi. # A safe coder receives a byte string, and returns a type that can be # understood by the runner when deserializing. -SafeCoderMapping = Dict[str, str] +SafeCoderMapping = dict[str, str] # DataSideInput maps SideInputIds to a tuple of the encoded bytes of the side # input content, and a payload specification regarding the type of side input # (MultiMap / Iterable). -DataSideInput = Dict[SideInputId, Tuple[bytes, SideInputAccessPattern]] +DataSideInput = dict[SideInputId, tuple[bytes, SideInputAccessPattern]] -DataOutput = Dict[str, BufferId] +DataOutput = dict[str, BufferId] # A map of [Transform ID, Timer Family ID] to [Buffer ID, Time Domain for timer] # The time domain comes from beam_runner_api_pb2.TimeDomain. It may be # EVENT_TIME or PROCESSING_TIME. -OutputTimers = MutableMapping[TimerFamilyId, Tuple[BufferId, Any]] +OutputTimers = MutableMapping[TimerFamilyId, tuple[BufferId, Any]] # A map of [Transform ID, Timer Family ID] to [Buffer CONTENTS, Timestamp] OutputTimerData = MutableMapping[TimerFamilyId, - Tuple['PartitionableBuffer', + tuple['PartitionableBuffer', timestamp.Timestamp]] -BundleProcessResult = Tuple[beam_fn_api_pb2.InstructionResponse, - List[beam_fn_api_pb2.ProcessBundleSplitResponse]] +BundleProcessResult = tuple[beam_fn_api_pb2.InstructionResponse, + list[beam_fn_api_pb2.ProcessBundleSplitResponse]] # TODO(pabloem): Change tha name to a more representative one @@ -902,7 +896,7 @@ def _group_stages_by_key(stages, get_stage_key): def _group_stages_with_limit(stages, get_limit): # type: (Iterable[Stage], Callable[[str], int]) -> Iterable[Collection[Stage]] stages_with_limit = [(stage, get_limit(stage.name)) for stage in stages] - group: List[Stage] = [] + group: list[Stage] = [] group_limit = 0 for stage, limit in sorted(stages_with_limit, key=operator.itemgetter(1)): if limit < 1: diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/trigger_manager.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/trigger_manager.py index 021f5950d71d..0bdcf2ac8e50 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/trigger_manager.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/trigger_manager.py @@ -99,7 +99,7 @@ def __init__( super().__init__(all_windows) self.trigger_context = context self.windowing = windowing - self.merged_away: typing.Dict[BoundedWindow, BoundedWindow] = {} + self.merged_away: dict[BoundedWindow, BoundedWindow] = {} def merge(self, to_be_merged, merge_result): _LOGGER.debug("Merging %s into %s", to_be_merged, merge_result) @@ -117,9 +117,9 @@ def merge(self, to_be_merged, merge_result): @typehints.with_input_types( - typing.Tuple[K, typing.Iterable[windowed_value.WindowedValue]]) + tuple[K, typing.Iterable[windowed_value.WindowedValue]]) @typehints.with_output_types( - typing.Tuple[K, typing.Iterable[windowed_value.WindowedValue]]) + tuple[K, typing.Iterable[windowed_value.WindowedValue]]) class GeneralTriggerManagerDoFn(DoFn): """A trigger manager that supports all windowing / triggering cases. @@ -153,7 +153,7 @@ def __init__(self, windowing: Windowing): def process( self, - element: typing.Tuple[K, typing.Iterable[windowed_value.WindowedValue]], + element: tuple[K, typing.Iterable[windowed_value.WindowedValue]], all_elements: BagRuntimeState = DoFn.StateParam(WINDOW_ELEMENT_PAIRS), # type: ignore latest_processing_time: AccumulatingRuntimeState = DoFn.StateParam(LAST_KNOWN_TIME), # type: ignore latest_watermark: AccumulatingRuntimeState = DoFn.StateParam( # type: ignore @@ -226,7 +226,7 @@ def _fire_eligible_windows( timestamp: Timestamp, timer_tag: typing.Optional[str], context: 'FnRunnerStatefulTriggerContext', - windows_of_interest: typing.Optional[typing.Set[BoundedWindow]] = None): + windows_of_interest: typing.Optional[set[BoundedWindow]] = None): windows_to_elements = context.windows_to_elements_map() context.all_elements_state.clear() @@ -254,7 +254,7 @@ def _fire_eligible_windows( elems = [WindowedValue(e.value, e.timestamp, (w, )) for e in elems] yield (key, elems) - finished_windows: typing.Set[BoundedWindow] = set( + finished_windows: set[BoundedWindow] = set( context.finished_windows_state.read()) # Add elements that were not fired back into state. for w, elems in windows_to_elements.items(): @@ -341,13 +341,11 @@ def __init__( self.finished_windows_state = finished_windows_state def windows_to_elements_map( - self - ) -> typing.Dict[BoundedWindow, typing.List[windowed_value.WindowedValue]]: - window_element_pairs: typing.Iterable[typing.Tuple[ - BoundedWindow, - windowed_value.WindowedValue]] = self.all_elements_state.read() - result: typing.Dict[BoundedWindow, - typing.List[windowed_value.WindowedValue]] = {} + self) -> dict[BoundedWindow, list[windowed_value.WindowedValue]]: + window_element_pairs: typing.Iterable[ + tuple[BoundedWindow, + windowed_value.WindowedValue]] = self.all_elements_state.read() + result: dict[BoundedWindow, list[windowed_value.WindowedValue]] = {} for w, e in window_element_pairs: if w not in result: result[w] = [] diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/visualization_tools.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/visualization_tools.py index 28efa0788b05..85aa11af4480 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/visualization_tools.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/visualization_tools.py @@ -16,8 +16,6 @@ # """Set of utilities to visualize a pipeline to be executed by FnApiRunner.""" -from typing import Set -from typing import Tuple from apache_beam.runners.portability.fn_api_runner.translations import Stage from apache_beam.runners.portability.fn_api_runner.watermark_manager import WatermarkManager @@ -89,8 +87,8 @@ def add_links(link_from=None, link_to=None, edge_style="solid"): g.edge(link_from, link_to, style=edge_style) seen_links.add((link_to, link_from, edge_style)) - seen_nodes: Set[str] = set() - seen_links: Set[Tuple[str, str]] = set() + seen_nodes: set[str] = set() + seen_links: set[tuple[str, str]] = set() for node in watermark_manager._stages_by_name.values(): name = 'STAGE_%s...%s' % (node.name[:30], node.name[-30:]) add_node(name, 'box') diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/watermark_manager.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/watermark_manager.py index 106eca108297..a61abb41e3e5 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/watermark_manager.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/watermark_manager.py @@ -19,9 +19,6 @@ from __future__ import absolute_import -from typing import Dict -from typing import List -from typing import Set from typing import Union from apache_beam.portability.api import beam_runner_api_pb2 @@ -40,7 +37,7 @@ class PCollectionNode(object): def __init__(self, name): self.name = name self._watermark = timestamp.MIN_TIMESTAMP - self.producers: Set[WatermarkManager.StageNode] = set() + self.producers: set[WatermarkManager.StageNode] = set() self.consumers = 0 self._fully_consumed_by = 0 self._produced_watermark = timestamp.MIN_TIMESTAMP @@ -77,9 +74,9 @@ def __init__(self, name): # the output watermark of the stage, because only the main input # can actually advance that watermark. self.name = name - self.inputs: Set[WatermarkManager.PCollectionNode] = set() - self.side_inputs: Set[WatermarkManager.PCollectionNode] = set() - self.outputs: Set[WatermarkManager.PCollectionNode] = set() + self.inputs: set[WatermarkManager.PCollectionNode] = set() + self.side_inputs: set[WatermarkManager.PCollectionNode] = set() + self.outputs: set[WatermarkManager.PCollectionNode] = set() def __str__(self): return 'StageNode None: - self._pcollections_by_name: Dict[Union[str, translations.TimerFamilyId], + def __init__(self, stages: list[translations.Stage]) -> None: + self._pcollections_by_name: dict[Union[str, translations.TimerFamilyId], WatermarkManager.PCollectionNode] = {} - self._stages_by_name: Dict[str, WatermarkManager.StageNode] = {} + self._stages_by_name: dict[str, WatermarkManager.StageNode] = {} def add_pcollection( pcname: str, @@ -179,7 +176,7 @@ def add_pcollection( self._verify(stages) - def _verify(self, stages: List[translations.Stage]): + def _verify(self, stages: list[translations.Stage]): for s in stages: if len(self._stages_by_name[s.name].inputs) == 0: from apache_beam.runners.portability.fn_api_runner import visualization_tools diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/worker_handlers.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/worker_handlers.py index d79b381f2d78..6214aaa31006 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/worker_handlers.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/worker_handlers.py @@ -34,14 +34,10 @@ from typing import BinaryIO # pylint: disable=unused-import from typing import Callable from typing import DefaultDict -from typing import Dict from typing import Iterable from typing import Iterator -from typing import List from typing import Mapping from typing import Optional -from typing import Tuple -from typing import Type from typing import TypeVar from typing import Union from typing import cast @@ -1034,7 +1030,7 @@ def append(self, item): def extend(self, other: Buffer) -> None: raise NotImplementedError() - StateType = Union[CopyOnWriteState, DefaultDict[bytes, Buffer]] + StateType = Union[CopyOnWriteState, collections.defaultdict[bytes, Buffer]] def __init__(self): # type: () -> None diff --git a/sdks/python/apache_beam/runners/portability/local_job_service.py b/sdks/python/apache_beam/runners/portability/local_job_service.py index 9d85e4d1e664..daa12f92202f 100644 --- a/sdks/python/apache_beam/runners/portability/local_job_service.py +++ b/sdks/python/apache_beam/runners/portability/local_job_service.py @@ -28,7 +28,6 @@ import time import traceback from typing import Any -from typing import List from typing import Mapping from typing import Optional @@ -248,7 +247,7 @@ def __init__( self._provision_info = provision_info self._artifact_staging_endpoint = artifact_staging_endpoint self._artifact_service = artifact_service - self._state_queues: List[queue.Queue] = [] + self._state_queues: list[queue.Queue] = [] self._log_queues = JobLogQueues() self.daemon = True self.result = None @@ -374,7 +373,7 @@ def Logging(self, log_bundles, context=None): class JobLogQueues(object): def __init__(self): - self._queues: List[queue.Queue] = [] + self._queues: list[queue.Queue] = [] self._cache = [] self._cache_size = 10 self._lock = threading.Lock() @@ -455,7 +454,7 @@ def emit(self, record): def _extract_dependency_sets( envs: Mapping[str, beam_runner_api_pb2.Environment] -) -> Mapping[Any, List[beam_runner_api_pb2.ArtifactInformation]]: +) -> Mapping[Any, list[beam_runner_api_pb2.ArtifactInformation]]: """Expands the set of environments into a mapping of (opaque) keys to dependency sets. This is not 1:1 in the case of AnyOf environments. @@ -472,7 +471,7 @@ def dependencies_iter(): def _update_dependency_sets( envs: Mapping[str, beam_runner_api_pb2.Environment], - resolved_deps: Mapping[Any, List[beam_runner_api_pb2.ArtifactInformation]]): + resolved_deps: Mapping[Any, list[beam_runner_api_pb2.ArtifactInformation]]): """Takes the mapping of beam Environments (originally passed to `_extract_dependency_sets`) and a set of (key-wise) updated dependencies, and updates the original environment protos to contain the updated diff --git a/sdks/python/apache_beam/runners/portability/portable_runner.py b/sdks/python/apache_beam/runners/portability/portable_runner.py index 45d727371263..22c3440b69ed 100644 --- a/sdks/python/apache_beam/runners/portability/portable_runner.py +++ b/sdks/python/apache_beam/runners/portability/portable_runner.py @@ -26,10 +26,8 @@ import threading import time from typing import Any -from typing import Dict from typing import Iterator from typing import Optional -from typing import Tuple import grpc from google.protobuf import struct_pb2 @@ -96,7 +94,7 @@ def __init__(self, job_service, options, retain_unknown_options=False): def submit( self, proto_pipeline: beam_runner_api_pb2.Pipeline - ) -> Tuple[str, + ) -> tuple[str, Iterator[beam_job_api_pb2.JobStateEvent], Iterator[beam_job_api_pb2.JobMessagesResponse]]: """ @@ -171,7 +169,7 @@ def add_runner_options(parser): @staticmethod def encode_pipeline_options( - all_options: Dict[str, Any]) -> 'struct_pb2.Struct': + all_options: dict[str, Any]) -> 'struct_pb2.Struct': def convert_pipeline_option_value(v): # convert int values: BEAM-5509 if type(v) == int: @@ -215,7 +213,7 @@ def stage( def run( self, preparation_id: str - ) -> Tuple[str, + ) -> tuple[str, Iterator[beam_job_api_pb2.JobStateEvent], Iterator[beam_job_api_pb2.JobMessagesResponse]]: """Run the job""" diff --git a/sdks/python/apache_beam/runners/portability/prism_runner.py b/sdks/python/apache_beam/runners/portability/prism_runner.py index d2164cfecd10..0458898b5e96 100644 --- a/sdks/python/apache_beam/runners/portability/prism_runner.py +++ b/sdks/python/apache_beam/runners/portability/prism_runner.py @@ -480,14 +480,13 @@ def _get_executable_path(self) -> str: return self._prepare_executable(local_path, self.BIN_CACHE, ignore_cache) - def subprocess_cmd_and_endpoint( - self) -> typing.Tuple[typing.List[typing.Any], str]: + def subprocess_cmd_and_endpoint(self) -> tuple[list[typing.Any], str]: bin_path = self._get_executable_path() job_port, = subprocess_server.pick_port(self._job_port) subprocess_cmd = [bin_path] + self.prism_arguments(job_port) return (subprocess_cmd, f"localhost:{job_port}") - def prism_arguments(self, job_port) -> typing.List[typing.Any]: + def prism_arguments(self, job_port) -> list[typing.Any]: return [ '--job_port', job_port, diff --git a/sdks/python/apache_beam/runners/portability/sdk_container_builder.py b/sdks/python/apache_beam/runners/portability/sdk_container_builder.py index b958995373d7..1e68fc305aaf 100644 --- a/sdks/python/apache_beam/runners/portability/sdk_container_builder.py +++ b/sdks/python/apache_beam/runners/portability/sdk_container_builder.py @@ -35,7 +35,6 @@ import tempfile import time import uuid -from typing import Type from google.protobuf.json_format import MessageToJson @@ -139,7 +138,7 @@ def build_container_image(cls, pipeline_options: PipelineOptions) -> str: return builder._build() @classmethod - def _get_subclass_by_key(cls, key: str) -> Type['SdkContainerImageBuilder']: + def _get_subclass_by_key(cls, key: str) -> type['SdkContainerImageBuilder']: available_builders = [ subclass for subclass in cls.get_all_subclasses() if subclass._builder_key() == key diff --git a/sdks/python/apache_beam/runners/portability/stager.py b/sdks/python/apache_beam/runners/portability/stager.py index 668477ce1461..e862fde4efef 100644 --- a/sdks/python/apache_beam/runners/portability/stager.py +++ b/sdks/python/apache_beam/runners/portability/stager.py @@ -57,9 +57,7 @@ import tempfile from importlib.metadata import distribution from typing import Callable -from typing import List from typing import Optional -from typing import Tuple from urllib.parse import urlparse from packaging import version @@ -138,7 +136,7 @@ def _create_file_pip_requirements_artifact(local_path): @staticmethod def extract_staging_tuple_iter( - artifacts: List[beam_runner_api_pb2.ArtifactInformation]): + artifacts: list[beam_runner_api_pb2.ArtifactInformation]): for artifact in artifacts: if artifact.type_urn == common_urns.artifact_types.FILE.urn: file_payload = beam_runner_api_pb2.ArtifactFilePayload() @@ -162,8 +160,8 @@ def extract_staging_tuple_iter( def create_job_resources( options: PipelineOptions, temp_dir: str, - build_setup_args: Optional[List[str]] = None, - pypi_requirements: Optional[List[str]] = None, + build_setup_args: Optional[list[str]] = None, + pypi_requirements: Optional[list[str]] = None, populate_requirements_cache: Optional[Callable[[str, str, bool], None]] = None, skip_prestaged_dependencies: Optional[bool] = False, @@ -200,7 +198,7 @@ def create_job_resources( while trying to create the resources (e.g., build a setup package). """ - resources: List[beam_runner_api_pb2.ArtifactInformation] = [] + resources: list[beam_runner_api_pb2.ArtifactInformation] = [] setup_options = options.view_as(SetupOptions) use_beam_default_container = options.view_as( @@ -403,7 +401,7 @@ def create_job_resources( def stage_job_resources( self, - resources: List[Tuple[str, str, str]], + resources: list[tuple[str, str, str]], staging_location: Optional[str] = None): """For internal use only; no backwards-compatibility guarantees. @@ -437,9 +435,9 @@ def stage_job_resources( def create_and_stage_job_resources( self, options: PipelineOptions, - build_setup_args: Optional[List[str]] = None, + build_setup_args: Optional[list[str]] = None, temp_dir: Optional[str] = None, - pypi_requirements: Optional[List[str]] = None, + pypi_requirements: Optional[list[str]] = None, populate_requirements_cache: Optional[Callable[[str, str, bool], None]] = None, staging_location: Optional[str] = None): @@ -544,7 +542,7 @@ def _is_remote_path(path): @staticmethod def _create_jar_packages( - jar_packages, temp_dir) -> List[beam_runner_api_pb2.ArtifactInformation]: + jar_packages, temp_dir) -> list[beam_runner_api_pb2.ArtifactInformation]: """Creates a list of local jar packages for Java SDK Harness. :param jar_packages: Ordered list of local paths to jar packages to be @@ -557,9 +555,9 @@ def _create_jar_packages( RuntimeError: If files specified are not found or do not have expected name patterns. """ - resources: List[beam_runner_api_pb2.ArtifactInformation] = [] + resources: list[beam_runner_api_pb2.ArtifactInformation] = [] staging_temp_dir = tempfile.mkdtemp(dir=temp_dir) - local_packages: List[str] = [] + local_packages: list[str] = [] for package in jar_packages: if not os.path.basename(package).endswith('.jar'): raise RuntimeError( @@ -595,7 +593,7 @@ def _create_jar_packages( @staticmethod def _create_extra_packages( extra_packages, - temp_dir) -> List[beam_runner_api_pb2.ArtifactInformation]: + temp_dir) -> list[beam_runner_api_pb2.ArtifactInformation]: """Creates a list of local extra packages. Args: @@ -614,9 +612,9 @@ def _create_extra_packages( RuntimeError: If files specified are not found or do not have expected name patterns. """ - resources: List[beam_runner_api_pb2.ArtifactInformation] = [] + resources: list[beam_runner_api_pb2.ArtifactInformation] = [] staging_temp_dir = tempfile.mkdtemp(dir=temp_dir) - local_packages: List[str] = [] + local_packages: list[str] = [] for package in extra_packages: if not (os.path.basename(package).endswith('.tar') or os.path.basename(package).endswith('.tar.gz') or @@ -815,7 +813,7 @@ def _populate_requirements_cache( def _build_setup_package( setup_file: str, temp_dir: str, - build_setup_args: Optional[List[str]] = None) -> str: + build_setup_args: Optional[list[str]] = None) -> str: saved_current_directory = os.getcwd() try: @@ -882,7 +880,7 @@ def _desired_sdk_filename_in_staging_location(sdk_location) -> str: @staticmethod def _create_beam_sdk( sdk_remote_location, - temp_dir) -> List[beam_runner_api_pb2.ArtifactInformation]: + temp_dir) -> list[beam_runner_api_pb2.ArtifactInformation]: """Creates a Beam SDK file with the appropriate version. Args: diff --git a/sdks/python/apache_beam/runners/portability/stager_test.py b/sdks/python/apache_beam/runners/portability/stager_test.py index 4ec1c697fbff..3d625fb287ae 100644 --- a/sdks/python/apache_beam/runners/portability/stager_test.py +++ b/sdks/python/apache_beam/runners/portability/stager_test.py @@ -26,7 +26,6 @@ import sys import tempfile import unittest -from typing import List import mock import pytest @@ -80,7 +79,7 @@ def create_temp_file(self, path, contents): def is_remote_path(self, path): return path.startswith('/tmp/remote/') - remote_copied_files: List[str] = [] + remote_copied_files: list[str] = [] def file_copy(self, from_path, to_path): if self.is_remote_path(from_path): diff --git a/sdks/python/apache_beam/runners/sdf_utils.py b/sdks/python/apache_beam/runners/sdf_utils.py index 01573656b6ac..ad1a4fd74038 100644 --- a/sdks/python/apache_beam/runners/sdf_utils.py +++ b/sdks/python/apache_beam/runners/sdf_utils.py @@ -25,7 +25,6 @@ from typing import Any from typing import NamedTuple from typing import Optional -from typing import Tuple from typing import Union from apache_beam.transforms.core import WatermarkEstimatorProvider @@ -117,7 +116,7 @@ def try_split(self, fraction_of_remainder): with self._lock: return self._restriction_tracker.try_split(fraction_of_remainder) - def deferred_status(self) -> Optional[Tuple[Any, Duration]]: + def deferred_status(self) -> Optional[tuple[Any, Duration]]: """Returns deferred work which is produced by ``defer_remainder()``. When there is a self-checkpoint performed, the system needs to fulfill the diff --git a/sdks/python/apache_beam/runners/trivial_runner.py b/sdks/python/apache_beam/runners/trivial_runner.py index 6517a3adc373..6f0402abf55c 100644 --- a/sdks/python/apache_beam/runners/trivial_runner.py +++ b/sdks/python/apache_beam/runners/trivial_runner.py @@ -20,7 +20,6 @@ from typing import Any from typing import Iterable from typing import Iterator -from typing import List from typing import TypeVar from apache_beam import coders @@ -347,10 +346,10 @@ def register_process_bundle_descriptor( self._process_bundle_descriptors[ process_bundle_descriptor.id] = process_bundle_descriptor - def get_pcollection_contents(self, pcoll_id: str) -> List[bytes]: + def get_pcollection_contents(self, pcoll_id: str) -> list[bytes]: return self._pcollections_to_encoded_chunks[pcoll_id] - def set_pcollection_contents(self, pcoll_id: str, chunks: List[bytes]): + def set_pcollection_contents(self, pcoll_id: str, chunks: list[bytes]): self._pcollections_to_encoded_chunks[pcoll_id] = chunks def new_id(self, prefix='') -> str: diff --git a/sdks/python/apache_beam/runners/worker/bundle_processor.py b/sdks/python/apache_beam/runners/worker/bundle_processor.py index faa756d7c5c5..1a852aa19d98 100644 --- a/sdks/python/apache_beam/runners/worker/bundle_processor.py +++ b/sdks/python/apache_beam/runners/worker/bundle_processor.py @@ -39,18 +39,11 @@ from typing import Any from typing import Callable from typing import Container -from typing import DefaultDict -from typing import Dict -from typing import FrozenSet from typing import Iterable from typing import Iterator -from typing import List from typing import Mapping from typing import MutableMapping from typing import Optional -from typing import Set -from typing import Tuple -from typing import Type from typing import TypeVar from typing import Union from typing import cast @@ -108,7 +101,7 @@ Any, beam_runner_api_pb2.PTransform, Union['message.Message', bytes], - Dict[str, List[operations.Operation]] + dict[str, list[operations.Operation]] ], operations.Operation] OperationT = TypeVar('OperationT', bound=operations.Operation) @@ -179,7 +172,7 @@ def __init__( self, operation_name: common.NameContext, step_name, - consumers: Mapping[Any, List[operations.Operation]], + consumers: Mapping[Any, list[operations.Operation]], counter_factory: counters.CounterFactory, state_sampler: statesampler.StateSampler, windowed_coder: coders.Coder, @@ -242,8 +235,8 @@ def process_encoded(self, encoded_windowed_values: bytes) -> None: self.output(decoded_value) def monitoring_infos( - self, transform_id: str, tag_to_pcollection_id: Dict[str, str] - ) -> Dict[FrozenSet, metrics_pb2.MonitoringInfo]: + self, transform_id: str, tag_to_pcollection_id: dict[str, str] + ) -> dict[frozenset, metrics_pb2.MonitoringInfo]: all_monitoring_infos = super().monitoring_infos( transform_id, tag_to_pcollection_id) read_progress_info = monitoring_infos.int64_counter( @@ -259,7 +252,7 @@ def monitoring_infos( def try_split( # type: ignore[override] self, fraction_of_remainder, total_buffer_size, allowed_split_points ) -> Optional[ - Tuple[ + tuple[ int, Iterable[operations.SdfSplitResultsPrimary], Iterable[operations.SdfSplitResultsResidual], @@ -317,7 +310,7 @@ def is_valid_split_point(index): # try splitting at the current element. if (keep_of_element_remainder < 1 and is_valid_split_point(index) and is_valid_split_point(index + 1)): - split: Optional[Tuple[ + split: Optional[tuple[ Iterable[operations.SdfSplitResultsPrimary], Iterable[operations.SdfSplitResultsResidual]]] = try_split( keep_of_element_remainder) @@ -408,7 +401,7 @@ def __init__( self._element_coder = coder.wrapped_value_coder self._target_window_coder = coder.window_coder # TODO(robertwb): Limit the cache size. - self._cache: Dict[BoundedWindow, Any] = {} + self._cache: dict[BoundedWindow, Any] = {} self._use_bulk_read = use_bulk_read def __getitem__(self, window): @@ -609,7 +602,7 @@ def __init__( self._state_key = state_key self._value_coder = value_coder self._cleared = False - self._added_elements: List[Any] = [] + self._added_elements: list[Any] = [] def read(self) -> Iterable[Any]: return _ConcatIterable([] if self._cleared else cast( @@ -647,7 +640,7 @@ def __init__( self._state_key = state_key self._value_coder = value_coder self._cleared = False - self._added_elements: Set[Any] = set() + self._added_elements: set[Any] = set() def _compact_data(self, rewrite=True): accumulator = set( @@ -667,7 +660,7 @@ def _compact_data(self, rewrite=True): return accumulator - def read(self) -> Set[Any]: + def read(self) -> set[Any]: return self._compact_data(rewrite=False) def add(self, value: Any) -> None: @@ -737,7 +730,7 @@ def __len__(self) -> int: assert len(self._sorted_starts) == len(self._sorted_ends) return len(self._sorted_starts) - def __iter__(self) -> Iterator[Tuple[int, int]]: + def __iter__(self) -> Iterator[tuple[int, int]]: return zip(self._sorted_starts, self._sorted_ends) def __str__(self) -> str: @@ -763,7 +756,7 @@ def __init__( self._pending_adds = SortedDict() self._pending_removes = RangeSet() - def add(self, elem: Tuple[timestamp.Timestamp, Any]) -> None: + def add(self, elem: tuple[timestamp.Timestamp, Any]) -> None: assert len(elem) == 2 key_ts, value = elem key = key_ts.micros @@ -772,14 +765,14 @@ def add(self, elem: Tuple[timestamp.Timestamp, Any]) -> None: raise ValueError("key value %d is out of range" % key) self._pending_adds.setdefault(key, []).append(value) - def read(self) -> Iterable[Tuple[timestamp.Timestamp, Any]]: + def read(self) -> Iterable[tuple[timestamp.Timestamp, Any]]: return self.read_range(self.TIMESTAMP_RANGE_MIN, self.TIMESTAMP_RANGE_MAX) def read_range( self, min_timestamp: timestamp.Timestamp, limit_timestamp: timestamp.Timestamp - ) -> Iterable[Tuple[timestamp.Timestamp, Any]]: + ) -> Iterable[tuple[timestamp.Timestamp, Any]]: # convert timestamp to int, as sort keys are stored as int internally. min_key = min_timestamp.micros limit_key = limit_timestamp.micros @@ -946,8 +939,8 @@ def __init__( self._key_coder = key_coder self._window_coder = window_coder # A mapping of {timer_family_id: TimerInfo} - self._timers_info: Dict[str, TimerInfo] = {} - self._all_states: Dict[tuple, FnApiUserRuntimeStateTypes] = {} + self._timers_info: dict[str, TimerInfo] = {} + self._all_states: dict[tuple, FnApiUserRuntimeStateTypes] = {} def add_timer_info(self, timer_family_id: str, timer_info: TimerInfo) -> None: self._timers_info[timer_family_id] = timer_info @@ -1086,7 +1079,7 @@ class BundleProcessor(object): """ A class for processing bundles of elements. """ def __init__( self, - runner_capabilities: FrozenSet[str], + runner_capabilities: frozenset[str], process_bundle_descriptor: beam_fn_api_pb2.ProcessBundleDescriptor, state_handler: sdk_worker.CachingStateHandler, data_channel_factory: data_plane.DataChannelFactory, @@ -1126,7 +1119,7 @@ def __init__( # {(transform_id, timer_family_id): TimerInfo} # The mapping is empty when there is no timer_family_specs in the # ProcessBundleDescriptor. - self.timers_info: Dict[Tuple[str, str], TimerInfo] = {} + self.timers_info: dict[tuple[str, str], TimerInfo] = {} # TODO(robertwb): Figure out the correct prefix to use for output counters # from StateSampler. @@ -1184,7 +1177,8 @@ def is_side_input(transform_proto, tag): transform_proto.spec.payload, beam_runner_api_pb2.ParDoPayload).side_inputs - pcoll_consumers: DefaultDict[str, List[str]] = collections.defaultdict(list) + pcoll_consumers: collections.defaultdict[ + str, list[str]] = collections.defaultdict(list) for transform_id, transform_proto in descriptor.transforms.items(): for tag, pcoll_id in transform_proto.inputs.items(): if not is_side_input(transform_proto, tag): @@ -1230,9 +1224,9 @@ def reset(self) -> None: def process_bundle( self, instruction_id: str - ) -> Tuple[List[beam_fn_api_pb2.DelayedBundleApplication], bool]: + ) -> tuple[list[beam_fn_api_pb2.DelayedBundleApplication], bool]: - expected_input_ops: List[DataInputOperation] = [] + expected_input_ops: list[DataInputOperation] = [] for op in self.ops.values(): if isinstance(op, DataOutputOperation): @@ -1258,10 +1252,9 @@ def process_bundle( # both data input and timer input. The data input is identied by # transform_id. The data input is identified by # (transform_id, timer_family_id). - data_channels: DefaultDict[data_plane.DataChannel, - List[Union[str, Tuple[ - str, - str]]]] = collections.defaultdict(list) + data_channels: collections.defaultdict[ + data_plane.DataChannel, + list[Union[str, tuple[str, str]]]] = collections.defaultdict(list) # Add expected data inputs for each data channel. input_op_by_transform_id = {} @@ -1410,7 +1403,7 @@ def construct_bundle_application( if output_watermark: proto_output_watermark = proto_utils.from_micros( timestamp_pb2.Timestamp, output_watermark.micros) - output_watermarks: Optional[Dict[str, timestamp_pb2.Timestamp]] = { + output_watermarks: Optional[dict[str, timestamp_pb2.Timestamp]] = { output: proto_output_watermark for output in outputs } @@ -1422,7 +1415,7 @@ def construct_bundle_application( output_watermarks=output_watermarks, element=main_input_coder.get_impl().encode_nested(element)) - def monitoring_infos(self) -> List[metrics_pb2.MonitoringInfo]: + def monitoring_infos(self) -> list[metrics_pb2.MonitoringInfo]: """Returns the list of MonitoringInfos collected processing this bundle.""" # Construct a new dict first to remove duplicates. all_monitoring_infos_dict = {} @@ -1442,7 +1435,7 @@ def shutdown(self) -> None: @dataclass class ExecutionContext: # Any splits to be processed later. - delayed_applications: List[Tuple[operations.DoOperation, + delayed_applications: list[tuple[operations.DoOperation, common.SplitResultResidual]] = field( default_factory=list) @@ -1457,7 +1450,7 @@ class BeamTransformFactory(object): """Factory for turning transform_protos into executable operations.""" def __init__( self, - runner_capabilities: FrozenSet[str], + runner_capabilities: frozenset[str], descriptor: beam_fn_api_pb2.ProcessBundleDescriptor, data_channel_factory: data_plane.DataChannelFactory, counter_factory: counters.CounterFactory, @@ -1480,21 +1473,21 @@ def __init__( element_coder_impl)) self.data_sampler = data_sampler - _known_urns: Dict[str, - Tuple[ConstructorFn, - Union[Type[message.Message], Type[bytes], + _known_urns: dict[str, + tuple[ConstructorFn, + Union[type[message.Message], type[bytes], None]]] = {} @classmethod def register_urn( - cls, urn: str, parameter_type: Optional[Type[T]] + cls, urn: str, parameter_type: Optional[type[T]] ) -> Callable[[ Callable[[ BeamTransformFactory, str, beam_runner_api_pb2.PTransform, T, - Dict[str, List[operations.Operation]] + dict[str, list[operations.Operation]] ], operations.Operation] ], @@ -1503,7 +1496,7 @@ def register_urn( str, beam_runner_api_pb2.PTransform, T, - Dict[str, List[operations.Operation]] + dict[str, list[operations.Operation]] ], operations.Operation]]: def wrapper(func): @@ -1514,7 +1507,7 @@ def wrapper(func): def create_operation( self, transform_id: str, - consumers: Dict[str, List[operations.Operation]]) -> operations.Operation: + consumers: dict[str, list[operations.Operation]]) -> operations.Operation: transform_proto = self.descriptor.transforms[transform_id] if not transform_proto.unique_name: _LOGGER.debug("No unique name set for transform %s" % transform_id) @@ -1524,7 +1517,7 @@ def create_operation( transform_proto.spec.payload, parameter_type) return creator(self, transform_id, transform_proto, payload, consumers) - def extract_timers_info(self) -> Dict[Tuple[str, str], TimerInfo]: + def extract_timers_info(self) -> dict[tuple[str, str], TimerInfo]: timers_info = {} for transform_id, transform_proto in self.descriptor.transforms.items(): if transform_proto.spec.urn == common_urns.primitives.PAR_DO.urn: @@ -1563,7 +1556,7 @@ def get_windowed_coder(self, pcoll_id: str) -> WindowedValueCoder: def get_output_coders( self, transform_proto: beam_runner_api_pb2.PTransform - ) -> Dict[str, coders.Coder]: + ) -> dict[str, coders.Coder]: return { tag: self.get_windowed_coder(pcoll_id) for tag, pcoll_id in transform_proto.outputs.items() @@ -1575,7 +1568,7 @@ def get_only_output_coder( def get_input_coders( self, transform_proto: beam_runner_api_pb2.PTransform - ) -> Dict[str, coders.WindowedValueCoder]: + ) -> dict[str, coders.WindowedValueCoder]: return { tag: self.get_windowed_coder(pcoll_id) for tag, pcoll_id in transform_proto.inputs.items() @@ -1598,7 +1591,7 @@ def augment_oldstyle_op( op: OperationT, step_name: str, consumers: Mapping[str, Iterable[operations.Operation]], - tag_list: Optional[List[str]] = None) -> OperationT: + tag_list: Optional[list[str]] = None) -> OperationT: op.step_name = step_name for tag, op_consumers in consumers.items(): for consumer in op_consumers: @@ -1613,7 +1606,7 @@ def create_source_runner( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, grpc_port: beam_fn_api_pb2.RemoteGrpcPort, - consumers: Dict[str, List[operations.Operation]]) -> DataInputOperation: + consumers: dict[str, list[operations.Operation]]) -> DataInputOperation: output_coder = factory.get_coder(grpc_port.coder_id) return DataInputOperation( @@ -1634,7 +1627,7 @@ def create_sink_runner( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, grpc_port: beam_fn_api_pb2.RemoteGrpcPort, - consumers: Dict[str, List[operations.Operation]]) -> DataOutputOperation: + consumers: dict[str, list[operations.Operation]]) -> DataOutputOperation: output_coder = factory.get_coder(grpc_port.coder_id) return DataOutputOperation( common.NameContext(transform_proto.unique_name, transform_id), @@ -1653,8 +1646,8 @@ def create_source_java( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter, - consumers: Dict[str, - List[operations.Operation]]) -> operations.ReadOperation: + consumers: dict[str, + list[operations.Operation]]) -> operations.ReadOperation: # The Dataflow runner harness strips the base64 encoding. source = pickler.loads(base64.b64encode(parameter)) spec = operation_specs.WorkerRead( @@ -1677,8 +1670,8 @@ def create_deprecated_read( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter: beam_runner_api_pb2.ReadPayload, - consumers: Dict[str, - List[operations.Operation]]) -> operations.ReadOperation: + consumers: dict[str, + list[operations.Operation]]) -> operations.ReadOperation: source = iobase.BoundedSource.from_runner_api( parameter.source, factory.context) spec = operation_specs.WorkerRead( @@ -1701,7 +1694,7 @@ def create_read_from_impulse_python( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter: beam_runner_api_pb2.ReadPayload, - consumers: Dict[str, List[operations.Operation]] + consumers: dict[str, list[operations.Operation]] ) -> operations.ImpulseReadOperation: return operations.ImpulseReadOperation( common.NameContext(transform_proto.unique_name, transform_id), @@ -1718,7 +1711,7 @@ def create_dofn_javasdk( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, serialized_fn, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): return _create_pardo_operation( factory, transform_id, transform_proto, consumers, serialized_fn) @@ -1806,7 +1799,7 @@ def create_process_sized_elements_and_restrictions( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter: beam_runner_api_pb2.ParDoPayload, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): return _create_pardo_operation( factory, transform_id, @@ -1852,7 +1845,7 @@ def create_par_do( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter: beam_runner_api_pb2.ParDoPayload, - consumers: Dict[str, List[operations.Operation]]) -> operations.DoOperation: + consumers: dict[str, list[operations.Operation]]) -> operations.DoOperation: return _create_pardo_operation( factory, transform_id, @@ -1985,7 +1978,7 @@ def create_assign_windows( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter: beam_runner_api_pb2.WindowingStrategy, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): class WindowIntoDoFn(beam.DoFn): def __init__(self, windowing): self.windowing = windowing @@ -2016,7 +2009,7 @@ def create_identity_dofn( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, parameter, - consumers: Dict[str, List[operations.Operation]] + consumers: dict[str, list[operations.Operation]] ) -> operations.FlattenOperation: return factory.augment_oldstyle_op( operations.FlattenOperation( @@ -2037,8 +2030,8 @@ def create_combine_per_key_precombine( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, payload: beam_runner_api_pb2.CombinePayload, - consumers: Dict[str, - List[operations.Operation]]) -> operations.PGBKCVOperation: + consumers: dict[str, + list[operations.Operation]]) -> operations.PGBKCVOperation: serialized_combine_fn = pickler.dumps(( beam.CombineFn.from_runner_api(payload.combine_fn, factory.context), [], {})) @@ -2063,7 +2056,7 @@ def create_combbine_per_key_merge_accumulators( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, payload: beam_runner_api_pb2.CombinePayload, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): return _create_combine_phase_operation( factory, transform_id, transform_proto, payload, consumers, 'merge') @@ -2076,7 +2069,7 @@ def create_combine_per_key_extract_outputs( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, payload: beam_runner_api_pb2.CombinePayload, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): return _create_combine_phase_operation( factory, transform_id, transform_proto, payload, consumers, 'extract') @@ -2089,7 +2082,7 @@ def create_combine_per_key_convert_to_accumulators( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, payload: beam_runner_api_pb2.CombinePayload, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): return _create_combine_phase_operation( factory, transform_id, transform_proto, payload, consumers, 'convert') @@ -2102,7 +2095,7 @@ def create_combine_grouped_values( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, payload: beam_runner_api_pb2.CombinePayload, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): return _create_combine_phase_operation( factory, transform_id, transform_proto, payload, consumers, 'all') @@ -2132,7 +2125,7 @@ def create_flatten( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, payload, - consumers: Dict[str, List[operations.Operation]] + consumers: dict[str, list[operations.Operation]] ) -> operations.FlattenOperation: return factory.augment_oldstyle_op( operations.FlattenOperation( @@ -2152,7 +2145,7 @@ def create_map_windows( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, mapping_fn_spec: beam_runner_api_pb2.FunctionSpec, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): assert mapping_fn_spec.urn == python_urns.PICKLED_WINDOW_MAPPING_FN window_mapping_fn = pickler.loads(mapping_fn_spec.payload) @@ -2172,7 +2165,7 @@ def create_merge_windows( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, mapping_fn_spec: beam_runner_api_pb2.FunctionSpec, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): assert mapping_fn_spec.urn == python_urns.PICKLED_WINDOWFN window_fn = pickler.loads(mapping_fn_spec.payload) @@ -2180,10 +2173,10 @@ class MergeWindows(beam.DoFn): def process(self, element): nonce, windows = element - original_windows: Set[window.BoundedWindow] = set(windows) + original_windows: set[window.BoundedWindow] = set(windows) merged_windows: MutableMapping[ window.BoundedWindow, - Set[window.BoundedWindow]] = collections.defaultdict( + set[window.BoundedWindow]] = collections.defaultdict( set) # noqa: F821 class RecordingMergeContext(window.WindowFn.MergeContext): @@ -2213,7 +2206,7 @@ def create_to_string_fn( transform_id: str, transform_proto: beam_runner_api_pb2.PTransform, mapping_fn_spec: beam_runner_api_pb2.FunctionSpec, - consumers: Dict[str, List[operations.Operation]]): + consumers: dict[str, list[operations.Operation]]): class ToString(beam.DoFn): def process(self, element): key, value = element diff --git a/sdks/python/apache_beam/runners/worker/data_sampler.py b/sdks/python/apache_beam/runners/worker/data_sampler.py index 4e4f1a64a508..10ea1cf5e91e 100644 --- a/sdks/python/apache_beam/runners/worker/data_sampler.py +++ b/sdks/python/apache_beam/runners/worker/data_sampler.py @@ -29,12 +29,8 @@ from dataclasses import dataclass from threading import Timer from typing import Any -from typing import Deque -from typing import Dict from typing import Iterable -from typing import List from typing import Optional -from typing import Tuple from typing import Union from apache_beam.coders.coder_impl import CoderImpl @@ -116,14 +112,15 @@ def __init__( coder: Coder, max_samples: int = 10, sample_every_sec: float = 5) -> None: - self._samples: Deque[Any] = collections.deque(maxlen=max_samples) + self._samples: collections.deque[Any] = collections.deque( + maxlen=max_samples) self._samples_lock: threading.Lock = threading.Lock() self._coder_impl: CoderImpl = coder.get_impl() self._sample_timer = SampleTimer(sample_every_sec, self) self.element_sampler = ElementSampler() self.element_sampler.has_element = False - self._exceptions: Deque[Tuple[Any, ExceptionMetadata]] = collections.deque( - maxlen=max_samples) + self._exceptions: collections.deque[tuple[ + Any, ExceptionMetadata]] = collections.deque(maxlen=max_samples) # For testing, it's easier to disable the Timer and manually sample. if sample_every_sec > 0: @@ -143,7 +140,7 @@ def remove_windowed_value(self, el: Union[WindowedValue, Any]) -> Any: el = el.value return el - def flush(self, clear: bool = True) -> List[beam_fn_api_pb2.SampledElement]: + def flush(self, clear: bool = True) -> list[beam_fn_api_pb2.SampledElement]: """Returns all samples and optionally clears buffer if clear is True.""" with self._samples_lock: # TODO(rohdesamuel): There can duplicates between the exceptions and @@ -226,13 +223,13 @@ def __init__( sample_only_exceptions: bool = False, clock=None) -> None: # Key is PCollection id. Is guarded by the _samplers_lock. - self._samplers: Dict[str, OutputSampler] = {} + self._samplers: dict[str, OutputSampler] = {} # Bundles are processed in parallel, so new samplers may be added when the # runner queries for samples. self._samplers_lock: threading.Lock = threading.Lock() self._max_samples = max_samples self._sample_every_sec = 0.0 if sample_only_exceptions else sample_every_sec - self._samplers_by_output: Dict[str, List[OutputSampler]] = {} + self._samplers_by_output: dict[str, list[OutputSampler]] = {} self._clock = clock _ENABLE_DATA_SAMPLING = 'enable_data_sampling' @@ -286,7 +283,7 @@ def initialize_samplers( self, transform_id: str, descriptor: beam_fn_api_pb2.ProcessBundleDescriptor, - coder_factory) -> List[OutputSampler]: + coder_factory) -> list[OutputSampler]: """Creates the OutputSamplers for the given PTransform. This initializes the samplers only once per PCollection Id. Note that an @@ -347,7 +344,7 @@ def samples( return ret def wait_for_samples( - self, pcollection_ids: List[str]) -> beam_fn_api_pb2.SampleDataResponse: + self, pcollection_ids: list[str]) -> beam_fn_api_pb2.SampleDataResponse: """Waits for samples to exist for the given PCollections (only testing).""" now = time.time() end = now + 30 diff --git a/sdks/python/apache_beam/runners/worker/data_sampler_test.py b/sdks/python/apache_beam/runners/worker/data_sampler_test.py index 47b6cca880d3..d54d7690e247 100644 --- a/sdks/python/apache_beam/runners/worker/data_sampler_test.py +++ b/sdks/python/apache_beam/runners/worker/data_sampler_test.py @@ -22,7 +22,6 @@ import traceback import unittest from typing import Any -from typing import List from typing import Optional from apache_beam.coders import FastPrimitivesCoder @@ -42,8 +41,8 @@ class DataSamplerTest(unittest.TestCase): def make_test_descriptor( self, - outputs: Optional[List[str]] = None, - transforms: Optional[List[str]] = None + outputs: Optional[list[str]] = None, + transforms: Optional[list[str]] = None ) -> beam_fn_api_pb2.ProcessBundleDescriptor: outputs = outputs or [MAIN_PCOLLECTION_ID] transforms = transforms or [MAIN_TRANSFORM_ID] diff --git a/sdks/python/apache_beam/runners/worker/log_handler.py b/sdks/python/apache_beam/runners/worker/log_handler.py index 69815acc7194..c9848a6460c2 100644 --- a/sdks/python/apache_beam/runners/worker/log_handler.py +++ b/sdks/python/apache_beam/runners/worker/log_handler.py @@ -29,7 +29,6 @@ import traceback from typing import Iterable from typing import Iterator -from typing import List from typing import Union from typing import cast @@ -184,7 +183,7 @@ def _write_log_entries(self) -> Iterator[beam_fn_api_pb2.LogEntry.List]: # typing: log_entries was initialized as List[Union[..., Sentinel]], # but now that we've popped the sentinel out (above) we can safely cast yield beam_fn_api_pb2.LogEntry.List( - log_entries=cast(List[beam_fn_api_pb2.LogEntry], log_entries)) + log_entries=cast(list[beam_fn_api_pb2.LogEntry], log_entries)) def _read_log_control_messages(self) -> None: # Only reconnect when we are alive. diff --git a/sdks/python/apache_beam/runners/worker/logger.py b/sdks/python/apache_beam/runners/worker/logger.py index 06e2508fb7d2..5fd326d6cd4d 100644 --- a/sdks/python/apache_beam/runners/worker/logger.py +++ b/sdks/python/apache_beam/runners/worker/logger.py @@ -20,6 +20,7 @@ # pytype: skip-file # mypy: disallow-untyped-defs +# ruff: noqa: UP006 import contextlib import json import logging diff --git a/sdks/python/apache_beam/runners/worker/opcounters.py b/sdks/python/apache_beam/runners/worker/opcounters.py index f5883cfbf2ef..66a369ba3bbb 100644 --- a/sdks/python/apache_beam/runners/worker/opcounters.py +++ b/sdks/python/apache_beam/runners/worker/opcounters.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import math import random import sys diff --git a/sdks/python/apache_beam/runners/worker/operations.py b/sdks/python/apache_beam/runners/worker/operations.py index f96eb3728717..bfa1c0f3d47c 100644 --- a/sdks/python/apache_beam/runners/worker/operations.py +++ b/sdks/python/apache_beam/runners/worker/operations.py @@ -20,6 +20,7 @@ # pytype: skip-file # pylint: disable=super-with-arguments +# ruff: noqa: UP006 import collections import logging import threading diff --git a/sdks/python/apache_beam/runners/worker/sdk_worker.py b/sdks/python/apache_beam/runners/worker/sdk_worker.py index 6060ff8d54a8..e1f17296057a 100644 --- a/sdks/python/apache_beam/runners/worker/sdk_worker.py +++ b/sdks/python/apache_beam/runners/worker/sdk_worker.py @@ -36,15 +36,11 @@ from typing import Any from typing import Callable from typing import DefaultDict -from typing import Dict -from typing import FrozenSet from typing import Generic from typing import Iterable from typing import Iterator -from typing import List from typing import MutableMapping from typing import Optional -from typing import Tuple from typing import TypeVar from typing import Union @@ -58,7 +54,6 @@ from apache_beam.portability.api import metrics_pb2 from apache_beam.runners.worker import bundle_processor from apache_beam.runners.worker import data_plane -from apache_beam.runners.worker import data_sampler from apache_beam.runners.worker import statesampler from apache_beam.runners.worker.channel_factory import GRPCChannelFactory from apache_beam.runners.worker.data_plane import PeriodicThread @@ -71,8 +66,7 @@ from apache_beam.version import __version__ as beam_version if TYPE_CHECKING: - from apache_beam.portability.api import endpoints_pb2 - from apache_beam.utils.profiler import Profile + pass T = TypeVar('T') _KT = TypeVar('_KT') @@ -1461,7 +1455,7 @@ def set(self, value): raise NotImplementedError() -class KeyedDefaultDict(DefaultDict[_KT, _VT]): +class KeyedDefaultDict(collections.defaultdict[_KT, _VT]): if TYPE_CHECKING: # we promise to only use a subset of what DefaultDict can do def __init__(self, default_factory): diff --git a/sdks/python/apache_beam/runners/worker/statecache.py b/sdks/python/apache_beam/runners/worker/statecache.py index d4e61cc9297f..33e1c55deeff 100644 --- a/sdks/python/apache_beam/runners/worker/statecache.py +++ b/sdks/python/apache_beam/runners/worker/statecache.py @@ -29,8 +29,6 @@ import weakref from typing import Any from typing import Callable -from typing import List -from typing import Tuple from typing import Union import objsize @@ -77,12 +75,12 @@ class CacheAware(object): def __init__(self) -> None: pass - def get_referents_for_cache(self) -> List[Any]: + def get_referents_for_cache(self) -> list[Any]: """Returns the list of objects accounted during cache measurement.""" raise NotImplementedError() -def _safe_isinstance(obj: Any, type: Union[type, Tuple[type, ...]]) -> bool: +def _safe_isinstance(obj: Any, type: Union[type, tuple[type, ...]]) -> bool: """ Return whether an object is an instance of a class or of a subclass thereof. See `isinstance()` for more information. @@ -126,7 +124,7 @@ def _size_func(obj: Any) -> int: _size_func.last_log_time = 0 # type: ignore -def _get_referents_func(*objs: List[Any]) -> List[Any]: +def _get_referents_func(*objs: list[Any]) -> list[Any]: """Returns the list of objects accounted during cache measurement. Users can inherit CacheAware to override which referents should be diff --git a/sdks/python/apache_beam/runners/worker/statesampler.py b/sdks/python/apache_beam/runners/worker/statesampler.py index b9c75f4de93d..22987b3f9c34 100644 --- a/sdks/python/apache_beam/runners/worker/statesampler.py +++ b/sdks/python/apache_beam/runners/worker/statesampler.py @@ -20,7 +20,6 @@ import contextlib import threading from typing import TYPE_CHECKING -from typing import Dict from typing import NamedTuple from typing import Optional from typing import Union @@ -96,7 +95,7 @@ def __init__( sampling_period_ms=DEFAULT_SAMPLING_PERIOD_MS): self._prefix = prefix self._counter_factory = counter_factory - self._states_by_name: Dict[CounterName, statesampler_impl.ScopedState] = {} + self._states_by_name: dict[CounterName, statesampler_impl.ScopedState] = {} self.sampling_period_ms = sampling_period_ms self.tracked_thread: Optional[threading.Thread] = None self.finished = False diff --git a/sdks/python/apache_beam/runners/worker/worker_pool_main.py b/sdks/python/apache_beam/runners/worker/worker_pool_main.py index 307261c2d3c3..425a9fc57752 100644 --- a/sdks/python/apache_beam/runners/worker/worker_pool_main.py +++ b/sdks/python/apache_beam/runners/worker/worker_pool_main.py @@ -37,9 +37,7 @@ import threading import time import traceback -from typing import Dict from typing import Optional -from typing import Tuple import grpc @@ -83,7 +81,7 @@ def __init__( self._container_executable = container_executable self._state_cache_size = state_cache_size self._data_buffer_time_limit_ms = data_buffer_time_limit_ms - self._worker_processes: Dict[str, subprocess.Popen] = {} + self._worker_processes: dict[str, subprocess.Popen] = {} @classmethod def start( @@ -92,7 +90,7 @@ def start( port=0, state_cache_size=0, data_buffer_time_limit_ms=-1, - container_executable: Optional[str] = None) -> Tuple[str, grpc.Server]: + container_executable: Optional[str] = None) -> tuple[str, grpc.Server]: options = [("grpc.http2.max_pings_without_data", 0), ("grpc.http2.max_ping_strikes", 0)] worker_server = grpc.server( diff --git a/sdks/python/apache_beam/testing/analyzers/github_issues_utils.py b/sdks/python/apache_beam/testing/analyzers/github_issues_utils.py index cbbb9e5d3a2e..83f8aa3bfb36 100644 --- a/sdks/python/apache_beam/testing/analyzers/github_issues_utils.py +++ b/sdks/python/apache_beam/testing/analyzers/github_issues_utils.py @@ -17,9 +17,7 @@ import json import logging import os -from typing import List from typing import Optional -from typing import Tuple import requests @@ -67,8 +65,8 @@ def create_issue( title: str, description: str, - labels: Optional[List[str]] = None, -) -> Tuple[int, str]: + labels: Optional[list[str]] = None, +) -> tuple[int, str]: """ Create an issue with title, description with a label. @@ -99,7 +97,7 @@ def create_issue( def comment_on_issue(issue_number: int, - comment_description: str) -> Tuple[bool, str]: + comment_description: str) -> tuple[bool, str]: """ This method looks for an issue with provided issue_number. If an open issue is found, comment on the open issue with provided description else @@ -210,9 +208,9 @@ def get_issue_description( def report_change_point_on_issues( title: str, description: str, - labels: Optional[List[str]] = None, + labels: Optional[list[str]] = None, existing_issue_number: Optional[int] = None, -) -> Tuple[int, str]: +) -> tuple[int, str]: """ Comments the description on the existing issue (if provided and still open), or creates a new issue. diff --git a/sdks/python/apache_beam/testing/analyzers/perf_analysis.py b/sdks/python/apache_beam/testing/analyzers/perf_analysis.py index 829f93fc1a35..118745a41b62 100644 --- a/sdks/python/apache_beam/testing/analyzers/perf_analysis.py +++ b/sdks/python/apache_beam/testing/analyzers/perf_analysis.py @@ -26,7 +26,6 @@ from datetime import datetime from datetime import timezone from typing import Any -from typing import Dict import pandas as pd @@ -46,7 +45,7 @@ def get_test_config_container( - params: Dict[str, Any], + params: dict[str, Any], test_id: str, metric_name: str, ) -> TestConfigContainer: @@ -69,7 +68,7 @@ def get_test_config_container( def get_change_point_config( - params: Dict[str, Any], + params: dict[str, Any], ) -> ChangePointConfig: """ Args: @@ -242,7 +241,7 @@ def run( defined in the config file. """ - tests_config: Dict[str, Dict[str, Any]] = read_test_config(config_file_path) + tests_config: dict[str, dict[str, Any]] = read_test_config(config_file_path) for test_id, params in tests_config.items(): # single test config can have multiple metrics so we need to diff --git a/sdks/python/apache_beam/testing/analyzers/perf_analysis_utils.py b/sdks/python/apache_beam/testing/analyzers/perf_analysis_utils.py index 62a0ec676499..2d91e201d428 100644 --- a/sdks/python/apache_beam/testing/analyzers/perf_analysis_utils.py +++ b/sdks/python/apache_beam/testing/analyzers/perf_analysis_utils.py @@ -19,10 +19,7 @@ from dataclasses import asdict from dataclasses import dataclass from statistics import median -from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Union import pandas as pd @@ -80,7 +77,7 @@ class TestConfigContainer: test_id: str # unique id for each test config. test_description: str test_name: Optional[str] = None - labels: Optional[List[str]] = None + labels: Optional[list[str]] = None @dataclass @@ -92,8 +89,8 @@ class MetricContainer: timestamps: List of pandas timestamps corresponding to the metric values. """ - values: List[Union[int, float]] - timestamps: List[pd.Timestamp] + values: list[Union[int, float]] + timestamps: list[pd.Timestamp] def sort_by_timestamp(self, in_place=True): """ @@ -138,9 +135,9 @@ def get_existing_issues_data(table_name: str) -> Optional[pd.DataFrame]: def is_sibling_change_point( - previous_change_point_timestamps: List[pd.Timestamp], + previous_change_point_timestamps: list[pd.Timestamp], change_point_index: int, - timestamps: List[pd.Timestamp], + timestamps: list[pd.Timestamp], min_runs_between_change_points: int, test_id: str, ) -> bool: @@ -181,7 +178,7 @@ def is_sibling_change_point( return True -def read_test_config(config_file_path: str) -> Dict: +def read_test_config(config_file_path: str) -> dict: """ Reads the config file in which the data required to run the change point analysis is specified. @@ -195,12 +192,12 @@ def validate_config(keys): return constants._PERF_TEST_KEYS.issubset(keys) -def find_change_points(metric_values: List[Union[float, int]]): +def find_change_points(metric_values: list[Union[float, int]]): return e_divisive(metric_values) def find_latest_change_point_index( - metric_values: List[Union[float, int]], + metric_values: list[Union[float, int]], median_abs_deviation_threshold: int = 2): """ Args: @@ -262,7 +259,7 @@ def create_performance_alert( metric_container: MetricContainer, change_point_index: int, existing_issue_number: Optional[int], -) -> Tuple[int, str]: +) -> tuple[int, str]: """ Creates performance alert on GitHub issues and returns GitHub issue number and issue URL. @@ -293,8 +290,8 @@ def create_performance_alert( def filter_change_points_by_median_threshold( - data: List[Union[int, float]], - change_points: List[int], + data: list[Union[int, float]], + change_points: list[int], threshold: float = 0.05, median_abs_deviation_threshold: int = 2, ): diff --git a/sdks/python/apache_beam/testing/benchmarks/nexmark/queries/query3.py b/sdks/python/apache_beam/testing/benchmarks/nexmark/queries/query3.py index f390c8c37001..df3684514f1f 100644 --- a/sdks/python/apache_beam/testing/benchmarks/nexmark/queries/query3.py +++ b/sdks/python/apache_beam/testing/benchmarks/nexmark/queries/query3.py @@ -108,11 +108,11 @@ def __init__(self, max_auction_wait_time): def process( # type: ignore self, - element: typing.Tuple[ + element: tuple[ str, - typing.Dict[str, - typing.Union[typing.List[nexmark_model.Auction], - typing.List[nexmark_model.Person]]]], + dict[str, + typing.Union[list[nexmark_model.Auction], + list[nexmark_model.Person]]]], auction_state=beam.DoFn.StateParam(auction_spec), person_state=beam.DoFn.StateParam(person_spec), person_timer=beam.DoFn.TimerParam(person_timer_spec)): diff --git a/sdks/python/apache_beam/testing/load_tests/load_test_metrics_utils.py b/sdks/python/apache_beam/testing/load_tests/load_test_metrics_utils.py index 33dfeeddba4f..6503cc9f273c 100644 --- a/sdks/python/apache_beam/testing/load_tests/load_test_metrics_utils.py +++ b/sdks/python/apache_beam/testing/load_tests/load_test_metrics_utils.py @@ -34,8 +34,6 @@ import time import uuid from typing import Any -from typing import Dict -from typing import List from typing import Mapping from typing import Optional from typing import Union @@ -212,7 +210,7 @@ def __init__( filters: MetricFilter to query only filtered metrics """ self._namespace = namespace - self.publishers: List[MetricsPublisher] = [] + self.publishers: list[MetricsPublisher] = [] # publish to console output self.publishers.append(ConsoleMetricsPublisher()) @@ -275,8 +273,8 @@ def publish_metrics( for publisher in self.publishers: publisher.publish(insert_dicts) - def _add_job_id_to_metrics(self, metrics: List[Dict[str, Any]], - job_id) -> List[Dict[str, Any]]: + def _add_job_id_to_metrics(self, metrics: list[dict[str, Any]], + job_id) -> list[dict[str, Any]]: for metric in metrics: metric[JOB_ID_LABEL] = job_id return metrics @@ -547,7 +545,7 @@ def __init__(self, options: InfluxDBMetricsPublisherOptions): self.options = options def publish( - self, results: List[Mapping[str, Union[float, str, int]]]) -> None: + self, results: list[Mapping[str, Union[float, str, int]]]) -> None: url = '{}/write'.format(self.options.hostname) payload = self._build_payload(results) query_str = {'db': self.options.db_name, 'precision': 's'} @@ -569,7 +567,7 @@ def publish( (response.status_code, content['error'])) def _build_payload( - self, results: List[Mapping[str, Union[float, str, int]]]) -> str: + self, results: list[Mapping[str, Union[float, str, int]]]) -> str: def build_kv(mapping, key): return '{}={}'.format(key, mapping[key]) diff --git a/sdks/python/apache_beam/testing/load_tests/sideinput_test.py b/sdks/python/apache_beam/testing/load_tests/sideinput_test.py index 3b5dfdf38cd9..4ccc0f09c4ed 100644 --- a/sdks/python/apache_beam/testing/load_tests/sideinput_test.py +++ b/sdks/python/apache_beam/testing/load_tests/sideinput_test.py @@ -58,9 +58,7 @@ import logging from typing import Any -from typing import Dict from typing import Iterable -from typing import Tuple from typing import Union import apache_beam as beam @@ -112,7 +110,7 @@ def __init__(self, first_n: int): self._first_n = first_n def process( - self, element: Any, side_input: Iterable[Tuple[bytes, + self, element: Any, side_input: Iterable[tuple[bytes, bytes]]) -> None: i = 0 it = iter(side_input) @@ -130,7 +128,7 @@ def __init__(self, first_n: int): self._first_n = first_n def process( - self, element: Any, dict_side_input: Dict[bytes, bytes]) -> None: + self, element: Any, dict_side_input: dict[bytes, bytes]) -> None: i = 0 for key in dict_side_input: if i == self._first_n: @@ -156,7 +154,7 @@ def __init__( self.key_size = key_size self.value_size = value_size - def process(self, element: Any) -> Iterable[Dict[str, Union[int, str]]]: + def process(self, element: Any) -> Iterable[dict[str, Union[int, str]]]: yield { 'num_records': self.elements_per_record, 'key_size': self.key_size, diff --git a/sdks/python/apache_beam/testing/synthetic_pipeline.py b/sdks/python/apache_beam/testing/synthetic_pipeline.py index 0ccbfbfbcc18..efc426ed0ff4 100644 --- a/sdks/python/apache_beam/testing/synthetic_pipeline.py +++ b/sdks/python/apache_beam/testing/synthetic_pipeline.py @@ -42,7 +42,6 @@ import time from random import Random from typing import Optional -from typing import Tuple import apache_beam as beam from apache_beam import pvalue @@ -911,7 +910,7 @@ def __init__(self, input_options, num_keys=100): self.value_size = input_options['value_size'] self.num_keys = num_keys - @typehints.with_output_types(Tuple[bytes, bytes]) + @typehints.with_output_types(tuple[bytes, bytes]) class GenerateKeys(beam.DoFn): def __init__(self, num_keys, key_size): self.num_keys = num_keys diff --git a/sdks/python/apache_beam/testing/util.py b/sdks/python/apache_beam/testing/util.py index 5a7c36fa4458..cfe8221b4aa2 100644 --- a/sdks/python/apache_beam/testing/util.py +++ b/sdks/python/apache_beam/testing/util.py @@ -25,7 +25,6 @@ import tempfile from typing import Any from typing import Iterable -from typing import List from typing import NamedTuple from apache_beam import pvalue @@ -67,7 +66,7 @@ class BeamAssertException(Exception): class TestWindowedValue(NamedTuple): value: Any timestamp: Any - windows: List + windows: list pane_info: PaneInfo = PANE_INFO_UNKNOWN diff --git a/sdks/python/apache_beam/tools/runtime_type_check_microbenchmark.py b/sdks/python/apache_beam/tools/runtime_type_check_microbenchmark.py index 6ba01b9a5172..07be679fb716 100644 --- a/sdks/python/apache_beam/tools/runtime_type_check_microbenchmark.py +++ b/sdks/python/apache_beam/tools/runtime_type_check_microbenchmark.py @@ -31,7 +31,6 @@ from collections import defaultdict from time import time from typing import Iterable -from typing import Tuple from typing import Union import apache_beam as beam @@ -39,27 +38,27 @@ from apache_beam.tools import utils -@beam.typehints.with_input_types(Tuple[int, ...]) +@beam.typehints.with_input_types(tuple[int, ...]) class SimpleInput(beam.DoFn): def process(self, element, *args, **kwargs): yield element -@beam.typehints.with_output_types(Tuple[int, ...]) +@beam.typehints.with_output_types(tuple[int, ...]) class SimpleOutput(beam.DoFn): def process(self, element, *args, **kwargs): yield element @beam.typehints.with_input_types( - Tuple[int, str, Tuple[float, ...], Iterable[int], Union[str, int]]) + tuple[int, str, tuple[float, ...], Iterable[int], Union[str, int]]) class NestedInput(beam.DoFn): def process(self, element, *args, **kwargs): yield element @beam.typehints.with_output_types( - Tuple[int, str, Tuple[float, ...], Iterable[int], Union[str, int]]) + tuple[int, str, tuple[float, ...], Iterable[int], Union[str, int]]) class NestedOutput(beam.DoFn): def process(self, element, *args, **kwargs): yield element diff --git a/sdks/python/apache_beam/transforms/core.py b/sdks/python/apache_beam/transforms/core.py index 35fa0e19ebd8..55a2cf40b64a 100644 --- a/sdks/python/apache_beam/transforms/core.py +++ b/sdks/python/apache_beam/transforms/core.py @@ -3441,8 +3441,8 @@ def StripNonce(nonce_key_value): | CombinePerKey(PostCombineFn())) -@typehints.with_input_types(typing.Tuple[K, V]) -@typehints.with_output_types(typing.Tuple[K, typing.Iterable[V]]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, typing.Iterable[V]]) class GroupByKey(PTransform): """A group by key transform. diff --git a/sdks/python/apache_beam/transforms/cy_combiners.py b/sdks/python/apache_beam/transforms/cy_combiners.py index b5cc7493a29a..6736ccdb3d66 100644 --- a/sdks/python/apache_beam/transforms/cy_combiners.py +++ b/sdks/python/apache_beam/transforms/cy_combiners.py @@ -22,6 +22,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import operator from apache_beam.transforms import core diff --git a/sdks/python/apache_beam/transforms/deduplicate.py b/sdks/python/apache_beam/transforms/deduplicate.py index 916b071fdf02..5ecdca0803f9 100644 --- a/sdks/python/apache_beam/transforms/deduplicate.py +++ b/sdks/python/apache_beam/transforms/deduplicate.py @@ -38,8 +38,8 @@ V = typing.TypeVar('V') -@typehints.with_input_types(typing.Tuple[K, V]) -@typehints.with_output_types(typing.Tuple[K, V]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, V]) class DeduplicatePerKey(ptransform.PTransform): """ A PTransform which deduplicates pair over a time domain and threshold. Values in different windows will NOT be considered duplicates of diff --git a/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql.py b/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql.py index d2ddd598209e..85ff530b1baf 100644 --- a/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql.py +++ b/sdks/python/apache_beam/transforms/enrichment_handlers/cloudsql.py @@ -23,8 +23,6 @@ from dataclasses import field from enum import Enum from typing import Any -from typing import Dict -from typing import List from typing import Optional from typing import Union @@ -59,7 +57,7 @@ class TableFieldsQueryConfig: """Configuration for using table name, where clause, and field names.""" table_id: str where_clause_template: str - where_clause_fields: List[str] + where_clause_fields: list[str] def __post_init__(self): if not self.table_id or not self.where_clause_template: @@ -144,8 +142,8 @@ class CloudSQLConnectionConfig(ConnectionConfig): password: str = field(default_factory=str) db_id: str = field(default_factory=str) refresh_strategy: RefreshStrategy = RefreshStrategy.LAZY - connector_kwargs: Dict[str, Any] = field(default_factory=dict) - connect_kwargs: Dict[str, Any] = field(default_factory=dict) + connector_kwargs: dict[str, Any] = field(default_factory=dict) + connect_kwargs: dict[str, Any] = field(default_factory=dict) def __post_init__(self): if not self.instance_connection_uri: @@ -191,7 +189,7 @@ class ExternalSQLDBConnectionConfig(ConnectionConfig): user: str = field(default_factory=str) password: str = field(default_factory=str) db_id: str = field(default_factory=str) - connect_kwargs: Dict[str, Any] = field(default_factory=dict) + connect_kwargs: dict[str, Any] = field(default_factory=dict) def __post_init__(self): if not self.host: @@ -338,7 +336,7 @@ def __call__( def _process_single_request(self, request: beam.Row): """Process a single request and return with its response.""" - response: Union[List[Dict[str, Any]], Dict[str, Any]] + response: Union[list[dict[str, Any]], dict[str, Any]] if isinstance(self._query_config, CustomQueryConfig): query = self._query_config.query_fn(request) response = self._execute_query(query, is_batch=False) @@ -369,7 +367,7 @@ def _process_batch_request(self, requests: list[beam.Row]): param_dict = self._build_parameters_dict(requests, batch_size) # Execute the parameterized query with validated parameters. - result: Union[List[Dict[str, Any]], Dict[str, Any]] = self._execute_query( + result: Union[list[dict[str, Any]], dict[str, Any]] = self._execute_query( raw_query, params=param_dict, is_batch=True) for response in result: response_row = beam.Row(**response) # type: ignore[arg-type] @@ -382,7 +380,7 @@ def _execute_query( self, query: str, params: Optional[dict] = None, - is_batch: bool = False) -> Union[List[Dict[str, Any]], Dict[str, Any]]: + is_batch: bool = False) -> Union[list[dict[str, Any]], dict[str, Any]]: connection = None try: connection = self._engine.connect() @@ -393,7 +391,7 @@ def _execute_query( else: result = connection.execute(text(query)) # Materialize results while transaction is active. - data: Union[List[Dict[str, Any]], Dict[str, Any]] + data: Union[list[dict[str, Any]], dict[str, Any]] if is_batch: data = [row._asdict() for row in result] else: diff --git a/sdks/python/apache_beam/transforms/external_test.py b/sdks/python/apache_beam/transforms/external_test.py index 5f2ffd34c3bd..137c92861ed7 100644 --- a/sdks/python/apache_beam/transforms/external_test.py +++ b/sdks/python/apache_beam/transforms/external_test.py @@ -131,7 +131,7 @@ def get_payload_from_typing_hints(self, values): ('integer_example', int), ('boolean', bool), ('string_example', str), - ('list_of_strings', typing.List[str]), + ('list_of_strings', list[str]), ('mapping', typing.Mapping[str, float]), ('optional_integer', typing.Optional[int]), ]) @@ -175,8 +175,7 @@ def test_implicit_payload_builder_with_bytes(self): # Verify we have not modified a cached type (BEAM-10766) # TODO(BEAM-7372): Remove when bytes coercion code is removed. - self.assertEqual( - typehints.List[bytes], convert_to_beam_type(typing.List[bytes])) + self.assertEqual(typehints.List[bytes], convert_to_beam_type(list[bytes])) class ExternalTransformTest(unittest.TestCase): @@ -417,7 +416,7 @@ def __init__( integer_example: int, boolean: bool, string_example: str, - list_of_strings: typing.List[str], + list_of_strings: list[str], mapping: typing.Mapping[str, float], optional_integer: typing.Optional[int] = None, expansion_service=None): @@ -474,7 +473,7 @@ class DataclassTransform(beam.ExternalTransform): integer_example: int boolean: bool string_example: str - list_of_strings: typing.List[str] + list_of_strings: list[str] mapping: typing.Mapping[str, float] = dataclasses.field(default=dict) optional_integer: typing.Optional[int] = None expansion_service: dataclasses.InitVar[typing.Optional[str]] = None diff --git a/sdks/python/apache_beam/transforms/external_transform_provider_it_test.py b/sdks/python/apache_beam/transforms/external_transform_provider_it_test.py index e86beab563bb..624ed81d7e86 100644 --- a/sdks/python/apache_beam/transforms/external_transform_provider_it_test.py +++ b/sdks/python/apache_beam/transforms/external_transform_provider_it_test.py @@ -195,18 +195,18 @@ def test_script_fails_with_invalid_destinations(self): def test_pretty_types(self): types = [ - typing.Optional[typing.List[str]], + typing.Optional[list[str]], numpy.int16, str, - typing.Dict[str, numpy.float64], - typing.Optional[typing.Dict[str, typing.List[numpy.int64]]], - typing.Dict[int, typing.Optional[str]] + dict[str, numpy.float64], + typing.Optional[dict[str, list[numpy.int64]]], + dict[int, typing.Optional[str]] ] - expected_type_names = [('List[str]', True), ('int16', False), - ('str', False), ('Dict[str, float64]', False), - ('Dict[str, List[int64]]', True), - ('Dict[int, Optional[str]]', False)] + expected_type_names = [('list[str]', True), ('int16', False), + ('str', False), ('dict[str, float64]', False), + ('dict[str, list[int64]]', True), + ('dict[int, Optional[str]]', False)] for i in range(len(types)): self.assertEqual( @@ -248,7 +248,7 @@ def get_module(self, dest): return importlib.import_module(module) def write_wrappers_to_destinations_and_validate( - self, destinations: typing.List[str]): + self, destinations: list[str]): """ Generate wrappers from the config path and validate all destinations are included. diff --git a/sdks/python/apache_beam/transforms/ptransform_test.py b/sdks/python/apache_beam/transforms/ptransform_test.py index 8c2acefccdb3..21691eebf3ae 100644 --- a/sdks/python/apache_beam/transforms/ptransform_test.py +++ b/sdks/python/apache_beam/transforms/ptransform_test.py @@ -1570,7 +1570,7 @@ def process(self, element, num): def test_pardo_does_not_type_check_using_type_hint_decorators(self): @with_input_types(a=int) - @with_output_types(typing.List[str]) + @with_output_types(list[str]) def int_to_str(a): return [str(a)] @@ -1585,7 +1585,7 @@ def int_to_str(a): def test_pardo_properly_type_checks_using_type_hint_decorators(self): @with_input_types(a=str) - @with_output_types(typing.List[str]) + @with_output_types(list[str]) def to_all_upper_case(a): return [a.upper()] @@ -1733,13 +1733,13 @@ def test_pardo_like_inheriting_output_types_from_annotation(self): def fn1(x: str) -> int: return 1 - def fn1_flat(x: str) -> typing.List[int]: + def fn1_flat(x: str) -> list[int]: return [1] def fn2(x: int, y: str) -> str: return y - def fn2_flat(x: int, y: str) -> typing.List[str]: + def fn2_flat(x: int, y: str) -> list[str]: return [y] # We only need the args section of the hints. @@ -1756,23 +1756,21 @@ def add(a: typing.Iterable[int]) -> int: return sum(a) self.assertCompatible( - typing.Tuple[typing.TypeVar('K'), int], - output_hints(beam.CombinePerKey(add))) + tuple[typing.TypeVar('K'), int], output_hints(beam.CombinePerKey(add))) def test_group_by_key_only_output_type_deduction(self): d = ( self.p | 'Str' >> beam.Create(['t', 'e', 's', 't']).with_output_types(str) | ( - 'Pair' >> beam.Map(lambda x: (x, ord(x))).with_output_types( - typing.Tuple[str, str])) + 'Pair' >> beam.Map(lambda x: + (x, ord(x))).with_output_types(tuple[str, str])) | beam.GroupByKey()) # Output type should correctly be deduced. # GBK-only should deduce that Tuple[A, B] is turned into # Tuple[A, Iterable[B]]. - self.assertCompatible( - typing.Tuple[str, typing.Iterable[str]], d.element_type) + self.assertCompatible(tuple[str, typing.Iterable[str]], d.element_type) def test_group_by_key_output_type_deduction(self): d = ( @@ -1780,13 +1778,12 @@ def test_group_by_key_output_type_deduction(self): | 'Str' >> beam.Create(range(20)).with_output_types(int) | ( 'PairNegative' >> beam.Map(lambda x: (x % 5, -x)).with_output_types( - typing.Tuple[int, int])) + tuple[int, int])) | beam.GroupByKey()) # Output type should correctly be deduced. # GBK should deduce that Tuple[A, B] is turned into Tuple[A, Iterable[B]]. - self.assertCompatible( - typing.Tuple[int, typing.Iterable[int]], d.element_type) + self.assertCompatible(tuple[int, typing.Iterable[int]], d.element_type) def test_group_by_key_only_does_not_type_check(self): # GBK will be passed raw int's here instead of some form of Tuple[A, B]. @@ -1886,7 +1883,7 @@ def test_run_time_type_checking_enabled_types_satisfied(self): self.p._options.view_as(TypeOptions).pipeline_type_check = False self.p._options.view_as(TypeOptions).runtime_type_check = True - @with_output_types(typing.Tuple[int, str]) + @with_output_types(tuple[int, str]) @with_input_types(x=str) def group_with_upper_ord(x): return (ord(x.upper()) % 5, x) @@ -1910,7 +1907,7 @@ def test_pipeline_checking_satisfied_but_run_time_types_violate(self): self.p._options.view_as(TypeOptions).pipeline_type_check = False self.p._options.view_as(TypeOptions).runtime_type_check = True - @with_output_types(typing.Tuple[bool, int]) + @with_output_types(tuple[bool, int]) @with_input_types(a=int) def is_even_as_key(a): # Simulate a programming error, should be: return (a % 2 == 0, a) @@ -1934,7 +1931,7 @@ def is_even_as_key(a): def test_pipeline_checking_satisfied_run_time_checking_satisfied(self): self.p._options.view_as(TypeOptions).pipeline_type_check = False - @with_output_types(typing.Tuple[bool, int]) + @with_output_types(tuple[bool, int]) @with_input_types(a=int) def is_even_as_key(a): # The programming error in the above test-case has now been fixed. @@ -1982,7 +1979,7 @@ def test_pipeline_runtime_checking_violation_composite_type_input(self): | ( 'Add' >> beam.FlatMap(lambda x_y: [x_y[0] + x_y[1]]).with_input_types( - typing.Tuple[int, int]).with_output_types(int))) + tuple[int, int]).with_output_types(int))) self.p.run() def test_pipeline_runtime_checking_violation_simple_type_output(self): @@ -2041,8 +2038,7 @@ def test_pipeline_runtime_checking_violation_composite_type_output(self): | ( 'Swap' >> beam.FlatMap(lambda x_y1: [x_y1[0] + x_y1[1]]).with_input_types( - typing.Tuple[int, float]).with_output_types( - typing.Tuple[float, int]))) + tuple[int, float]).with_output_types(tuple[float, int]))) self.p.run() def test_pipeline_runtime_checking_violation_with_side_inputs_decorator(self): @@ -2126,7 +2122,7 @@ def test_combine_pipeline_type_propagation_using_decorators(self): def sum_ints(ints): return sum(ints) - @with_output_types(typing.List[int]) + @with_output_types(list[int]) @with_input_types(n=int) def range_from_zero(n): return list(range(n + 1)) @@ -2312,10 +2308,10 @@ def test_mean_per_key_pipeline_checking_satisfied(self): | beam.Create(range(5)).with_output_types(int) | ( 'EvenGroup' >> beam.Map(lambda x: (not x % 2, x)).with_output_types( - typing.Tuple[bool, int])) + tuple[bool, int])) | 'EvenMean' >> combine.Mean.PerKey()) - self.assertCompatible(typing.Tuple[bool, float], d.element_type) + self.assertCompatible(tuple[bool, float], d.element_type) assert_that(d, equal_to([(False, 2.0), (True, 2.0)])) self.p.run() @@ -2327,7 +2323,7 @@ def test_mean_per_key_pipeline_checking_violated(self): | ( 'UpperPair' >> beam.Map(lambda x: (x.upper(), x)).with_output_types( - typing.Tuple[str, str])) + tuple[str, str])) | 'EvenMean' >> combine.Mean.PerKey()) self.p.run() err_msg = e.exception.args[0] @@ -2346,10 +2342,10 @@ def test_mean_per_key_runtime_checking_satisfied(self): | ( 'OddGroup' >> beam.Map(lambda x: (bool(x % 2), x)).with_output_types( - typing.Tuple[bool, int])) + tuple[bool, int])) | 'OddMean' >> combine.Mean.PerKey()) - self.assertCompatible(typing.Tuple[bool, float], d.element_type) + self.assertCompatible(tuple[bool, float], d.element_type) assert_that(d, equal_to([(False, 2.0), (True, 2.0)])) self.p.run() @@ -2366,7 +2362,7 @@ def test_mean_per_key_runtime_checking_violated(self): | ( 'OddGroup' >> beam.Map(lambda x: (x, str(bool(x % 2)))).with_output_types( - typing.Tuple[int, str])) + tuple[int, str])) | 'OddMean' >> combine.Mean.PerKey()) self.p.run() @@ -2397,10 +2393,10 @@ def test_count_perkey_pipeline_type_checking_satisfied(self): self.p | beam.Create(range(5)).with_output_types(int) | 'EvenGroup' >> beam.Map(lambda x: (not x % 2, x)).with_output_types( - typing.Tuple[bool, int]) + tuple[bool, int]) | 'CountInt' >> combine.Count.PerKey()) - self.assertCompatible(typing.Tuple[bool, int], d.element_type) + self.assertCompatible(tuple[bool, int], d.element_type) assert_that(d, equal_to([(False, 2), (True, 3)])) self.p.run() @@ -2420,11 +2416,11 @@ def test_count_perkey_runtime_type_checking_satisfied(self): d = ( self.p | beam.Create(['t', 'e', 's', 't']).with_output_types(str) - | 'DupKey' >> beam.Map(lambda x: (x, x)).with_output_types( - typing.Tuple[str, str]) + | 'DupKey' >> beam.Map(lambda x: + (x, x)).with_output_types(tuple[str, str]) | 'CountDups' >> combine.Count.PerKey()) - self.assertCompatible(typing.Tuple[str, int], d.element_type) + self.assertCompatible(tuple[str, int], d.element_type) assert_that(d, equal_to([('e', 1), ('s', 1), ('t', 2)])) self.p.run() @@ -2434,7 +2430,7 @@ def test_count_perelement_pipeline_type_checking_satisfied(self): | beam.Create([1, 1, 2, 3]).with_output_types(int) | 'CountElems' >> combine.Count.PerElement()) - self.assertCompatible(typing.Tuple[int, int], d.element_type) + self.assertCompatible(tuple[int, int], d.element_type) assert_that(d, equal_to([(1, 2), (2, 1), (3, 1)])) self.p.run() @@ -2461,7 +2457,7 @@ def test_count_perelement_runtime_type_checking_satisfied(self): | beam.Create([True, True, False, True, True]).with_output_types(bool) | 'CountElems' >> combine.Count.PerElement()) - self.assertCompatible(typing.Tuple[bool, int], d.element_type) + self.assertCompatible(tuple[bool, int], d.element_type) assert_that(d, equal_to([(False, 1), (True, 4)])) self.p.run() @@ -2506,11 +2502,10 @@ def test_per_key_pipeline_checking_satisfied(self): | beam.Create(range(100)).with_output_types(int) | ( 'GroupMod 3' >> beam.Map(lambda x: (x % 3, x)).with_output_types( - typing.Tuple[int, int])) + tuple[int, int])) | 'TopMod' >> combine.Top.PerKey(1)) - self.assertCompatible( - typing.Tuple[int, typing.Iterable[int]], d.element_type) + self.assertCompatible(tuple[int, typing.Iterable[int]], d.element_type) assert_that(d, equal_to([(0, [99]), (1, [97]), (2, [98])])) self.p.run() @@ -2522,11 +2517,10 @@ def test_per_key_runtime_checking_satisfied(self): | beam.Create(range(21)) | ( 'GroupMod 3' >> beam.Map(lambda x: (x % 3, x)).with_output_types( - typing.Tuple[int, int])) + tuple[int, int])) | 'TopMod' >> combine.Top.PerKey(1)) - self.assertCompatible( - typing.Tuple[int, typing.Iterable[int]], d.element_type) + self.assertCompatible(tuple[int, typing.Iterable[int]], d.element_type) assert_that(d, equal_to([(0, [18]), (1, [19]), (2, [20])])) self.p.run() @@ -2571,11 +2565,10 @@ def test_sample_per_key_pipeline_satisfied(self): self.p | ( beam.Create([(1, 2), (1, 2), (2, 3), - (2, 3)]).with_output_types(typing.Tuple[int, int])) + (2, 3)]).with_output_types(tuple[int, int])) | 'Sample' >> combine.Sample.FixedSizePerKey(2)) - self.assertCompatible( - typing.Tuple[int, typing.Iterable[int]], d.element_type) + self.assertCompatible(tuple[int, typing.Iterable[int]], d.element_type) def matcher(expected_len): def match(actual): @@ -2594,11 +2587,10 @@ def test_sample_per_key_runtime_satisfied(self): self.p | ( beam.Create([(1, 2), (1, 2), (2, 3), - (2, 3)]).with_output_types(typing.Tuple[int, int])) + (2, 3)]).with_output_types(tuple[int, int])) | 'Sample' >> combine.Sample.FixedSizePerKey(1)) - self.assertCompatible( - typing.Tuple[int, typing.Iterable[int]], d.element_type) + self.assertCompatible(tuple[int, typing.Iterable[int]], d.element_type) def matcher(expected_len): def match(actual): @@ -2616,7 +2608,7 @@ def test_to_list_pipeline_check_satisfied(self): | beam.Create((1, 2, 3, 4)).with_output_types(int) | combine.ToList()) - self.assertCompatible(typing.List[int], d.element_type) + self.assertCompatible(list[int], d.element_type) def matcher(expected): def match(actual): @@ -2635,7 +2627,7 @@ def test_to_list_runtime_check_satisfied(self): | beam.Create(list('test')).with_output_types(str) | combine.ToList()) - self.assertCompatible(typing.List[str], d.element_type) + self.assertCompatible(list[str], d.element_type) def matcher(expected): def match(actual): @@ -2661,11 +2653,10 @@ def test_to_dict_pipeline_check_violated(self): def test_to_dict_pipeline_check_satisfied(self): d = ( self.p - | beam.Create([(1, 2), - (3, 4)]).with_output_types(typing.Tuple[int, int]) + | beam.Create([(1, 2), (3, 4)]).with_output_types(tuple[int, int]) | combine.ToDict()) - self.assertCompatible(typing.Dict[int, int], d.element_type) + self.assertCompatible(dict[int, int], d.element_type) assert_that(d, equal_to([{1: 2, 3: 4}])) self.p.run() @@ -2674,12 +2665,11 @@ def test_to_dict_runtime_check_satisfied(self): d = ( self.p - | ( - beam.Create([('1', 2), - ('3', 4)]).with_output_types(typing.Tuple[str, int])) + | + (beam.Create([('1', 2), ('3', 4)]).with_output_types(tuple[str, int])) | combine.ToDict()) - self.assertCompatible(typing.Dict[str, int], d.element_type) + self.assertCompatible(dict[str, int], d.element_type) assert_that(d, equal_to([{'1': 2, '3': 4}])) self.p.run() diff --git a/sdks/python/apache_beam/transforms/sideinputs_test.py b/sdks/python/apache_beam/transforms/sideinputs_test.py index 9b79b9d1fa8d..e2af0d2c12f0 100644 --- a/sdks/python/apache_beam/transforms/sideinputs_test.py +++ b/sdks/python/apache_beam/transforms/sideinputs_test.py @@ -24,9 +24,7 @@ import logging import unittest from typing import Any -from typing import Dict from typing import Iterable -from typing import Tuple from typing import Union import pytest @@ -445,7 +443,7 @@ def test_side_input_with_sdf(self): class GetSyntheticSDFOptions(beam.DoFn): """A DoFn that emits elements for genenrating SDF.""" - def process(self, element: Any) -> Iterable[Dict[str, Union[int, str]]]: + def process(self, element: Any) -> Iterable[dict[str, Union[int, str]]]: yield { 'num_records': num_records // initial_elements, 'key_size': key_size, @@ -464,8 +462,8 @@ class SideInputTrackingDoFn(beam.DoFn): """ def process( self, element: Any, - side_input: Iterable[Tuple[bytes, - bytes]]) -> Iterable[Tuple[int, str]]: + side_input: Iterable[tuple[bytes, + bytes]]) -> Iterable[tuple[int, str]]: # Sort for consistent hashing. sorted_side_input = sorted(side_input) diff --git a/sdks/python/apache_beam/transforms/stats.py b/sdks/python/apache_beam/transforms/stats.py index fb38a883dd39..7cefe58dd133 100644 --- a/sdks/python/apache_beam/transforms/stats.py +++ b/sdks/python/apache_beam/transforms/stats.py @@ -28,6 +28,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import hashlib import heapq import itertools diff --git a/sdks/python/apache_beam/transforms/util.py b/sdks/python/apache_beam/transforms/util.py index 4e16b805184a..2ea9df9399cb 100644 --- a/sdks/python/apache_beam/transforms/util.py +++ b/sdks/python/apache_beam/transforms/util.py @@ -35,9 +35,7 @@ from collections.abc import Iterable from typing import TYPE_CHECKING from typing import Any -from typing import List from typing import Optional -from typing import Tuple from typing import TypeVar from typing import Union @@ -586,7 +584,7 @@ def setup(self): self.fernet = Fernet(self._hmac_key) def process(self, - element: Any) -> Iterable[Tuple[bytes, Tuple[bytes, bytes]]]: + element: Any) -> Iterable[tuple[bytes, tuple[bytes, bytes]]]: """Encrypts the key and value of an element. Args: @@ -622,7 +620,7 @@ def setup(self): hmac_key = self.hmac_key_secret.get_secret_bytes() self.fernet = Fernet(hmac_key) - def decode_value(self, encoded_element: Tuple[bytes, bytes]) -> Any: + def decode_value(self, encoded_element: tuple[bytes, bytes]) -> Any: encrypted_value = encoded_element[1] encoded_value = self.fernet.decrypt(encrypted_value) real_val = self.value_coder.decode(encoded_value) @@ -631,7 +629,7 @@ def decode_value(self, encoded_element: Tuple[bytes, bytes]) -> Any: def filter_elements_by_key( self, encrypted_key: bytes, - encoded_elements: Iterable[Tuple[bytes, bytes]]) -> Iterable[Any]: + encoded_elements: Iterable[tuple[bytes, bytes]]) -> Iterable[Any]: for e in encoded_elements: if encrypted_key == self.fernet.decrypt(e[0]): yield self.decode_value(e) @@ -640,8 +638,8 @@ def filter_elements_by_key( # here. This does mean that the whole list will be materialized every time, # but passing an Iterable containing an Iterable breaks when pickling happens def process( - self, element: Tuple[bytes, Iterable[Tuple[bytes, bytes]]] - ) -> Iterable[Tuple[Any, List[Any]]]: + self, element: tuple[bytes, Iterable[tuple[bytes, bytes]]] + ) -> Iterable[tuple[Any, list[Any]]]: """Decrypts the key and values of an element. Args: @@ -669,8 +667,8 @@ def process( list(self.filter_elements_by_key(encoded_key, encoded_elements))) -@typehints.with_input_types(Tuple[K, V]) -@typehints.with_output_types(Tuple[K, Iterable[V]]) +@typehints.with_input_types(tuple[K, V]) +@typehints.with_output_types(tuple[K, Iterable[V]]) class GroupByEncryptedKey(PTransform): """A PTransform that provides a secure alternative to GroupByKey. @@ -727,7 +725,7 @@ def expand(self, pcoll): gbk = beam.GroupByKey() gbk._inside_gbek = True - output_type = Tuple[key_type, Iterable[value_type]] + output_type = tuple[key_type, Iterable[value_type]] return ( pcoll diff --git a/sdks/python/apache_beam/transforms/validate_runner_xlang_test.py b/sdks/python/apache_beam/transforms/validate_runner_xlang_test.py index 72371b38fdf6..e50c8b064f7e 100644 --- a/sdks/python/apache_beam/transforms/validate_runner_xlang_test.py +++ b/sdks/python/apache_beam/transforms/validate_runner_xlang_test.py @@ -53,7 +53,6 @@ import logging import os import sys -import typing import unittest from datetime import datetime @@ -145,7 +144,7 @@ def run_group_by_key(self, pipeline): p | beam.Create([(0, "1"), (0, "2"), (1, "3")], reshuffle=False).with_output_types( - typing.Tuple[int, str]) + tuple[int, str]) | beam.ExternalTransform(TEST_GBK_URN, None, self.expansion_service) | beam.Map(lambda x: "{}:{}".format(x[0], ','.join(sorted(x[1]))))) assert_that(res, equal_to(['0:1,2', '1:3'])) @@ -165,7 +164,7 @@ def run_group_by_key_no_assert(self, pipeline): p | beam.Create([(0, "1"), (0, "2"), (1, "3")], reshuffle=False).with_output_types( - typing.Tuple[int, str]) + tuple[int, str]) | beam.ExternalTransform(TEST_GBK_URN, None, self.expansion_service)) def run_cogroup_by_key(self, pipeline): @@ -180,11 +179,11 @@ def run_cogroup_by_key(self, pipeline): """ with pipeline as p: col1 = p | 'create_col1' >> beam.Create( - [(0, "1"), (0, "2"), (1, "3")], reshuffle=False).with_output_types( - typing.Tuple[int, str]) + [(0, "1"), (0, "2"), + (1, "3")], reshuffle=False).with_output_types(tuple[int, str]) col2 = p | 'create_col2' >> beam.Create( - [(0, "4"), (1, "5"), (1, "6")], reshuffle=False).with_output_types( - typing.Tuple[int, str]) + [(0, "4"), (1, "5"), + (1, "6")], reshuffle=False).with_output_types(tuple[int, str]) res = ( dict(col1=col1, col2=col2) | beam.ExternalTransform(TEST_CGBK_URN, None, self.expansion_service) @@ -223,7 +222,7 @@ def run_combine_per_key(self, pipeline): res = ( p | beam.Create([('a', 1), ('a', 2), - ('b', 3)]).with_output_types(typing.Tuple[str, int]) + ('b', 3)]).with_output_types(tuple[str, int]) | beam.ExternalTransform( TEST_COMPK_URN, None, self.expansion_service)) assert_that(res, equal_to([('a', 3), ('b', 3)])) diff --git a/sdks/python/apache_beam/typehints/arrow_type_compatibility.py b/sdks/python/apache_beam/typehints/arrow_type_compatibility.py index ac0985f4b9c6..012c55388c3a 100644 --- a/sdks/python/apache_beam/typehints/arrow_type_compatibility.py +++ b/sdks/python/apache_beam/typehints/arrow_type_compatibility.py @@ -20,6 +20,7 @@ For internal use only, no backward compatibility guarantees. """ +# ruff: noqa: UP006 from functools import partial from typing import Dict from typing import List diff --git a/sdks/python/apache_beam/typehints/batch_test.py b/sdks/python/apache_beam/typehints/batch_test.py index 3fbad76fce06..c62822edf406 100644 --- a/sdks/python/apache_beam/typehints/batch_test.py +++ b/sdks/python/apache_beam/typehints/batch_test.py @@ -17,6 +17,7 @@ """Unit tests for batched type converters.""" +# ruff: noqa: UP006 import contextlib import random import typing diff --git a/sdks/python/apache_beam/typehints/decorators.py b/sdks/python/apache_beam/typehints/decorators.py index e393113c002e..bc44ec745d5a 100644 --- a/sdks/python/apache_beam/typehints/decorators.py +++ b/sdks/python/apache_beam/typehints/decorators.py @@ -79,6 +79,7 @@ def foo((a, b)): # pytype: skip-file +# ruff: noqa: UP006 import inspect import itertools import logging diff --git a/sdks/python/apache_beam/typehints/decorators_test.py b/sdks/python/apache_beam/typehints/decorators_test.py index 95745f4e3d88..73efea9e9abf 100644 --- a/sdks/python/apache_beam/typehints/decorators_test.py +++ b/sdks/python/apache_beam/typehints/decorators_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import functools import typing import unittest diff --git a/sdks/python/apache_beam/typehints/native_type_compatibility.py b/sdks/python/apache_beam/typehints/native_type_compatibility.py index f1f58f8c2bea..7f28d57b2f81 100644 --- a/sdks/python/apache_beam/typehints/native_type_compatibility.py +++ b/sdks/python/apache_beam/typehints/native_type_compatibility.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import collections import collections.abc import dataclasses diff --git a/sdks/python/apache_beam/typehints/native_type_compatibility_test.py b/sdks/python/apache_beam/typehints/native_type_compatibility_test.py index e9e101bb13da..33d6051afc7a 100644 --- a/sdks/python/apache_beam/typehints/native_type_compatibility_test.py +++ b/sdks/python/apache_beam/typehints/native_type_compatibility_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import collections.abc import dataclasses import enum diff --git a/sdks/python/apache_beam/typehints/pandas_type_compatibility.py b/sdks/python/apache_beam/typehints/pandas_type_compatibility.py index 9fbfd5c3ddfc..45ae27baffe7 100644 --- a/sdks/python/apache_beam/typehints/pandas_type_compatibility.py +++ b/sdks/python/apache_beam/typehints/pandas_type_compatibility.py @@ -51,6 +51,7 @@ compatibility guarantees, except for the type mapping itself. """ +# ruff: noqa: UP006 from typing import Any from typing import List from typing import Optional diff --git a/sdks/python/apache_beam/typehints/row_type.py b/sdks/python/apache_beam/typehints/row_type.py index 0697581cb435..5e0faf91f55b 100644 --- a/sdks/python/apache_beam/typehints/row_type.py +++ b/sdks/python/apache_beam/typehints/row_type.py @@ -17,6 +17,7 @@ # pytype: skip-file +# ruff: noqa: UP006 from __future__ import annotations import dataclasses diff --git a/sdks/python/apache_beam/typehints/schemas.py b/sdks/python/apache_beam/typehints/schemas.py index 2a7576946833..ca33e1eefcbe 100644 --- a/sdks/python/apache_beam/typehints/schemas.py +++ b/sdks/python/apache_beam/typehints/schemas.py @@ -67,6 +67,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import datetime import decimal import logging diff --git a/sdks/python/apache_beam/typehints/schemas_test.py b/sdks/python/apache_beam/typehints/schemas_test.py index 3a6d81fab5b9..a8db60c9260a 100644 --- a/sdks/python/apache_beam/typehints/schemas_test.py +++ b/sdks/python/apache_beam/typehints/schemas_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import dataclasses import datetime import itertools diff --git a/sdks/python/apache_beam/typehints/typecheck_test.py b/sdks/python/apache_beam/typehints/typecheck_test.py index c2eaa0f6f9f7..81046cd3cc0f 100644 --- a/sdks/python/apache_beam/typehints/typecheck_test.py +++ b/sdks/python/apache_beam/typehints/typecheck_test.py @@ -23,6 +23,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import os import tempfile import unittest diff --git a/sdks/python/apache_beam/typehints/typed_pipeline_test.py b/sdks/python/apache_beam/typehints/typed_pipeline_test.py index 820f78fa9ef5..c97a1977e9c3 100644 --- a/sdks/python/apache_beam/typehints/typed_pipeline_test.py +++ b/sdks/python/apache_beam/typehints/typed_pipeline_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import typing import unittest from typing import Tuple diff --git a/sdks/python/apache_beam/typehints/typehints.py b/sdks/python/apache_beam/typehints/typehints.py index f429935c3a0e..eec9ea86bd4c 100644 --- a/sdks/python/apache_beam/typehints/typehints.py +++ b/sdks/python/apache_beam/typehints/typehints.py @@ -65,6 +65,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import copy import logging import types diff --git a/sdks/python/apache_beam/typehints/typehints_test.py b/sdks/python/apache_beam/typehints/typehints_test.py index 992c129fd8a5..a335ab05f1b7 100644 --- a/sdks/python/apache_beam/typehints/typehints_test.py +++ b/sdks/python/apache_beam/typehints/typehints_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import collections.abc import functools import re diff --git a/sdks/python/apache_beam/utils/counters.py b/sdks/python/apache_beam/utils/counters.py index 57d73fa283eb..3281c286b8fb 100644 --- a/sdks/python/apache_beam/utils/counters.py +++ b/sdks/python/apache_beam/utils/counters.py @@ -25,6 +25,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import threading from collections import namedtuple from typing import TYPE_CHECKING diff --git a/sdks/python/apache_beam/utils/multi_process_shared.py b/sdks/python/apache_beam/utils/multi_process_shared.py index de4b94bc5da3..b05fdd305a60 100644 --- a/sdks/python/apache_beam/utils/multi_process_shared.py +++ b/sdks/python/apache_beam/utils/multi_process_shared.py @@ -32,7 +32,6 @@ import traceback from typing import Any from typing import Callable -from typing import Dict from typing import Generic from typing import Optional from typing import TypeVar @@ -156,7 +155,7 @@ def unsafe_hard_delete(self): class _SingletonManager: - entries: Dict[Any, Any] = {} + entries: dict[Any, Any] = {} def __init__(self): self._hard_delete_callback = None diff --git a/sdks/python/apache_beam/utils/proto_utils.py b/sdks/python/apache_beam/utils/proto_utils.py index 60c0af2ebac0..89a6f8a4648c 100644 --- a/sdks/python/apache_beam/utils/proto_utils.py +++ b/sdks/python/apache_beam/utils/proto_utils.py @@ -19,7 +19,6 @@ # pytype: skip-file -from typing import Type from typing import TypeVar from typing import Union from typing import overload @@ -64,7 +63,7 @@ def pack_Any(msg): @overload -def unpack_Any(any_msg: any_pb2.Any, msg_class: Type[MessageT]) -> MessageT: +def unpack_Any(any_msg: any_pb2.Any, msg_class: type[MessageT]) -> MessageT: pass @@ -86,13 +85,13 @@ def unpack_Any(any_msg, msg_class): @overload -def parse_Bytes(serialized_bytes: bytes, msg_class: Type[MessageT]) -> MessageT: +def parse_Bytes(serialized_bytes: bytes, msg_class: type[MessageT]) -> MessageT: pass @overload def parse_Bytes( - serialized_bytes: bytes, msg_class: Union[Type[bytes], None]) -> bytes: + serialized_bytes: bytes, msg_class: Union[type[bytes], None]) -> bytes: pass @@ -116,7 +115,7 @@ def pack_Struct(**kwargs) -> struct_pb2.Struct: return msg -def from_micros(cls: Type[TimeMessageT], micros: int) -> TimeMessageT: +def from_micros(cls: type[TimeMessageT], micros: int) -> TimeMessageT: result = cls() if isinstance(result, duration_pb2.Duration): result.FromMicroseconds(micros) diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index ff1a0d9c46aa..988bd680b923 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -32,7 +32,6 @@ import time import zipfile from typing import Any -from typing import Set from urllib.error import URLError from urllib.request import Request from urllib.request import urlopen @@ -49,7 +48,7 @@ @dataclasses.dataclass class _SharedCacheEntry: obj: Any - owners: Set[str] + owners: set[str] class _SharedCache: diff --git a/sdks/python/apache_beam/utils/urns.py b/sdks/python/apache_beam/utils/urns.py index a8074137d178..8732df16db12 100644 --- a/sdks/python/apache_beam/utils/urns.py +++ b/sdks/python/apache_beam/utils/urns.py @@ -26,10 +26,7 @@ from typing import TYPE_CHECKING from typing import Any from typing import Callable -from typing import Dict from typing import Optional -from typing import Tuple -from typing import Type from typing import TypeVar from typing import Union from typing import overload @@ -65,7 +62,7 @@ class RunnerApiFn(object): # classes + abc metaclass # __metaclass__ = abc.ABCMeta - _known_urns: Dict[str, Tuple[Optional[type], ConstructorFn]] = {} + _known_urns: dict[str, tuple[Optional[type], ConstructorFn]] = {} # @abc.abstractmethod is disabled here to avoid an error with mypy. mypy # performs abc.abtractmethod/property checks even if a class does @@ -75,7 +72,7 @@ class RunnerApiFn(object): # concrete implementation. # @abc.abstractmethod def to_runner_api_parameter( - self, unused_context: 'PipelineContext') -> Tuple[str, Any]: + self, unused_context: 'PipelineContext') -> tuple[str, Any]: """Returns the urn and payload for this Fn. The returned urn(s) should be registered with `register_urn`. @@ -87,7 +84,7 @@ def to_runner_api_parameter( def register_urn( cls, urn: str, - parameter_type: Type[T], + parameter_type: type[T], ) -> Callable[[Callable[[T, 'PipelineContext'], Any]], Callable[[T, 'PipelineContext'], Any]]: pass @@ -107,7 +104,7 @@ def register_urn( def register_urn( cls, urn: str, - parameter_type: Type[T], + parameter_type: type[T], fn: Callable[[T, 'PipelineContext'], Any]) -> None: pass @@ -170,7 +167,7 @@ def to_runner_api( @classmethod def from_runner_api( - cls: Type[RunnerApiFnT], + cls: type[RunnerApiFnT], fn_proto: beam_runner_api_pb2.FunctionSpec, context: 'PipelineContext') -> RunnerApiFnT: """Converts from an FunctionSpec to a Fn object. diff --git a/sdks/python/apache_beam/utils/windowed_value.py b/sdks/python/apache_beam/utils/windowed_value.py index 305e66b85940..6a0987611f5c 100644 --- a/sdks/python/apache_beam/utils/windowed_value.py +++ b/sdks/python/apache_beam/utils/windowed_value.py @@ -24,6 +24,7 @@ # pytype: skip-file +# ruff: noqa: UP006 import collections from typing import TYPE_CHECKING from typing import Any diff --git a/sdks/python/apache_beam/yaml/examples/testing/examples_test.py b/sdks/python/apache_beam/yaml/examples/testing/examples_test.py index 82127d1e27aa..97acf427f548 100644 --- a/sdks/python/apache_beam/yaml/examples/testing/examples_test.py +++ b/sdks/python/apache_beam/yaml/examples/testing/examples_test.py @@ -26,8 +26,6 @@ import unittest from typing import Any from typing import Callable -from typing import Dict -from typing import List from typing import Optional from typing import Union from unittest import mock @@ -58,7 +56,7 @@ def test_enrichment( pcoll, enrichment_handler: str, - handler_config: Dict[str, Any], + handler_config: dict[str, Any], timeout: Optional[float] = 30): """ Mocks the Enrichment transform for testing purposes. @@ -163,7 +161,7 @@ def test_pubsub_read( subscription: Optional[str] = None, format: Optional[str] = None, schema: Optional[Any] = None, - attributes: Optional[List[str]] = None, + attributes: Optional[list[str]] = None, attributes_map: Optional[str] = None, id_attribute: Optional[str] = None, timestamp_attribute: Optional[str] = None): @@ -287,7 +285,7 @@ def _format_predicition_result_ouput(pcoll, inference_tag): INPUT_TRANSFORM_TEST_PROVIDERS = ['TestReadFromKafka', 'TestReadFromPubSub'] -def check_output(expected: List[str]): +def check_output(expected: list[str]): """ Helper function to check the output of a pipeline against expected values. @@ -302,7 +300,7 @@ def check_output(expected: List[str]): A callable that takes a list of PCollections and asserts their combined elements match the expected output. """ - def _check_inner(actual: List[PCollection[str]]): + def _check_inner(actual: list[PCollection[str]]): formatted_actual = actual | beam.Flatten() | beam.Map( lambda row: str(beam.Row(**row._asdict()))) assert_matches_stdout(formatted_actual, expected) @@ -312,7 +310,7 @@ def _check_inner(actual: List[PCollection[str]]): def create_test_method( pipeline_spec_file: str, - custom_preprocessors: List[Callable[..., Union[Dict, List]]]): + custom_preprocessors: list[Callable[..., Union[dict, list]]]): """ Generates a test method for a given YAML pipeline specification file. @@ -426,7 +424,7 @@ class YamlExamplesTestSuite: files and dynamically generate a Python test method. Additionally, it creates a method to complete some preprocessing for mocking IO. """ - _test_preprocessor: Dict[str, List[Callable[..., Union[Dict, List]]]] = {} + _test_preprocessor: dict[str, list[Callable[..., Union[dict, list]]]] = {} def __init__(self, name: str, path: str): """ @@ -501,7 +499,7 @@ def create_test_suite(cls, name: str, path: str): name, (unittest.TestCase, ), dict(cls.parse_test_methods(path))) @classmethod - def register_test_preprocessor(cls, test_names: Union[str, List]): + def register_test_preprocessor(cls, test_names: Union[str, list]): """Decorator to register a preprocessor function for specific tests. This decorator is used to associate a preprocessor function with one or @@ -534,7 +532,7 @@ def apply(preprocessor): @YamlExamplesTestSuite.register_test_preprocessor( ['test_wordcount_minimal_yaml']) def _wordcount_minimal_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for the wordcount_minimal.yaml test. @@ -569,7 +567,7 @@ def _wordcount_minimal_test_preprocessor( 'test_wordCountInheritance_yaml' ]) def _wordcount_jinja_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for the wordcount Jinja tests. @@ -599,7 +597,7 @@ def _wordcount_jinja_test_preprocessor( def _wordcount_random_shuffler( - test_spec: dict, all_words: List[str], env: TestEnvironment): + test_spec: dict, all_words: list[str], env: TestEnvironment): """ Helper function to create a randomized input file for wordcount-style tests. @@ -638,7 +636,7 @@ def _wordcount_random_shuffler( @YamlExamplesTestSuite.register_test_preprocessor( ['test_kafka_yaml', 'test_kafka_to_iceberg_yaml']) def _kafka_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): test_spec = replace_recursive( test_spec, @@ -687,7 +685,7 @@ def _kafka_test_preprocessor( 'test_bigquery_write_yaml' ]) def _io_write_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve writing to IO. @@ -724,7 +722,7 @@ def _io_write_test_preprocessor( 'test_gcs_text_to_bigquery_yaml' ]) def _file_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ This preprocessor replaces any file IO ReadFrom transform with a Create transform that reads from a predefined in-memory dictionary. This allows @@ -758,7 +756,7 @@ def _file_io_read_test_preprocessor( @YamlExamplesTestSuite.register_test_preprocessor( ['test_iceberg_read_yaml', 'test_iceberg_to_alloydb_yaml']) def _iceberg_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from Iceberg. @@ -804,7 +802,7 @@ def _iceberg_io_read_test_preprocessor( 'test_spanner_to_bigquery_yaml' ]) def _spanner_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from Spanner. @@ -855,7 +853,7 @@ def _spanner_io_read_test_preprocessor( @YamlExamplesTestSuite.register_test_preprocessor( ['test_bigtable_enrichment_yaml', 'test_enrich_spanner_with_bigquery_yaml']) def _enrichment_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve the Enrichment transform. @@ -887,7 +885,7 @@ def _enrichment_test_preprocessor( 'test_pubsub_to_iceberg_yaml' ]) def _pubsub_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from Pub/Sub. This preprocessor replaces any ReadFromPubSub transform with a Create @@ -908,7 +906,7 @@ def _pubsub_io_read_test_preprocessor( 'test_jdbc_to_bigquery_yaml', ]) def _jdbc_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from generic Jdbc. url syntax: 'jdbc:://:/' @@ -921,7 +919,7 @@ def _jdbc_io_read_test_preprocessor( 'test_sqlserver_to_bigquery_yaml', ]) def __sqlserver_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from SqlServer. url syntax: 'jdbc:sqlserver://:;databaseName=; @@ -935,7 +933,7 @@ def __sqlserver_io_read_test_preprocessor( 'test_postgres_to_bigquery_yaml', ]) def __postgres_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from Postgres. url syntax: 'jdbc:postgresql://:/shipment?user=& @@ -949,7 +947,7 @@ def __postgres_io_read_test_preprocessor( 'test_oracle_to_bigquery_yaml', ]) def __oracle_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from Oracle. url syntax: 'jdbc:oracle:thin:system/oracle@:{port}/' @@ -962,7 +960,7 @@ def __oracle_io_read_test_preprocessor( 'test_mysql_to_bigquery_yaml', ]) def __mysql_io_read_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve reading from MySql. url syntax: 'jdbc:mysql://:/?user=& @@ -1014,7 +1012,7 @@ def _db_io_read_test_processor( @YamlExamplesTestSuite.register_test_preprocessor( 'test_streaming_sentiment_analysis_yaml') def _streaming_sentiment_analysis_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve the streaming sentiment analysis example. @@ -1072,7 +1070,7 @@ def _streaming_sentiment_analysis_test_preprocessor( @YamlExamplesTestSuite.register_test_preprocessor( 'test_streaming_taxifare_prediction_yaml') def _streaming_taxifare_prediction_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve the streaming taxi fare prediction example. @@ -1158,7 +1156,7 @@ def _streaming_taxifare_prediction_test_preprocessor( 'test_anomaly_scoring_yaml' ]) def _batch_log_analysis_test_preprocessor( - test_spec: dict, expected: List[str], env: TestEnvironment): + test_spec: dict, expected: list[str], env: TestEnvironment): """ Preprocessor for tests that involve the batch log analysis example. diff --git a/sdks/python/apache_beam/yaml/yaml_testing.py b/sdks/python/apache_beam/yaml/yaml_testing.py index 10d23dd54868..c7f5f5f4e93c 100644 --- a/sdks/python/apache_beam/yaml/yaml_testing.py +++ b/sdks/python/apache_beam/yaml/yaml_testing.py @@ -21,11 +21,8 @@ import random import unittest import uuid -from typing import Dict -from typing import List from typing import Mapping from typing import Optional -from typing import Tuple from typing import TypeVar from typing import Union @@ -539,11 +536,11 @@ def _first_n(transform_spec, options, n, providers=None): K2 = TypeVar('K2') V = TypeVar('V') -InputsType = Dict[str, Union[str, List[str]]] +InputsType = dict[str, Union[str, list[str]]] def _composite_key_to_nested( - d: Mapping[Tuple[K1, K2], V]) -> Mapping[K1, Mapping[K2, V]]: + d: Mapping[tuple[K1, K2], V]) -> Mapping[K1, Mapping[K2, V]]: nested = collections.defaultdict(dict) for (k1, k2), v in d.items(): nested[k1][k2] = v diff --git a/sdks/python/apache_beam/yaml/yaml_utils.py b/sdks/python/apache_beam/yaml/yaml_utils.py index 6e8a97c8b4f8..7a7d985f8327 100644 --- a/sdks/python/apache_beam/yaml/yaml_utils.py +++ b/sdks/python/apache_beam/yaml/yaml_utils.py @@ -22,7 +22,6 @@ from collections.abc import Iterable from collections.abc import Mapping from typing import Any -from typing import Tuple import yaml from yaml import SafeLoader @@ -150,7 +149,7 @@ def wrapper(self, node): # This (recursively) finds the portion of the original string that must # be replaced with new content. - def diff(a: Any, b: Any) -> Iterable[Tuple[int, int, str]]: + def diff(a: Any, b: Any) -> Iterable[tuple[int, int, str]]: if a == b: return elif (isinstance(a, dict) and isinstance(b, dict) and diff --git a/sdks/python/gen_managed_doc.py b/sdks/python/gen_managed_doc.py index 75301d6a7bb5..d492caba23bc 100644 --- a/sdks/python/gen_managed_doc.py +++ b/sdks/python/gen_managed_doc.py @@ -22,7 +22,6 @@ import argparse import os import re -from typing import Dict import yaml from gen_protos import PROJECT_ROOT @@ -125,7 +124,7 @@ def generate_managed_doc(output_location): for gradle_target in expansion_service_jar_targets: provider = ExternalTransformProvider(BeamJarExpansionService(gradle_target)) - discovered: Dict[str, ExternalTransform] = provider.get_all() + discovered: dict[str, ExternalTransform] = provider.get_all() for identifier, transform in discovered.items(): if identifier in read_names_and_identifiers.values(): diff --git a/sdks/python/gen_xlang_wrappers.py b/sdks/python/gen_xlang_wrappers.py index 3176b74e836a..62d30042fc04 100644 --- a/sdks/python/gen_xlang_wrappers.py +++ b/sdks/python/gen_xlang_wrappers.py @@ -29,8 +29,6 @@ import subprocess import typing from typing import Any -from typing import Dict -from typing import List from typing import Union import yaml @@ -117,7 +115,7 @@ class name. This can be overriden by manually providing a name. from apache_beam.transforms.external_transform_provider import ExternalTransform from apache_beam.transforms.external_transform_provider import ExternalTransformProvider - transform_list: List[Dict[str, Any]] = [] + transform_list: list[dict[str, Any]] = [] with open(input_services) as f: services = yaml.safe_load(f) @@ -128,7 +126,7 @@ class name. This can be overriden by manually providing a name. raise ValueError( f"Expansion service with target '{target}' does not " "specify any default destinations.") - service_destinations: Dict[str, str] = service['destinations'] + service_destinations: dict[str, str] = service['destinations'] for sdk, dest in service_destinations.items(): validate_sdks_destinations(sdk, dest, target) @@ -136,7 +134,7 @@ class name. This can be overriden by manually providing a name. # use dynamic provider to discover and populate wrapper details provider = ExternalTransformProvider(BeamJarExpansionService(target)) - discovered: Dict[str, ExternalTransform] = provider.get_all() + discovered: dict[str, ExternalTransform] = provider.get_all() for identifier in sorted(discovered.keys()): wrapper = discovered[identifier] if identifier in transforms_to_skip: @@ -227,7 +225,11 @@ def pretty_type(tp): # TODO(ahmedabu98): Make this more generic to support other remote SDKs # Potentially use Runner API types if tp.__module__ == 'builtins': - tp = tp.__name__ + if getattr(tp, '__origin__', None) is None: + tp = tp.__name__ + else: + # Remove nested typing module name (like Optional) + tp = str(tp).replace("typing.", "") elif tp.__module__ == 'typing': tp = str(tp).replace("typing.", "") tp = tp.replace("Sequence", "list") @@ -242,7 +244,7 @@ def pretty_type(tp): return (tp, nullable) -def get_wrappers_from_transform_configs(config_file) -> Dict[str, List[str]]: +def get_wrappers_from_transform_configs(config_file) -> dict[str, list[str]]: """ Generates code for external transform wrapper classes (subclasses of :class:`ExternalTransform`). @@ -264,7 +266,7 @@ def get_wrappers_from_transform_configs(config_file) -> Dict[str, List[str]]: # maintain a list of wrappers to write in each file. if modified destinations # are used, we may end up with multiple wrappers in one file. - destinations: Dict[str, List[str]] = {} + destinations: dict[str, list[str]] = {} with open(config_file) as f: transforms = yaml.safe_load(f) @@ -308,7 +310,7 @@ def get_wrappers_from_transform_configs(config_file) -> Dict[str, List[str]]: def write_wrappers_to_destinations( - grouped_wrappers: Dict[str, List[str]], + grouped_wrappers: dict[str, list[str]], output_dir=PY_WRAPPER_OUTPUT_DIR, format_code=True): """ diff --git a/sdks/python/ruff.toml b/sdks/python/ruff.toml index 5a862e013f12..c0f896c21721 100644 --- a/sdks/python/ruff.toml +++ b/sdks/python/ruff.toml @@ -56,7 +56,7 @@ target-version = "py310" src = ["apache_beam"] [lint] -select = ["E9", "PL", "F821", "F822", "F823"] +select = ["E9", "PL", "F821", "F822", "F823", "UP006"] ignore = [ # Ignored Pylint Checks "PLC0415", # import-outside-toplevel From c38d13c279b713725972c03cb7ae13deaa8678bf Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Tue, 5 May 2026 07:47:01 -0700 Subject: [PATCH 074/490] Add pipeline hash (#38357) * Add Pipeline Options * Add Pipeline Hash along with URL * fix go lang staging * Fix formatting * backmerge master * remove cache * fix test * fix proto hash creation * fix lint * move pipeline hash to sdkharness * fix spotless --- .../beam/runners/dataflow/DataflowRunner.java | 3 ++ sdks/go/pkg/beam/runners/dataflow/dataflow.go | 8 ++- .../runners/dataflow/dataflowlib/execute.go | 9 +++- .../beam/runners/dataflow/dataflowlib/job.go | 12 +++-- .../runners/dataflow/dataflowlib/job_test.go | 51 ++++++++++++++++++- .../beam/sdk/options/SdkHarnessOptions.java | 6 +++ .../runners/dataflow/internal/apiclient.py | 13 +++-- .../dataflow/internal/apiclient_test.py | 35 +++++++++++++ 8 files changed, 125 insertions(+), 12 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index b375de661885..ecc231ab825e 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1368,6 +1368,9 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.getStager().stageToFile(serializedProtoPipeline, PIPELINE_FILE_NAME); dataflowOptions.setPipelineUrl(stagedPipeline.getLocation()); + String pipelineProtoHash = Hashing.sha256().hashBytes(serializedProtoPipeline).toString(); + options.as(SdkHarnessOptions.class).setPipelineProtoHash(pipelineProtoHash); + if (useUnifiedWorker(options)) { LOG.info("Skipping v1 transform replacements since job will run on v2."); } else { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow.go b/sdks/go/pkg/beam/runners/dataflow/dataflow.go index ecbfe53939ec..e968911fcca1 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow.go @@ -24,6 +24,8 @@ package dataflow import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "flag" "fmt" @@ -40,6 +42,7 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/pipelinex" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/hooks" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/protox" "github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors" "github.com/apache/beam/sdks/v2/go/pkg/beam/log" "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts" @@ -239,7 +242,10 @@ func Execute(ctx context.Context, p *beam.Pipeline) (beam.PipelineResult, error) log.Info(ctx, "Dry-run: not submitting job!") log.Info(ctx, model.String()) - job, err := dataflowlib.Translate(ctx, model, opts, workerURL, modelURL) + modelBytes := protox.MustEncode(model) + hash := sha256.Sum256(modelBytes) + pipelineProtoHash := hex.EncodeToString(hash[:]) + job, err := dataflowlib.Translate(ctx, model, opts, workerURL, modelURL, pipelineProtoHash) if err != nil { return nil, err } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go index 806b8940ae99..396eefab7318 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go @@ -19,6 +19,8 @@ package dataflowlib import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "os" "strings" @@ -83,14 +85,17 @@ func Execute(ctx context.Context, raw *pipepb.Pipeline, opts *JobOptions, worker // (2) Upload model to GCS log.Info(ctx, raw.String()) - if err := StageModel(ctx, opts.Project, modelURL, protox.MustEncode(raw)); err != nil { + modelBytes := protox.MustEncode(raw) + modelHash := sha256.Sum256(modelBytes) + pipelineProtoHash := hex.EncodeToString(modelHash[:]) + if err := StageModel(ctx, opts.Project, modelURL, modelBytes); err != nil { return presult, err } log.Infof(ctx, "Staged model pipeline: %v", modelURL) // (3) Translate to v1b3 and submit - job, err := Translate(ctx, raw, opts, workerURL, modelURL) + job, err := Translate(ctx, raw, opts, workerURL, modelURL, pipelineProtoHash) if err != nil { return presult, err } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go index 2f8057b6d506..f0adb21cf714 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go @@ -117,7 +117,7 @@ func containerImages(p *pipepb.Pipeline) ([]*df.SdkHarnessContainerImage, []stri } // Translate translates a pipeline to a Dataflow job. -func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, workerURL, modelURL string) (*df.Job, error) { +func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, workerURL, modelURL string, pipelineProtoHash string) (*df.Job, error) { // (1) Translate pipeline to v1b3 speak. jobType := "JOB_TYPE_BATCH" @@ -181,10 +181,11 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker SdkPipelineOptions: newMsg(pipelineOptions{ DisplayData: printOptions(opts, images), Options: dataflowOptions{ - PipelineURL: modelURL, - Region: opts.Region, - Experiments: opts.Experiments, - TempLocation: opts.TempLocation, + PipelineURL: modelURL, + PipelineProtoHash: pipelineProtoHash, + Region: opts.Region, + Experiments: opts.Experiments, + TempLocation: opts.TempLocation, }, GoOptions: opts.Options, }), @@ -359,6 +360,7 @@ func GetMetrics(ctx context.Context, client *df.Service, project, region, jobID type dataflowOptions struct { Experiments []string `json:"experiments,omitempty"` PipelineURL string `json:"pipelineUrl"` + PipelineProtoHash string `json:"pipelineProtoHash,omitempty"` Region string `json:"region"` TempLocation string `json:"tempLocation"` DiskProvisionedIops int64 `json:"diskProvisionedIops"` diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go index 303fcb776bff..901adb6c7b72 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go @@ -21,6 +21,9 @@ import ( "reflect" "testing" + "encoding/json" + + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/protox" pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1" @@ -293,7 +296,7 @@ func TestTranslate(t *testing.T) { workerURL := "gs://any-location/temp" modelURL := "gs://any-location/temp" - job, err := Translate(ctx, p, opts, workerURL, modelURL) + job, err := Translate(ctx, p, opts, workerURL, modelURL, "dummy-hash-12345") if err != nil { t.Fatalf("Translate(...) error = %v, want nil", err) } @@ -310,3 +313,49 @@ func TestTranslate(t *testing.T) { t.Errorf("DiskProvisionedThroughputMibps = %v, want 200", wp.DiskProvisionedThroughputMibps) } } + +func TestTranslateWithPipelineHash(t *testing.T) { + p := &pipepb.Pipeline{ + Components: &pipepb.Components{ + Environments: map[string]*pipepb.Environment{ + "env1": { + Payload: protox.MustEncode(&pipepb.DockerPayload{ + ContainerImage: "dummy_image", + }), + }, + }, + }, + } + opts := &JobOptions{ + Name: "test-job", + Project: "test-project", + Region: "test-region", + Options: runtime.RawOptions{ + Options: make(map[string]string), + }, + } + + expectedHashStr := "dummy-hash-12345" + + job, err := Translate(context.Background(), p, opts, "worker-url", "model-url", expectedHashStr) + if err != nil { + t.Fatalf("Translate failed: %v", err) + } + + // Verify PipelineProtoHash + var recoveredOptions struct { + Options struct { + PipelineURL string `json:"pipelineUrl"` + PipelineProtoHash string `json:"pipelineProtoHash"` + } `json:"options"` + } + + rawOpts := job.Environment.SdkPipelineOptions + if err := json.Unmarshal(rawOpts, &recoveredOptions); err != nil { + t.Fatalf("Failed to unmarshal SdkPipelineOptions: %v", err) + } + + if recoveredOptions.Options.PipelineProtoHash != expectedHashStr { + t.Errorf("Expected PipelineProtoHash %v, got %v", expectedHashStr, recoveredOptions.Options.PipelineProtoHash) + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/SdkHarnessOptions.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/SdkHarnessOptions.java index 831dd69ec95f..7267dda9ed0b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/SdkHarnessOptions.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/SdkHarnessOptions.java @@ -481,4 +481,10 @@ public OpenTelemetry create(PipelineOptions options) { return GlobalOpenTelemetry.get(); } } + + /** The hex-encoded SHA256 hash of the staged portable pipeline proto. */ + @Description("The hex-encoded SHA256 hash of the staged portable pipeline proto") + String getPipelineProtoHash(); + + void setPipelineProtoHash(String hash); } diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 29cb36071488..f38a2ee34bbf 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -97,7 +97,8 @@ def __init__( options, environment_version, proto_pipeline_staged_url, - proto_pipeline=None): + proto_pipeline=None, + pipeline_proto_hash=None): self.standard_options = options.view_as(StandardOptions) self.google_cloud_options = options.view_as(GoogleCloudOptions) self.worker_options = options.view_as(WorkerOptions) @@ -279,6 +280,8 @@ def __init__( for k, v in sdk_pipeline_options.items() if v is not None } options_dict["pipelineUrl"] = proto_pipeline_staged_url + if pipeline_proto_hash: + options_dict["pipelineProtoHash"] = pipeline_proto_hash # Don't pass impersonate_service_account through to the harness. # Though impersonation should start a job, the workers should # not try to modify their credentials. @@ -831,10 +834,13 @@ def create_job_description(self, job): resources = self._stage_resources(job.proto_pipeline, job.options) # Stage proto pipeline. + serialized_pipeline = job.proto_pipeline.SerializeToString() + pipeline_proto_hash = hashlib.sha256(serialized_pipeline).hexdigest() + self.stage_file_with_retry( job.google_cloud_options.staging_location, shared_names.STAGED_PIPELINE_FILENAME, - io.BytesIO(job.proto_pipeline.SerializeToString())) + io.BytesIO(serialized_pipeline)) job.proto.environment = Environment( proto_pipeline_staged_url=FileSystems.join( @@ -843,7 +849,8 @@ def create_job_description(self, job): packages=resources, options=job.options, environment_version=self.environment_version, - proto_pipeline=job.proto_pipeline).proto + proto_pipeline=job.proto_pipeline, + pipeline_proto_hash=pipeline_proto_hash).proto _LOGGER.debug('JOB: %s', job) @retry.with_exponential_backoff(num_retries=3, initial_delay_secs=3) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index 66b1c8e1e5bb..43f4c8a21513 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +import hashlib import io import itertools import json @@ -97,6 +98,40 @@ def test_pipeline_url(self): self.assertEqual(pipeline_url.string_value, FAKE_PIPELINE_URL) + def test_pipeline_proto_hash(self): + pipeline_options = PipelineOptions( + ['--temp_location', 'gs://any-location/temp']) + proto_pipeline = beam_runner_api_pb2.Pipeline() + proto_pipeline.components.transforms['dummy'].unique_name = 'dummy' + + expected_hash = hashlib.sha256( + proto_pipeline.SerializeToString()).hexdigest() + + env = apiclient.Environment([], + pipeline_options, + '2.0.0', + FAKE_PIPELINE_URL, + proto_pipeline, + pipeline_proto_hash=expected_hash) + + recovered_options = None + for additionalProperty in env.proto.sdkPipelineOptions.additionalProperties: + if additionalProperty.key == 'options': + recovered_options = additionalProperty.value + break + else: + self.fail('No pipeline options found') + + pipeline_proto_hash = None + for property in recovered_options.object_value.properties: + if property.key == 'pipelineProtoHash': + pipeline_proto_hash = property.value + break + else: + self.fail('No pipelineProtoHash found') + + self.assertEqual(pipeline_proto_hash.string_value, expected_hash) + def test_set_network(self): pipeline_options = PipelineOptions([ '--network', From ae910b4d481ed6d3e4efce418c83e23f3228c1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Tue, 5 May 2026 17:09:00 +0200 Subject: [PATCH 075/490] fix error prone. (#38372) --- .../apache/beam/sdk/values/ValueInSingleWindow.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java index 4cb8b920d64f..afd5aa845200 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java @@ -119,12 +119,14 @@ public static Coder of( @Override public void encode(ValueInSingleWindow windowedElem, OutputStream outStream) throws IOException { - encode(windowedElem, outStream, Coder.Context.NESTED); + encode(windowedElem, outStream, org.apache.beam.sdk.coders.Coder.Context.NESTED); } @Override public void encode( - ValueInSingleWindow windowedElem, OutputStream outStream, Coder.Context context) + ValueInSingleWindow windowedElem, + OutputStream outStream, + org.apache.beam.sdk.coders.Coder.Context context) throws IOException { InstantCoder.of().encode(windowedElem.getTimestamp(), outStream); windowCoder.encode(windowedElem.getWindow(), outStream); @@ -154,13 +156,13 @@ public void encode( @Override public ValueInSingleWindow decode(InputStream inStream) throws IOException { - return decode(inStream, Coder.Context.NESTED); + return decode(inStream, org.apache.beam.sdk.coders.Coder.Context.NESTED); } @Override @SuppressWarnings("IgnoredPureGetter") - public ValueInSingleWindow decode(InputStream inStream, Coder.Context context) - throws IOException { + public ValueInSingleWindow decode( + InputStream inStream, org.apache.beam.sdk.coders.Coder.Context context) throws IOException { Instant timestamp = InstantCoder.of().decode(inStream); BoundedWindow window = windowCoder.decode(inStream); PaneInfo paneInfo = PaneInfo.PaneInfoCoder.INSTANCE.decode(inStream); From 1a3b1ec1fe694059a72370cdcc10cd3f175691af Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Tue, 5 May 2026 14:56:14 -0400 Subject: [PATCH 076/490] extend to yaml (#38371) --- sdks/python/apache_beam/yaml/yaml_io.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/yaml/yaml_io.py b/sdks/python/apache_beam/yaml/yaml_io.py index 336e32adc253..92f8ec47cca7 100644 --- a/sdks/python/apache_beam/yaml/yaml_io.py +++ b/sdks/python/apache_beam/yaml/yaml_io.py @@ -563,6 +563,7 @@ def write_to_iceberg( drop: Optional[Iterable[str]] = None, only: Optional[str] = None, distribution_mode: Optional[str] = None, + autosharding: Optional[bool] = None, ): # TODO(robertwb): It'd be nice to derive this list of parameters, along with # their types and docs, programmatically from the iceberg (or managed) @@ -616,6 +617,11 @@ def write_to_iceberg( distributions: - none: don't shuffle rows (default) - hash: shuffle rows by partition key before writing data + autosharding: Enables dynamic sharding to automatically adjust the number + of parallel writers based on data volume. It handles data skew by + further sub-dividing partitions into multiple shards to prevent + bottlenecks during high-throughput writes. Only available with 'hash' + distribution mode. """ return beam.managed.Write( "iceberg", @@ -630,7 +636,8 @@ def write_to_iceberg( keep=keep, drop=drop, only=only, - distribution_mode=distribution_mode)) + distribution_mode=distribution_mode, + autosharding=autosharding)) def io_providers(): From 35c2f1319429b4f906a09c0b76f604218ea8011f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 09:01:47 -0400 Subject: [PATCH 077/490] Bump minimatch in /scripts/ci/pr-bot (#37732) Bumps and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together. Updates `minimatch` from 9.0.5 to 9.0.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9) Updates `minimatch` from 5.1.6 to 5.1.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9) --- updated-dependencies: - dependency-name: minimatch dependency-version: 5.1.9 dependency-type: indirect - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/ci/pr-bot/package-lock.json | 43 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/scripts/ci/pr-bot/package-lock.json b/scripts/ci/pr-bot/package-lock.json index d1220a65482d..7f32478d08cd 100644 --- a/scripts/ci/pr-bot/package-lock.json +++ b/scripts/ci/pr-bot/package-lock.json @@ -292,11 +292,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -689,13 +688,12 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -876,11 +874,10 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1827,9 +1824,9 @@ "dev": true }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -2088,12 +2085,12 @@ }, "dependencies": { "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } } } @@ -2222,9 +2219,9 @@ "dev": true }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" From e885c19a4f03bb815db3abe3ae5d07e3a578f297 Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Tue, 5 May 2026 13:08:32 +0200 Subject: [PATCH 078/490] Refactor metadata propagation in ReduceFnRunner to support extensible PipelineMetadata --- .../beam/runners/core/CombinedMetadata.java | 87 +++++++++++++++++ .../core/CombinedMetadataCombiner.java | 78 +++++++++++++++ .../beam/runners/core/MetadataCombiner.java | 25 +++++ .../beam/runners/core/ReduceFnRunner.java | 55 ++++++++--- .../runners/core/CombinedMetadataTest.java | 70 ++++++++++++++ .../beam/runners/core/ReduceFnTester.java | 5 +- .../worker/StreamingDataflowWorkerTest.java | 37 +++++++- .../beam/sdk/values/WindowedValues.java | 4 + .../transforms/MetadataPropagationTest.java | 95 +++++++++++++++++-- 9 files changed, 428 insertions(+), 28 deletions(-) create mode 100644 runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java create mode 100644 runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java create mode 100644 runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java create mode 100644 runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java new file mode 100644 index 000000000000..bdce67f6bad5 --- /dev/null +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +import com.google.auto.value.AutoValue; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.beam.model.fnexecution.v1.BeamFnApi; +import org.apache.beam.sdk.coders.AtomicCoder; +import org.apache.beam.sdk.values.CausedByDrain; + +/** + * Encapsulates metadata that propagates with elements in the pipeline. + * + *

    This metadata is sent along with elements. It currently includes fields like {@link + * CausedByDrain}, and is designed to be extensible to support future metadata fields such as + * OpenTelemetry context or CDC (Change Data Capture) kind. + * + *

    The purpose of this class is to group targeted metadata fields together. This makes it easier + * to define combination strategies (e.g., when accumulating state in {@code ReduceFnRunner}) when + * multiple elements are merged or grouped, without having to extend method signatures or state + * handling for every new metadata field. + */ +@AutoValue +public abstract class CombinedMetadata { + public abstract CausedByDrain causedByDrain(); + + public static CombinedMetadata create(CausedByDrain causedByDrain) { + return new AutoValue_CombinedMetadata(causedByDrain); + } + + public static CombinedMetadata createDefault() { + return create(CausedByDrain.NORMAL); + } + + public static class Coder extends AtomicCoder { + private static final Coder INSTANCE = new Coder(); + + public static Coder of() { + return INSTANCE; + } + + @Override + public void encode(CombinedMetadata value, OutputStream outStream) throws IOException { + BeamFnApi.Elements.ElementMetadata proto = + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setDrain( + value.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN + ? BeamFnApi.Elements.DrainMode.Enum.DRAINING + : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .build(); + proto.writeDelimitedTo(outStream); + } + + @Override + public CombinedMetadata decode(InputStream inStream) throws IOException { + BeamFnApi.Elements.ElementMetadata proto = + BeamFnApi.Elements.ElementMetadata.parseDelimitedFrom(inStream); + if (proto == null) { + return CombinedMetadata.createDefault(); + } + + CausedByDrain causedByDrain = + proto.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING + ? CausedByDrain.CAUSED_BY_DRAIN + : CausedByDrain.NORMAL; + + return CombinedMetadata.create(causedByDrain); + } + } +} diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java new file mode 100644 index 000000000000..a2f3f26520ef --- /dev/null +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +import org.apache.beam.sdk.transforms.Combine.CombineFn; +import org.apache.beam.sdk.values.CausedByDrain; + +/** Combiner for CombinedMetadata. */ +class CombinedMetadataCombiner + extends CombineFn { + private static final CombinedMetadataCombiner INSTANCE = new CombinedMetadataCombiner(); + + public static CombinedMetadataCombiner of() { + return INSTANCE; + } + + @Override + public CombinedMetadata createAccumulator() { + return CombinedMetadata.create(CausedByDrainCombiner.of().createAccumulator()); + } + + @Override + public CombinedMetadata addInput(CombinedMetadata accumulator, CombinedMetadata input) { + return CombinedMetadata.create( + CausedByDrainCombiner.of().addInput(accumulator.causedByDrain(), input.causedByDrain())); + } + + @Override + public CombinedMetadata mergeAccumulators(Iterable accumulators) { + CombinedMetadata result = createAccumulator(); + for (CombinedMetadata accum : accumulators) { + result = addInput(result, accum); + } + return result; + } + + @Override + public CombinedMetadata extractOutput(CombinedMetadata accumulator) { + return accumulator; + } + + /** Combiner for CausedByDrain metadata. */ + static class CausedByDrainCombiner implements MetadataCombiner { + private static final CausedByDrainCombiner INSTANCE = new CausedByDrainCombiner(); + + public static CausedByDrainCombiner of() { + return INSTANCE; + } + + @Override + public CausedByDrain createAccumulator() { + return CausedByDrain.NORMAL; + } + + @Override + public CausedByDrain addInput(CausedByDrain current, CausedByDrain input) { + if (current == CausedByDrain.CAUSED_BY_DRAIN || input == CausedByDrain.CAUSED_BY_DRAIN) { + return CausedByDrain.CAUSED_BY_DRAIN; + } + return CausedByDrain.NORMAL; + } + } +} diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java new file mode 100644 index 000000000000..55884a8e43a8 --- /dev/null +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +/** Interface for combining pipeline metadata. */ +interface MetadataCombiner { + T createAccumulator(); + + T addInput(T accumulator, T input); +} diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java index 0721ddc4685e..1ae0c52f853a 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java @@ -38,6 +38,7 @@ import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.state.CombiningState; import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -107,6 +108,12 @@ public class ReduceFnRunner { *

  • It uses discarding or accumulation mode according to the {@link WindowingStrategy}. * */ + static final StateTag> + METADATA_TAG = + StateTags.makeSystemTagInternal( + StateTags.combiningValue( + "combinedMetadata", CombinedMetadata.Coder.of(), CombinedMetadataCombiner.of())); + private final WindowingStrategy windowingStrategy; private final WindowedValueReceiver> outputter; @@ -376,7 +383,7 @@ public void processElements(Iterable> values) throws Excep emit( contextFactory.base(window, StateStyle.DIRECT), contextFactory.base(window, StateStyle.RENAMED), - CausedByDrain.NORMAL); + CombinedMetadata.createDefault()); } // We're all done with merging and emitting elements so can compress the activeWindow state. @@ -590,6 +597,7 @@ private void processElement(Map windowToMergeResult, WindowedValue value.getTimestamp(), StateStyle.DIRECT, value.causedByDrain()); + if (triggerRunner.isClosed(directContext.state())) { // This window has already been closed. droppedDueToClosedWindow.inc(); @@ -604,6 +612,11 @@ private void processElement(Map windowToMergeResult, WindowedValue continue; } + CombinedMetadata metadata = CombinedMetadata.create(value.causedByDrain()); + if (!metadata.equals(CombinedMetadata.createDefault())) { + directContext.state().access(METADATA_TAG).add(metadata); + } + activeWindows.ensureWindowIsActive(window); ReduceFn.ProcessValueContext renamedContext = contextFactory.forValue( @@ -649,15 +662,15 @@ private class WindowActivation { // garbage collect the window. We'll consider any timer at or after the // end-of-window time to be a signal to garbage collect. public final boolean isGarbageCollection; - public final CausedByDrain causedByDrain; + public final CombinedMetadata combinedMetadata; WindowActivation( ReduceFn.Context directContext, ReduceFn.Context renamedContext, - CausedByDrain causedByDrain) { + CombinedMetadata combinedMetadata) { this.directContext = directContext; this.renamedContext = renamedContext; - this.causedByDrain = causedByDrain; + this.combinedMetadata = combinedMetadata; W window = directContext.window(); // The output watermark is before the end of the window if it is either unknown @@ -742,7 +755,8 @@ public void onTimers(Iterable timers) throws Exception { ReduceFn.Context renamedContext = contextFactory.base(window, StateStyle.RENAMED); WindowActivation windowActivation = - new WindowActivation(directContext, renamedContext, timer.causedByDrain()); + new WindowActivation( + directContext, renamedContext, CombinedMetadata.create(timer.causedByDrain())); windowActivations.put(window, windowActivation); // Perform prefetching of state to determine if the trigger should fire. @@ -778,7 +792,7 @@ public void onTimers(Iterable timers) throws Exception { directContext.window(), timerInternals.currentInputWatermarkTime(), timerInternals.currentOutputWatermarkTime(), - windowActivation.causedByDrain); + windowActivation.combinedMetadata.causedByDrain()); boolean windowIsActiveAndOpen = windowActivation.windowIsActiveAndOpen(); if (windowIsActiveAndOpen) { @@ -792,7 +806,7 @@ public void onTimers(Iterable timers) throws Exception { renamedContext, true /* isFinished */, windowActivation.isEndOfWindow, - windowActivation.causedByDrain); + windowActivation.combinedMetadata); checkState(newHold == null, "Hold placed at %s despite isFinished being true.", newHold); } @@ -810,7 +824,7 @@ public void onTimers(Iterable timers) throws Exception { if (windowActivation.windowIsActiveAndOpen() && triggerRunner.shouldFire( directContext.window(), directContext.timers(), directContext.state())) { - emit(directContext, renamedContext, windowActivation.causedByDrain); + emit(directContext, renamedContext, windowActivation.combinedMetadata); } if (windowActivation.isEndOfWindow) { @@ -874,6 +888,7 @@ private void clearAllState( triggerRunner.clearState( directContext.window(), directContext.timers(), directContext.state()); paneInfoTracker.clear(directContext.state()); + directContext.state().access(METADATA_TAG).clear(); } else { // If !windowIsActiveAndOpen then !activeWindows.isActive (1) or triggerRunner.isClosed (2). // For (1), if !activeWindows.isActive then the window must be merging and has been @@ -934,8 +949,9 @@ private void prefetchEmit( private void emit( ReduceFn.Context directContext, ReduceFn.Context renamedContext, - CausedByDrain causedByDrain) + CombinedMetadata metadata) throws Exception { + checkState( triggerRunner.shouldFire( directContext.window(), directContext.timers(), directContext.state())); @@ -950,13 +966,14 @@ private void emit( // Run onTrigger to produce the actual pane contents. // As a side effect it will clear all element holds, but not necessarily any // end-of-window or garbage collection holds. - onTrigger(directContext, renamedContext, isFinished, false /*isEndOfWindow*/, causedByDrain); + onTrigger(directContext, renamedContext, isFinished, false /*isEndOfWindow*/, metadata); // Now that we've triggered, the pane is empty. nonEmptyPanes.clearPane(renamedContext.state()); // Cleanup buffered data if appropriate if (shouldDiscard) { + directContext.state().access(METADATA_TAG).clear(); // Cleanup flavor C: The user does not want any buffered data to persist between panes. reduceFn.clearState(renamedContext); } @@ -1002,6 +1019,7 @@ private void prefetchOnTrigger( /** * Run the {@link ReduceFn#onTrigger} method and produce any necessary output. * + * @param metadata from timer or default * @return output watermark hold added, or {@literal null} if none. */ private @Nullable Instant onTrigger( @@ -1009,8 +1027,19 @@ private void prefetchOnTrigger( ReduceFn.Context renamedContext, final boolean isFinished, boolean isEndOfWindow, - CausedByDrain causedByDrain) + CombinedMetadata metadata) throws Exception { + CombiningState metadataState = + directContext.state().access(METADATA_TAG); + CombinedMetadata aggregatedMetadata = metadataState.read(); + CombinedMetadata fullyAggregatedMetadata = + CombinedMetadataCombiner.of().addInput(aggregatedMetadata, metadata); + final CausedByDrain aggregatedCausedByDrain = fullyAggregatedMetadata.causedByDrain(); + if (isFinished) { + metadataState.clear(); + } else if (!metadata.equals(CombinedMetadata.createDefault())) { + metadataState.add(metadata); + } // Extract the window hold, and as a side effect clear it. final WatermarkHold.OldAndNewHolds pair = watermarkHold.extractAndRelease(renamedContext, isFinished).read(); @@ -1081,12 +1110,12 @@ private void prefetchOnTrigger( .setValue(KV.of(key, toOutput)) .setTimestamp(outputTimestamp) .setWindows(windows) - .setCausedByDrain(causedByDrain) + .setCausedByDrain(aggregatedCausedByDrain) .setPaneInfo(paneInfo) .setReceiver(outputter) .output(); }, - causedByDrain); + aggregatedCausedByDrain); reduceFn.onTrigger(renamedTriggerContext); } diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java new file mode 100644 index 000000000000..29d3efe53b03 --- /dev/null +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import org.apache.beam.sdk.values.CausedByDrain; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CombinedMetadata}. */ +@RunWith(JUnit4.class) +public class CombinedMetadataTest { + + @Test + public void testCoderEncodeDecode() throws Exception { + CombinedMetadata metadata = CombinedMetadata.create(CausedByDrain.CAUSED_BY_DRAIN); + CombinedMetadata.Coder coder = CombinedMetadata.Coder.of(); + + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + coder.encode(metadata, outStream); + + ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray()); + CombinedMetadata decoded = coder.decode(inStream); + + assertEquals(metadata, decoded); + } + + @Test + public void testCoderDecodeEOF() throws Exception { + CombinedMetadata.Coder coder = CombinedMetadata.Coder.of(); + + // Stream with no data (EOF immediately) + ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[0]); + CombinedMetadata decoded = coder.decode(inStream); + + assertEquals(CombinedMetadata.createDefault(), decoded); + } + + @Test + public void testCoderDecodeEmptyMessage() throws Exception { + CombinedMetadata.Coder coder = CombinedMetadata.Coder.of(); + + // Stream with a 0-length delimited message + ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[] {0}); + CombinedMetadata decoded = coder.decode(inStream); + + // ElementMetadata.parseDelimitedFrom(0-length) should yield default proto with NOT_DRAINING + // which translates to CausedByDrain.NORMAL, which is the default! + assertEquals(CombinedMetadata.createDefault(), decoded); + } +} diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java index 43b6a3cb0cb0..2326a1c77d38 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java @@ -318,7 +318,7 @@ public boolean hasNoActiveWindows() { public final void assertHasOnlyGlobalAndFinishedSetsFor(W... expectedWindows) { assertHasOnlyGlobalAndAllowedTags( ImmutableSet.copyOf(expectedWindows), - ImmutableSet.of(TriggerStateMachineRunner.FINISHED_BITS_TAG)); + ImmutableSet.of(TriggerStateMachineRunner.FINISHED_BITS_TAG, ReduceFnRunner.METADATA_TAG)); } @SafeVarargs @@ -331,7 +331,8 @@ public final void assertHasOnlyGlobalAndStateFor(W... expectedWindows) { PaneInfoTracker.PANE_INFO_TAG, WatermarkHold.watermarkHoldTagForTimestampCombiner( objectStrategy.getTimestampCombiner()), - WatermarkHold.EXTRA_HOLD_TAG)); + WatermarkHold.EXTRA_HOLD_TAG, + ReduceFnRunner.METADATA_TAG)); } @SafeVarargs diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java index cff5ba4a2fc4..d8a1d1b90d47 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java @@ -1807,6 +1807,7 @@ public void testMergeWindows() throws Exception { String timerTagPrefix = "/s" + window + "+0"; ByteString bufferTag = ByteString.copyFromUtf8(window + "+ubuf"); ByteString paneInfoTag = ByteString.copyFromUtf8(window + "+upaneInfo"); + ByteString combinedMetadataTag = ByteString.copyFromUtf8(window + "+ucombinedMetadata"); String watermarkDataHoldTag = window + "+uhold"; String watermarkExtraHoldTag = window + "+uextra"; String stateFamily = "MergeWindows"; @@ -1836,10 +1837,10 @@ public void testMergeWindows() throws Exception { // Set timer verifyTimers(actualOutput, buildWatermarkTimer(timerTagPrefix, 999)); - + // no combined metadata as it's default assertThat( actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) @@ -1915,6 +1916,13 @@ public void testMergeWindows() throws Exception { .getValueBuilder() .setTimestamp(0) .setData(ByteString.EMPTY); + dataBuilder + .addBagsBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + // 0x02: Protobuf Delimited Length (Payload is 2 bytes), 0x08: Protobuf Tag + // (Field #1), 0x01: Protobuf Value (Enum 1 = NOT_DRAINING). + .addValues(ByteString.copyFrom(new byte[] {0x02, 0x08, 0x01})); server.whenGetDataCalled().thenReturn(dataResponse.build()); expectedBytesRead += dataBuilder.build().getSerializedSize(); @@ -1960,12 +1968,18 @@ public void testMergeWindows() throws Exception { assertThat( "" + actualOutput.getBagUpdatesList(), actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) .setStateFamily(stateFamily) .setDeleteAll(true) + .build()), + Matchers.equalTo( + Windmill.TagBag.newBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + .setDeleteAll(true) .build()))); verifyHolds( @@ -2097,6 +2111,7 @@ public void testMergeWindowsCaching() throws Exception { String timerTagPrefix = "/s" + window + "+0"; ByteString bufferTag = ByteString.copyFromUtf8(window + "+ubuf"); ByteString paneInfoTag = ByteString.copyFromUtf8(window + "+upaneInfo"); + ByteString combinedMetadataTag = ByteString.copyFromUtf8(window + "+ucombinedMetadata"); String watermarkDataHoldTag = window + "+uhold"; String watermarkExtraHoldTag = window + "+uextra"; String stateFamily = "MergeWindows"; @@ -2127,9 +2142,10 @@ public void testMergeWindowsCaching() throws Exception { // Set timer verifyTimers(actualOutput, buildWatermarkTimer(timerTagPrefix, 999)); + // no combinedMetadataTag as it is default assertThat( actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) @@ -2205,6 +2221,11 @@ public void testMergeWindowsCaching() throws Exception { .getValueBuilder() .setTimestamp(0) .setData(ByteString.EMPTY); + dataBuilder + .addBagsBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + .addValues(ByteString.copyFrom(new byte[] {0x02, 0x08, 0x01})); server.whenGetDataCalled().thenReturn(dataResponse.build()); expectedBytesRead += dataBuilder.build().getSerializedSize(); @@ -2250,12 +2271,18 @@ public void testMergeWindowsCaching() throws Exception { assertThat( "" + actualOutput.getBagUpdatesList(), actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) .setStateFamily(stateFamily) .setDeleteAll(true) + .build()), + Matchers.equalTo( + Windmill.TagBag.newBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + .setDeleteAll(true) .build()))); verifyHolds( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java index 570b60ed035c..58595875fd3e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java @@ -957,6 +957,10 @@ public static void setMetadataSupported() { metadataSupported = true; } + public static void setMetadataNotSupported() { + metadataSupported = false; + } + public static boolean isMetadataSupported() { return metadataSupported; } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java index a2ff99905f6c..cba13ecb6bf2 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java @@ -17,13 +17,23 @@ */ package org.apache.beam.sdk.transforms; +import org.apache.beam.sdk.coders.BooleanCoder; import org.apache.beam.sdk.testing.NeedsRunner; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.testing.TestStream; +import org.apache.beam.sdk.testing.ValidatesRunner; +import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.Window; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.TimestampedValue; import org.apache.beam.sdk.values.WindowedValues; -import org.junit.Ignore; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -36,10 +46,14 @@ public class MetadataPropagationTest { /** Tests for metadata propagation. */ @Rule public final transient TestPipeline pipeline = TestPipeline.create(); - static class CausedByDrainSettingDoFn extends DoFn { + static class CausedByDrainSettingDoFn extends DoFn { @ProcessElement - public void process(OutputReceiver r) { - r.builder("value").setCausedByDrain(CausedByDrain.CAUSED_BY_DRAIN).output(); + public void process(@Element Boolean isDrain, OutputReceiver r) { + if (isDrain) { + r.builder("value").setCausedByDrain(CausedByDrain.CAUSED_BY_DRAIN).output(); + } else { + r.builder("value").setCausedByDrain(CausedByDrain.NORMAL).output(); + } } } @@ -52,12 +66,11 @@ public void process(ProcessContext pc, OutputReceiver r) { @Test @Category(NeedsRunner.class) - @Ignore public void testMetadataPropagationAcrossShuffleParameter() { WindowedValues.WindowedValueCoder.setMetadataSupported(); PCollection results = pipeline - .apply(Create.of(1)) + .apply(Create.of(true)) .apply(ParDo.of(new CausedByDrainSettingDoFn())) .apply(Redistribute.arbitrarily()) .apply(ParDo.of(new CausedByDrainExtractingDoFn())); @@ -68,11 +81,11 @@ public void testMetadataPropagationAcrossShuffleParameter() { } @Test - @Category(NeedsRunner.class) + @Category({ValidatesRunner.class, NeedsRunner.class}) public void testMetadataPropagationParameter() { PCollection results = pipeline - .apply(Create.of(1)) + .apply(Create.of(true)) .apply(ParDo.of(new CausedByDrainSettingDoFn())) .apply(ParDo.of(new CausedByDrainExtractingDoFn())); @@ -80,4 +93,70 @@ public void testMetadataPropagationParameter() { pipeline.run(); } + + static class CausedByDrainExtracingFromGBKDoFn + extends DoFn>, String> { + @ProcessElement + public void process(ProcessContext pc, OutputReceiver r) { + r.output(pc.causedByDrain().toString()); + } + } + + /** + * Tests metadata propagation across GroupByKey. Note: This test works only with DirectRunner and + * runners that support metadata propagation (e.g. via a flag to enable metadata encoding in + * coders). It fails on portable runners like Prism because they do not have implementation for + * metadata propagation, leading to coder mismatches. + */ + @Test + @Category(NeedsRunner.class) + public void testMetadataPropagationAcrossGBK() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + Instant baseTime = new Instant(0); + TestStream stream = + TestStream.create(BooleanCoder.of()) + .advanceWatermarkTo(baseTime) + .addElements(TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(10)))) + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(1))) + .addElements( + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(71))), + TimestampedValue.of(true, baseTime.plus(Duration.standardSeconds(72)))) + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(2))) + .addElements( + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(130))), + TimestampedValue.of(true, baseTime.plus(Duration.standardSeconds(131))), // drain + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(132)))) + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(3))) + .addElements( + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(181)))) // normal + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(4))) + .advanceWatermarkToInfinity(); + + Duration windowDuration = Duration.standardMinutes(1); + IntervalWindow window1 = new IntervalWindow(baseTime, windowDuration); + IntervalWindow window2 = new IntervalWindow(window1.end(), windowDuration); + IntervalWindow window3 = new IntervalWindow(window2.end(), windowDuration); + IntervalWindow window4 = new IntervalWindow(window3.end(), windowDuration); + + PCollection results = + pipeline + .apply(stream) + .apply(ParDo.of(new CausedByDrainSettingDoFn())) + .apply(WithKeys.of("1")) + .apply(Window.into(FixedWindows.of(windowDuration))) + .apply(GroupByKey.create()) + .apply(ParDo.of(new CausedByDrainExtracingFromGBKDoFn())); + + PAssert.that(results).inWindow(window1).containsInAnyOrder("NORMAL"); + PAssert.that(results).inWindow(window2).containsInAnyOrder("CAUSED_BY_DRAIN"); + PAssert.that(results).inWindow(window3).containsInAnyOrder("CAUSED_BY_DRAIN"); + PAssert.that(results).inWindow(window4).containsInAnyOrder("NORMAL"); + + pipeline.run(); + } + + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } } From 29498c51d74624c6a87b411abc7de797502b71b8 Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Tue, 21 Apr 2026 10:38:31 +0000 Subject: [PATCH 079/490] Add missing dependency on model:fn-execution to runners:core-java --- runners/core-java/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/runners/core-java/build.gradle b/runners/core-java/build.gradle index 9f24ce39b974..403cf4f2bc5a 100644 --- a/runners/core-java/build.gradle +++ b/runners/core-java/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation project(path: ":model:pipeline", configuration: "shadow") implementation project(path: ":sdks:java:core", configuration: "shadow") implementation project(path: ":model:job-management", configuration: "shadow") + implementation project(path: ":model:fn-execution", configuration: "shadow") implementation library.java.vendored_guava_32_1_2_jre implementation library.java.joda_time implementation library.java.vendored_grpc_1_69_0 From 962159cac2db7831a366dc233b8327c9932d4ee1 Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Fri, 24 Apr 2026 14:20:09 +0000 Subject: [PATCH 080/490] document asserts due to new state added --- .../wrappers/streaming/WindowDoFnOperatorTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java index 3bee828f23dd..6287ad119ac2 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java @@ -156,6 +156,11 @@ public void testTimerCleanupOfPendingTimerList() throws Exception { // Note that the following is 1 because the state is key-partitioned assertThat(Iterables.size(timerInternals.pendingTimersById.keys()), is(1)); + // Expected 6 state entries: + // - 2 entries for user buffer state ("buf") - one per key + // - 2 entries for watermark hold state ("hold") - one per key + // - 2 entries for non-empty panes count state ("count") - one per key + // - additional 2 entries for "combinedMetadata" state if non default metadata will be added assertThat(testHarness.numKeyedStateEntries(), is(6)); // close bundle testHarness.setProcessingTime( @@ -169,6 +174,11 @@ public void testTimerCleanupOfPendingTimerList() throws Exception { // Note that the following is zero because we only the first key is active assertThat(Iterables.size(timerInternals.pendingTimersById.keys()), is(0)); + // Expected 4 state entries remaining for the second key (which is still active): + // - 1 entry for user buffer state ("buf") + // - 1 entry for watermark hold state ("hold") + // - 1 entry for non-empty panes count state ("count") + // - 1 entry for "combinedMetadata" state if non default metadata will be added assertThat(testHarness.numKeyedStateEntries(), is(3)); // close bundle From f5e3050cf302ff7727d3279fee711c238e02a9f5 Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Fri, 24 Apr 2026 21:44:57 +0000 Subject: [PATCH 081/490] Fix propagation of metadata --- .../beam/runners/core/LateDataDroppingDoFnRunner.java | 8 +++++++- .../apache/beam/runners/dataflow/worker/WindmillSink.java | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java index f5587b46598a..b206e66dcf3e 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java @@ -145,7 +145,13 @@ public Iterable> filter( } else { nonLateElements.add( WindowedValues.of( - element.getValue(), element.getTimestamp(), window, element.getPaneInfo())); + element.getValue(), + element.getTimestamp(), + window, + element.getPaneInfo(), + element.getRecordId(), + element.getRecordOffset(), + element.causedByDrain())); } } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java index b07dc670e326..3ab46f0ddb42 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java @@ -39,6 +39,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.PaneInfo.PaneInfoCoder; import org.apache.beam.sdk.util.ByteStringOutputStream; +import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.ValueWithRecordId; import org.apache.beam.sdk.values.ValueWithRecordId.ValueWithRecordIdCoder; @@ -220,7 +221,12 @@ public long add(WindowedValue data) throws IOException { ByteString id = ByteString.EMPTY; // todo #33176 specify additional metadata in the future BeamFnApi.Elements.ElementMetadata additionalMetadata = - BeamFnApi.Elements.ElementMetadata.newBuilder().build(); + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setDrain( + data.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN + ? BeamFnApi.Elements.DrainMode.Enum.DRAINING + : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .build(); ByteString metadata = encodeMetadata( stream, windowsCoder, data.getWindows(), data.getPaneInfo(), additionalMetadata); From 84fa30d1f19e0633589f880aad0693c724496525 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 6 May 2026 11:50:41 -0400 Subject: [PATCH 082/490] Upgrade to Avro 1.12 (#38373) --- .github/workflows/README.md | 3 -- .../beam/gradle/BeamModulePlugin.groovy | 4 +- sdks/java/extensions/avro/build.gradle | 4 +- .../avro/schemas/utils/AvroUtils.java | 2 + .../avro/AvroVersionVerificationTest.java | 5 +-- .../extensions/avro/coders/AvroCoderTest.java | 5 ++- .../avro/io/AvroGeneratedUserFactory.java | 4 +- .../extensions/avro/io/AvroSourceTest.java | 11 +++--- .../io/SerializableAvroCodecFactoryTest.java | 4 +- .../schemas/TestAvroConversionFactory.java | 5 +-- .../avro/schemas/TestAvroFactory.java | 4 +- .../avro/schemas/utils/AvroUtilsTest.java | 4 +- .../avro/vendored-test/build.gradle | 38 ------------------- sdks/java/io/expansion-service/build.gradle | 2 - .../expansion-service/build.gradle | 6 --- .../sdk/io/gcp/pubsub/PubsubClientTest.java | 3 +- settings.gradle.kts | 1 - 17 files changed, 26 insertions(+), 79 deletions(-) delete mode 100644 sdks/java/extensions/avro/vendored-test/build.gradle diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 68b6a6bb7eb5..a6539d18f360 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -290,7 +290,6 @@ PreCommit Jobs run in a schedule and also get triggered in a PR if relevant sour | [ PreCommit RAT ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_RAT.yml) | N/A | `Run RAT PreCommit` | [![.github/workflows/beam_PreCommit_RAT.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_RAT.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_RAT.yml?query=event%3Aschedule) | | [ PreCommit Spotless ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Spotless.yml) | N/A | `Run Spotless PreCommit` | [![.github/workflows/beam_PreCommit_Spotless.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Spotless.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Spotless.yml?query=event%3Aschedule) | | [ PreCommit SQL ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL.yml) | N/A |`Run SQL PreCommit`| [![.github/workflows/beam_PreCommit_SQL.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL.yml?query=event%3Aschedule) | -| [ PreCommit SQL Java8 ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java8.yml) | N/A |`Run SQL_Java8 PreCommit`| [![.github/workflows/beam_PreCommit_SQL_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java8.yml?query=event%3Aschedule) | | [ PreCommit SQL Java17 ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java17.yml) | N/A |`Run SQL_Java17 PreCommit`| [![.github/workflows/beam_PreCommit_SQL_Java17.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java17.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java17.yml?query=event%3Aschedule) | | [ PreCommit Typescript ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Typescript.yml) | N/A |`Run Typescript PreCommit`| [![.github/workflows/beam_PreCommit_Typescript.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Typescript.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Typescript.yml?query=event%3Aschedule) | | [ PreCommit Website ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Website.yml) | N/A |`Run Website PreCommit`| [![.github/workflows/beam_PreCommit_Website.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Website.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Website.yml?query=event%3Aschedule) | @@ -368,9 +367,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Dataflow.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Direct JavaVersions ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml) | ['8','25'] |`beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Direct.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Flink Java8 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink_Java8.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Spark Java8 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark_Java8.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner SparkStructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Twister2 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Twister2.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml?query=event%3Aschedule) | diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 362eef052438..e32d1c9afb77 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -682,8 +682,8 @@ class BeamModulePlugin implements Plugin { aircompressor : "io.airlift:aircompressor:2.0.3", args4j : "args4j:args4j:2.33", auto_value_annotations : "com.google.auto.value:auto-value-annotations:$autovalue_version", - // TODO: https://github.com/apache/beam/issues/34993 after stopping supporting Java 8 - avro : "org.apache.avro:avro:1.11.4", + // TODO: upgrade post 1.12.1 once https://issues.apache.org/jira/browse/AVRO-4209 resolved + avro : "org.apache.avro:avro:1.12.0", aws_java_sdk2_apache_client : "software.amazon.awssdk:apache-client:$aws_java_sdk2_version", aws_java_sdk2_netty_client : "software.amazon.awssdk:netty-nio-client:$aws_java_sdk2_version", aws_java_sdk2_auth : "software.amazon.awssdk:auth:$aws_java_sdk2_version", diff --git a/sdks/java/extensions/avro/build.gradle b/sdks/java/extensions/avro/build.gradle index 6b24bf693dfb..718f07c03660 100644 --- a/sdks/java/extensions/avro/build.gradle +++ b/sdks/java/extensions/avro/build.gradle @@ -40,7 +40,7 @@ def avroVersions = [ '182' : "1.8.2", '192' : "1.9.2", '1102': "1.10.2", - '1120': "1.12.0", + '1113': "1.11.3", ] avroVersions.each { k, v -> @@ -73,7 +73,7 @@ dependencies { // Exclude Avro dependencies from "core" since Avro support moved to this extension exclude group: "org.apache.avro", module: "avro" } - testImplementation project(path: ":sdks:java:extensions:avro:vendored-test", configuration: "shadowTest") + testImplementation(library.java.avro + ':tests') testImplementation library.java.junit testImplementation "org.tukaani:xz:1.9" // marked as optional in avro testImplementation "com.esotericsoftware:kryo:5.6.2" // Used by Avro coder test diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java index 457265646420..99eb7f961901 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java @@ -156,6 +156,8 @@ "rawtypes" }) public class AvroUtils { + public static final String VERSION_AVRO = + org.apache.avro.Schema.class.getPackage().getImplementationVersion(); private static final ForLoadedType BYTES = new ForLoadedType(byte[].class); private static final ForLoadedType JAVA_INSTANT = new ForLoadedType(java.time.Instant.class); private static final ForLoadedType JAVA_LOCALE_DATE = diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java index f9e9a54b0531..ac17afadae2a 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java @@ -17,9 +17,9 @@ */ package org.apache.beam.sdk.extensions.avro; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.junit.Assert.assertEquals; -import org.apache.avro.Schema; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Assume; @@ -33,7 +33,6 @@ public class AvroVersionVerificationTest { public void testAvroVersion() { @Nullable String targetVer = System.getProperty("beam.target.avro.version"); Assume.assumeTrue(!Strings.isNullOrEmpty(targetVer)); - String actualVer = Schema.class.getPackage().getImplementationVersion(); - assertEquals(targetVer, actualVer); + assertEquals(targetVer, VERSION_AVRO); } } diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java index 69dfe71ee0b1..e1843d79eff5 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.avro.coders; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -131,8 +132,6 @@ public class AvroCoderTest { ImmutableList.of(AVRO_NESTED_SPECIFIC_RECORD, AVRO_NESTED_SPECIFIC_RECORD), ImmutableMap.of("k1", AVRO_NESTED_SPECIFIC_RECORD, "k2", AVRO_NESTED_SPECIFIC_RECORD)); - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); - @DefaultCoder(AvroCoder.class) private static class Pojo { public String text; @@ -875,6 +874,7 @@ private static class StringableClass {} @Test public void testDeterminismCyclicClass() { + // Note: this test fails on Avro 1.12.1 due to https://issues.apache.org/jira/browse/AVRO-4209 assertNonDeterministic( AvroCoder.of(Cyclic.class), reasonField(Cyclic.class, "cyclicField", "appears recursively")); @@ -1164,6 +1164,7 @@ private static class NullableCyclicField { @Test public void testNullableNonDeterministicField() { + // Note: this test fails on Avro 1.12.1 due to https://issues.apache.org/jira/browse/AVRO-4209 assertNonDeterministic( AvroCoder.of(NullableCyclic.class), reasonField( diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java index 65552b705dff..8f16141443e0 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java @@ -17,15 +17,15 @@ */ package org.apache.beam.sdk.extensions.avro.io; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; + import java.lang.reflect.Constructor; -import org.apache.avro.Schema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Create a {@link AvroGeneratedUser} instance with different constructors. */ public class AvroGeneratedUserFactory { private static final Logger LOG = LoggerFactory.getLogger(AvroGeneratedUserFactory.class); - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); public static AvroGeneratedUser newInstance( String name, Integer favoriteNumber, String favoriteColor) { diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java index f4841f1cdc02..27d070d6920a 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.avro.io; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasDisplayItem; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -74,9 +75,6 @@ /** Tests for AvroSource. */ @RunWith(JUnit4.class) public class AvroSourceTest { - private static final String VERSION_AVRO = - org.apache.avro.Schema.class.getPackage().getImplementationVersion(); - @Rule public TemporaryFolder tmpFolder = new TemporaryFolder(); @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -560,10 +558,11 @@ public void testDatumReaderFactoryWithGenericRecord() throws Exception { AvroSource.DatumReaderFactory factory = (writer, reader) -> - new GenericDatumReader(writer, reader) { + new GenericDatumReader<>(writer, reader) { @Override - protected Object readString(Object old, Decoder in) throws IOException { - return super.readString(old, in) + "_custom"; + protected Object readString(Object old, Schema schema, Decoder in) + throws IOException { + return super.readString(old, schema, in) + "_custom"; } }; diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java index 52f7a6700275..19141a82148a 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java @@ -22,6 +22,7 @@ import static org.apache.avro.file.DataFileConstants.NULL_CODEC; import static org.apache.avro.file.DataFileConstants.SNAPPY_CODEC; import static org.apache.avro.file.DataFileConstants.XZ_CODEC; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; @@ -40,9 +41,6 @@ /** Tests of SerializableAvroCodecFactory. */ @RunWith(JUnit4.class) public class SerializableAvroCodecFactoryTest { - private static final String VERSION_AVRO = - org.apache.avro.Schema.class.getPackage().getImplementationVersion(); - private static final List avroCodecs = new ArrayList<>(); static { diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java index d6d6aeaffa3f..cdf16f71a69b 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java @@ -17,15 +17,14 @@ */ package org.apache.beam.sdk.extensions.avro.schemas; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; + import java.lang.reflect.Constructor; -import org.apache.avro.Schema; import org.joda.time.LocalDate; /** Create a {@link TestAvroConversion} instance with different constructors. */ public class TestAvroConversionFactory { - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); - public static TestAvroConversion newInstance(LocalDate date) throws Exception { if (VERSION_AVRO.equals("1.8.2")) { Constructor constructor = TestAvroConversion.class.getDeclaredConstructor(LocalDate.class); diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java index 4d52a35543e0..ab1fdee92691 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java @@ -17,11 +17,12 @@ */ package org.apache.beam.sdk.extensions.avro.schemas; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; + import java.lang.reflect.Constructor; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; -import org.apache.avro.Schema; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.slf4j.Logger; @@ -30,7 +31,6 @@ /** Create a {@link TestAvro} instance with different constructors. */ public class TestAvroFactory { private static final Logger LOG = LoggerFactory.getLogger(TestAvroFactory.class); - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); public static TestAvro newInstance( Boolean boolNonNullable, diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java index 282158bec03d..6db5206c3cf6 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.avro.schemas.utils; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -86,9 +87,6 @@ public class AvroUtilsTest { private static final org.apache.avro.Schema NULL_SCHEMA = org.apache.avro.Schema.create(org.apache.avro.Schema.Type.NULL); - private static final String VERSION_AVRO = - org.apache.avro.Schema.class.getPackage().getImplementationVersion(); - private Iterable randomData(org.apache.avro.Schema schema, int maxLength) throws Exception { Iterable data; if (VERSION_AVRO.equals("1.8.2")) { diff --git a/sdks/java/extensions/avro/vendored-test/build.gradle b/sdks/java/extensions/avro/vendored-test/build.gradle deleted file mode 100644 index b0489c27a13b..000000000000 --- a/sdks/java/extensions/avro/vendored-test/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -plugins { id 'org.apache.beam.module' } -applyJavaNature( - automaticModuleName: 'org.apache.beam.sdk.extensions.avro', - exportJavadoc: false, - shadowClosure: { - dependencies { - include(dependency("org.apache.avro:avro:1.11.3:tests")) - } - }, -) - -configurations.all { - resolutionStrategy.force "org.apache.avro:avro:1.11.3:tests" -} - -dependencies { - testRuntimeOnly "org.apache.avro:avro:1.11.3:tests" -} - -description = "Apache Beam :: SDKs :: Java :: Extensions :: Avro :: Vendored Tests" -ext.summary = "Vendor Avro 1.11.3 tests for Beam, a workaround of Avro 1.11.4 not release test jar" diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index a6a99c52d712..4e4c15f367cb 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -35,8 +35,6 @@ applyJavaNature( configurations.runtimeClasspath { // Pin kafka-clients version due to <3.4.0 missing auth callback classes. resolutionStrategy.force 'org.apache.kafka:kafka-clients:3.9.0' - // iceberg needs avro:1.12.0 - resolutionStrategy.force 'org.apache.avro:avro:1.12.0' // force parquet-avro:1.15.2 to fix CVE-2025-46762 resolutionStrategy.force 'org.apache.parquet:parquet-avro:1.15.2' diff --git a/sdks/java/io/google-cloud-platform/expansion-service/build.gradle b/sdks/java/io/google-cloud-platform/expansion-service/build.gradle index d1f65c88bdc5..824a2659b84b 100644 --- a/sdks/java/io/google-cloud-platform/expansion-service/build.gradle +++ b/sdks/java/io/google-cloud-platform/expansion-service/build.gradle @@ -26,12 +26,6 @@ applyJavaNature( shadowClosure: {}, ) -configurations.runtimeClasspath { - // Pin avro to 1.11.4 due to https://github.com/apache/beam/issues/34968 - // cannot upgrade this to the latest version due to https://github.com/apache/beam/issues/34993 - resolutionStrategy.force 'org.apache.avro:avro:1.11.4' -} - description = "Apache Beam :: SDKs :: Java :: IO :: Google Cloud Platform :: Expansion Service" ext.summary = "Expansion service serving GCP Java IOs" diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java index 9d7bc65f5954..e834d4eef5eb 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java @@ -22,6 +22,7 @@ import com.google.pubsub.v1.Schema; import java.util.Map; +import org.apache.avro.AvroTypeException; import org.apache.avro.SchemaParseException; import org.apache.beam.sdk.io.gcp.pubsub.PubsubClient.ProjectPath; import org.apache.beam.sdk.io.gcp.pubsub.PubsubClient.SchemaPath; @@ -262,7 +263,7 @@ public void fromPubsubSchema() { assertThrows( "'notatype' Avro type should throw an exception", - SchemaParseException.class, + AvroTypeException.class, () -> PubsubClient.fromPubsubSchema( new com.google.api.services.pubsub.model.Schema() diff --git a/settings.gradle.kts b/settings.gradle.kts index 4080206bb542..60ff1cf8ce1b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -174,7 +174,6 @@ include(":sdks:java:expansion-service:container") include(":sdks:java:expansion-service:app") include(":sdks:java:extensions:arrow") include(":sdks:java:extensions:avro") -include("sdks:java:extensions:avro:vendored-test") include(":sdks:java:extensions:euphoria") include(":sdks:java:extensions:kryo") include(":sdks:java:extensions:google-cloud-platform-core") From bbc1f1b4b237d70698769619e3e38734326f5b1b Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 6 May 2026 12:33:52 -0400 Subject: [PATCH 083/490] GCS client library migration in Java SDK - part 3 (#37900) * Implement open method for gcsutilv2. Add an integration test. * Implement create method and add an integration test. * Store the gcs path into GcsWritableByteChannel. * Rename the new create method to createV2. * Revise according to reviewer comments. --- .../beam/sdk/extensions/gcp/util/GcsUtil.java | 23 +++ .../sdk/extensions/gcp/util/GcsUtilV2.java | 163 +++++++++++++++++- .../gcp/util/GcsUtilParameterizedIT.java | 116 +++++++++++-- 3 files changed, 291 insertions(+), 11 deletions(-) diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java index e3f01dd85295..ed727d495cf8 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java @@ -25,6 +25,8 @@ import com.google.cloud.storage.BucketInfo; import com.google.cloud.storage.Storage.BlobGetOption; import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.Storage.BucketGetOption; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; @@ -186,9 +188,19 @@ public Page listBlobs( } public SeekableByteChannel open(GcsPath path) throws IOException { + if (delegateV2 != null) { + return delegateV2.open(path); + } return delegate.open(path); } + public SeekableByteChannel openV2(GcsPath path, BlobSourceOption... options) throws IOException { + if (delegateV2 != null) { + return delegateV2.open(path, options); + } + throw new IOException("GcsUtil V2 not initialized."); + } + /** @deprecated Use {@link #create(GcsPath, CreateOptions)} instead. */ @Deprecated public WritableByteChannel create(GcsPath path, String type) throws IOException { @@ -254,9 +266,20 @@ public CreateOptions build() { } public WritableByteChannel create(GcsPath path, CreateOptions options) throws IOException { + if (delegateV2 != null) { + delegateV2.create(path, options.delegate); + } return delegate.create(path, options.delegate); } + public WritableByteChannel createV2( + GcsPath path, CreateOptions options, BlobWriteOption... writeOptions) throws IOException { + if (delegateV2 != null) { + return delegateV2.create(path, options.delegate, writeOptions); + } + throw new IOException("GcsUtil V2 not initialized."); + } + public void verifyBucketAccessible(GcsPath path) throws IOException { if (delegateV2 != null) { delegateV2.verifyBucketAccessible(path); diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java index b00b7ce0d728..9119dd79652e 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java @@ -23,6 +23,8 @@ import com.google.api.gax.paging.Page; import com.google.auto.value.AutoValue; +import com.google.cloud.ReadChannel; +import com.google.cloud.WriteChannel; import com.google.cloud.storage.Blob; import com.google.cloud.storage.BlobId; import com.google.cloud.storage.BlobInfo; @@ -33,21 +35,29 @@ import com.google.cloud.storage.Storage.BlobField; import com.google.cloud.storage.Storage.BlobGetOption; import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.Storage.BucketField; import com.google.cloud.storage.Storage.BucketGetOption; import com.google.cloud.storage.Storage.CopyRequest; import com.google.cloud.storage.StorageBatch; import com.google.cloud.storage.StorageBatchResult; +import com.google.cloud.storage.StorageChannelUtils; import com.google.cloud.storage.StorageException; import com.google.cloud.storage.StorageOptions; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; import java.nio.file.FileAlreadyExistsException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.extensions.gcp.options.GcsOptions; import org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath; import org.apache.beam.sdk.options.DefaultValueFactory; import org.apache.beam.sdk.options.PipelineOptions; @@ -70,6 +80,8 @@ public GcsUtilV2 create(PipelineOptions options) { private Storage storage; + private final @Nullable Integer uploadBufferSizeBytes; + /** Maximum number of items to retrieve per Objects.List request. */ private static final long MAX_LIST_BLOBS_PER_CALL = 1024; @@ -85,13 +97,14 @@ public GcsUtilV2 create(PipelineOptions options) { GcsUtilV2(PipelineOptions options) { String projectId = options.as(GcpOptions.class).getProject(); storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService(); + uploadBufferSizeBytes = options.as(GcsOptions.class).getGcsUploadBufferSizeBytes(); } @SuppressWarnings({ "nullness" // For Creating AccessDeniedException FileNotFoundException, and // FileAlreadyExistsException with null. }) - private IOException translateStorageException(GcsPath gcsPath, StorageException e) { + private static IOException translateStorageException(GcsPath gcsPath, StorageException e) { switch (e.getCode()) { case 403: return new AccessDeniedException(gcsPath.toString(), null, e.getMessage()); @@ -481,4 +494,152 @@ public void removeBucket(BucketInfo bucketInfo) throws IOException { throw translateStorageException(bucketInfo.getName(), null, e); } } + + /** A bridge that allows a GCS ReadChannel to behave as a SeekableByteChannel. */ + private static class GcsSeekableByteChannel implements SeekableByteChannel { + private final ReadChannel reader; + private final long size; + private long position = 0; + + GcsSeekableByteChannel(ReadChannel reader, long size) { + this.reader = reader; + this.size = size; + this.position = 0; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int count = StorageChannelUtils.blockingFillFrom(dst, reader); + if (count > 0) { + this.position += count; + } + return count; + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + checkArgument(newPosition >= 0, "Position must be non-negative: %s", newPosition); + reader.seek(newPosition); + this.position = newPosition; + return this; + } + + @Override + public long position() throws IOException { + return this.position; + } + + @Override + public long size() throws IOException { + return size; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new UnsupportedOperationException( + "GcsSeekableByteChannels are read-only and cannot be truncated."); + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new UnsupportedOperationException( + "GcsSeekableByteChannel are read-only and does not support writing."); + } + + @Override + public boolean isOpen() { + return reader.isOpen(); + } + + @Override + public void close() throws IOException { + if (isOpen()) { + reader.close(); + } + } + } + + public SeekableByteChannel open(GcsPath path, BlobSourceOption... sourceOptions) + throws IOException { + Blob blob = getBlob(path, BlobGetOption.fields(BlobField.SIZE)); + ReadChannel reader = blob.getStorage().reader(blob.getBlobId(), sourceOptions); + // disable internal buffering, and make the channel non-blocking + reader.setChunkSize(0); + return new GcsSeekableByteChannel(reader, blob.getSize()); + } + + /** A bridge that allows a GCS WriteChannel to behave as a WritableByteChannel. */ + private static class GcsWritableByteChannel implements WritableByteChannel { + private final WriteChannel writer; + private final GcsPath gcsPath; + + GcsWritableByteChannel(WriteChannel writer, GcsPath gcsPath) { + this.writer = writer; + this.gcsPath = gcsPath; + } + + @Override + public int write(ByteBuffer src) throws IOException { + try { + return writer.write(src); + } catch (StorageException e) { + throw translateStorageException(gcsPath, e); + } + } + + @Override + public boolean isOpen() { + return writer.isOpen(); + } + + @Override + public void close() throws IOException { + writer.close(); + } + } + + public WritableByteChannel create( + GcsPath path, GcsUtilV1.CreateOptions options, BlobWriteOption... writeOptions) + throws IOException { + try { + // Define the metadata for the new object + BlobInfo.Builder builder = BlobInfo.newBuilder(path.getBucket(), path.getObject()); + String type = options.getContentType(); + if (type != null) { + builder.setContentType(type); + } + + BlobInfo blobInfo = builder.build(); + + List writeOptionList = new ArrayList<>(Arrays.asList(writeOptions)); + if (options.getExpectFileToNotExist()) { + writeOptionList.add(BlobWriteOption.doesNotExist()); + } else { + // We do not merge this check with the getExpectFileToNotExist() branch above + // because we don't want to always make the storage.get() RPC call. + Blob blob = storage.get(path.getBucket(), path.getObject()); + if (blob == null) { + writeOptionList.add(BlobWriteOption.doesNotExist()); + } else { + writeOptionList.add(BlobWriteOption.generationMatch(blob.getGeneration())); + } + } + // Open a WriteChannel from the storage service + WriteChannel writer = + storage.writer(blobInfo, writeOptionList.toArray(new BlobWriteOption[0])); + Integer uploadBufferSizeBytes = + options.getUploadBufferSizeBytes() != null + ? options.getUploadBufferSizeBytes() + : this.uploadBufferSizeBytes; + if (uploadBufferSizeBytes != null) { + writer.setChunkSize(uploadBufferSizeBytes); + } + + // Return the bridge wrapper + return new GcsWritableByteChannel(writer, path); + + } catch (StorageException e) { + throw translateStorageException(path, e); + } + } } diff --git a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java index 80ffd72924fa..5759bb10a654 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.gcp.util; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -28,15 +29,25 @@ import com.google.api.services.storage.model.StorageObject; import com.google.cloud.storage.Blob; import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.StorageChannelUtils; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; import java.nio.file.FileAlreadyExistsException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.apache.beam.sdk.extensions.gcp.options.GcsOptions; +import org.apache.beam.sdk.extensions.gcp.util.GcsUtil.CreateOptions; import org.apache.beam.sdk.extensions.gcp.util.GcsUtilV2.MissingStrategy; import org.apache.beam.sdk.extensions.gcp.util.GcsUtilV2.OverwriteStrategy; import org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath; @@ -301,7 +312,8 @@ public void testCreateAndRemoveBucket() throws IOException { } } - private List createTestBucketHelper(String bucketName) throws IOException { + private List createTestBucketHelper(String bucketName, boolean copyData) + throws IOException { final List originPaths = Arrays.asList( GcsPath.fromUri("gs://apache-beam-samples/shakespeare/kingrichardii.txt"), @@ -316,16 +328,24 @@ private List createTestBucketHelper(String bucketName) throws IOExcepti if (experiment.equals("use_gcsutil_v2")) { gcsUtil.createBucket(BucketInfo.of(bucketName)); - gcsUtil.copyV2(originPaths, testPaths); + if (copyData) { + gcsUtil.copyV2(originPaths, testPaths); + } else { + return Collections.emptyList(); + } } else { GcsOptions gcsOptions = options.as(GcsOptions.class); gcsUtil.createBucket(gcsOptions.getProject(), new Bucket().setName(bucketName)); - final List originList = - originPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); - final List testList = - testPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); - gcsUtil.copy(originList, testList); + if (copyData) { + final List originList = + originPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); + final List testList = + testPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); + gcsUtil.copy(originList, testList); + } else { + return Collections.emptyList(); + } } return testPaths; @@ -355,7 +375,7 @@ public void testCopy() throws IOException { final String nonExistentBucket = "my-random-test-bucket-12345"; try { - final List srcPaths = createTestBucketHelper(existingBucket); + final List srcPaths = createTestBucketHelper(existingBucket, true); final List dstPaths = srcPaths.stream() .map(o -> GcsPath.fromComponents(existingBucket, o.getObject() + ".bak")) @@ -423,7 +443,7 @@ public void testRemove() throws IOException { final String nonExistentBucket = "my-random-test-bucket-12345"; try { - final List srcPaths = createTestBucketHelper(existingBucket); + final List srcPaths = createTestBucketHelper(existingBucket, true); final List errPaths = srcPaths.stream() .map(o -> GcsPath.fromComponents(nonExistentBucket, o.getObject())) @@ -485,7 +505,7 @@ public void testRename() throws IOException { final String nonExistentBucket = "my-random-test-bucket-12345"; try { - final List srcPaths = createTestBucketHelper(existingBucket); + final List srcPaths = createTestBucketHelper(existingBucket, true); final List tmpPaths = srcPaths.stream() .map(o -> GcsPath.fromComponents(existingBucket, "tmp/" + o.getObject())) @@ -587,4 +607,80 @@ private void assertNotExists(GcsPath path) throws IOException { assertThrows(FileNotFoundException.class, () -> gcsUtil.getObject(path)); } } + + String computeHash(ByteBuffer buffer) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(buffer); + byte[] hashBytes = digest.digest(); + + // Convert bytes to Hex String + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + @Test + public void testRead() throws IOException, NoSuchAlgorithmException { + final GcsPath gcsPath = GcsPath.fromUri("gs://apache-beam-samples/shakespeare/kinglear.txt"); + final String expectedHash = "674a2725884307c96398440497c889ad8cecccedf5689df85e6b0faabe4e0fe8"; + final long expectedSize = 157283L; + + try (SeekableByteChannel channel = gcsUtil.open(gcsPath)) { + // Verify Size + assertEquals(expectedSize, channel.size()); + assertEquals(0, channel.position()); + + // Read content into ByteBuffer. + // Allocate a larger buffer to ensure we receive the EOF at the expected place. + ByteBuffer buffer = ByteBuffer.allocate((int) expectedSize + 1024); + int bytesRead = StorageChannelUtils.blockingFillFrom(buffer, channel); + + // Verify total bytes read and position + assertEquals(expectedSize, bytesRead); + assertEquals(expectedSize, channel.position()); + + // Flip the buffer to prepare it for reading (sets limit to current position, position to 0) + buffer.flip(); + + // Verify hash + String actualHash = computeHash(buffer); + assertEquals("Content hash should match", expectedHash, actualHash); + } + } + + @Test + public void testWriteAndRead() throws IOException { + final String bucketName = "apache-beam-temp-bucket-12345"; + final GcsPath targetPath = + GcsPath.fromComponents(bucketName, "test-object-" + java.util.UUID.randomUUID() + ".txt"); + final byte[] content = "Hello, GCS!".getBytes(StandardCharsets.UTF_8); + + try { + createTestBucketHelper(bucketName, false); + + // Write content to a GCS file + CreateOptions options = CreateOptions.builder().setExpectFileToNotExist(true).build(); + try (WritableByteChannel writer = gcsUtil.create(targetPath, options)) { + writer.write(ByteBuffer.wrap(content)); + } + + // Read content into a buffer + ByteArrayOutputStream readContent = new ByteArrayOutputStream(); + try (ReadableByteChannel reader = gcsUtil.open(targetPath)) { + ByteBuffer buffer = ByteBuffer.allocate(1024); + while (reader.read(buffer) != -1) { + buffer.flip(); + readContent.write(buffer.array(), 0, buffer.limit()); + buffer.clear(); + } + } + + // Verify content + assertArrayEquals(content, readContent.toByteArray()); + } finally { + tearDownTestBucketHelper(bucketName); + } + } } From dd6c451eeff4515da1c66b4e3b56d95636097c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Wed, 6 May 2026 19:38:16 +0200 Subject: [PATCH 084/490] Fix WindowedValue.of() invocation. (#38380) --- .../apache/beam/runners/core/LateDataDroppingDoFnRunner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java index b206e66dcf3e..2ce94533d9e2 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java @@ -151,7 +151,8 @@ public Iterable> filter( element.getPaneInfo(), element.getRecordId(), element.getRecordOffset(), - element.causedByDrain())); + element.causedByDrain(), + element.getOpenTelemetryContext())); } } } From c0731bf07ee1bf7e664a50cb1f3ae07459f44fcd Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 6 May 2026 16:59:35 -0400 Subject: [PATCH 085/490] Go VR Flink test on Flink 2.0 (#37640) --- .github/trigger_files/beam_PostCommit_Go_VR_Flink.json | 1 + .../beam/runners/flink/FlinkExecutionEnvironments.java | 10 ++++++++++ runners/flink/flink_runner.gradle | 7 +++++++ .../beam/runners/flink/FlinkExecutionEnvironments.java | 8 ++++++++ sdks/go/test/build.gradle | 3 +-- sdks/go/test/integration/integration.go | 2 +- 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json b/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json index ed3d846bc7b0..939c43396fda 100644 --- a/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json +++ b/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json @@ -2,4 +2,5 @@ "comment": "Modify this file in a trivial way to cause this test suite to run", "modification": 2, "https://github.com/apache/beam/pull/32440": "testing datastream optimizations", + "pr": "37640" } diff --git a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java index 0d48526e1d0a..4a1c1cb0c5e1 100644 --- a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java +++ b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java @@ -36,6 +36,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.net.HostAndPort; import org.apache.flink.api.common.ExecutionConfig; import org.apache.flink.api.common.RuntimeExecutionMode; +import org.apache.flink.api.common.serialization.SerializerConfigImpl; import org.apache.flink.configuration.CheckpointingOptions; import org.apache.flink.configuration.Configuration; import org.apache.flink.configuration.CoreOptions; @@ -270,6 +271,7 @@ public static StreamExecutionEnvironment createStreamExecutionEnvironment( flinkStreamEnv.getConfig().setAutoWatermarkInterval(options.getAutoWatermarkInterval()); } configureWebUIOptions(flinkStreamEnv.getConfig(), options.as(PipelineOptions.class)); + configureCustomKryoSerializers(flinkStreamEnv.getConfig()); return flinkStreamEnv; } @@ -294,6 +296,14 @@ private static void configureWebUIOptions( } } + private static void configureCustomKryoSerializers(ExecutionConfig config) { + SerializerConfigImpl serializerConfig = (SerializerConfigImpl) config.getSerializerConfig(); + // Force Beam schema to use JavaSerializer to fix serialization involving ImmutableMap + serializerConfig.registerTypeWithKryoSerializer( + org.apache.beam.sdk.schemas.Schema.class, + com.esotericsoftware.kryo.serializers.JavaSerializer.class); + } + private static class GlobalJobParametersImpl extends ExecutionConfig.GlobalJobParameters { private final Map jobOptions; diff --git a/runners/flink/flink_runner.gradle b/runners/flink/flink_runner.gradle index 98e7d547b4d3..837561ec71b7 100644 --- a/runners/flink/flink_runner.gradle +++ b/runners/flink/flink_runner.gradle @@ -230,6 +230,13 @@ dependencies { // configuration (https://issues.apache.org/jira/browse/BEAM-11732). permitUnusedDeclared "org.apache.flink:flink-clients:$flink_version" + // align with Flink's kryo version (groupId changed) + if (flink_major.startsWith('2')) { + implementation "com.esotericsoftware:kryo:5.6.2" + } else { + implementation "com.esotericsoftware.kryo:kryo:2.24.0" + } + implementation "org.apache.flink:flink-streaming-java:$flink_version" testImplementation "org.apache.flink:flink-statebackend-rocksdb:$flink_version" testImplementation "org.apache.flink:flink-streaming-java:$flink_version:tests" diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java index a0e5908cc99d..de92dd94605a 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java @@ -298,6 +298,7 @@ public static StreamExecutionEnvironment createStreamExecutionEnvironment( configureStateBackend(options, flinkStreamEnv); configureWebUIOptions(flinkStreamEnv.getConfig(), options.as(PipelineOptions.class)); + configureCustomKryoSerializers(flinkStreamEnv.getConfig()); return flinkStreamEnv; } @@ -322,6 +323,13 @@ private static void configureWebUIOptions( } } + private static void configureCustomKryoSerializers(ExecutionConfig config) { + // Force Beam schema to use JavaSerializer to fix serialization involving ImmutableMap + config.registerTypeWithKryoSerializer( + org.apache.beam.sdk.schemas.Schema.class, + com.esotericsoftware.kryo.serializers.JavaSerializer.class); + } + private static class GlobalJobParametersImpl extends ExecutionConfig.GlobalJobParameters { private final Map jobOptions; diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index ddaaa136b432..677134716062 100644 --- a/sdks/go/test/build.gradle +++ b/sdks/go/test/build.gradle @@ -79,8 +79,7 @@ task dataflowValidatesRunnerARM64() { task flinkValidatesRunner { group = "Verification" - // TODO(https://github.com/apache/beam/issues/37600) use project.ext.latestFlinkVersion after resolved - def flinkVersion = '1.20' + def flinkVersion = project.ext.latestFlinkVersion dependsOn ":sdks:go:test:goBuild" dependsOn ":sdks:go:container:docker" diff --git a/sdks/go/test/integration/integration.go b/sdks/go/test/integration/integration.go index b23547bf4fa1..a0eef7d10189 100644 --- a/sdks/go/test/integration/integration.go +++ b/sdks/go/test/integration/integration.go @@ -199,7 +199,7 @@ var flinkFilters = []string{ "TestTestStreamToGBK", "TestTestStreamTimersEventTime", - "TestTimers_EventTime_Unbounded", // (failure when comparing on side inputs (NPE on window lookup)) + "TestTimers_EventTime_WithNoOutputTimestamp", // Encounter error: TimestampCombiner moved element from TIMESTAMP_MAX_VALUE to earlier time (end of global window) for window GlobalWindow "TestTimers_ProcessingTime.*", // Flink doesn't support processing time timers. // no support for BundleFinalizer From 9e8e36c61144ffe0d481e0ab145ce55cc47ccc40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 15:50:10 -0700 Subject: [PATCH 086/490] Bump github.com/apache/thrift from 0.21.0 to 0.23.0 in /sdks (#38383) Bumps [github.com/apache/thrift](https://github.com/apache/thrift) from 0.21.0 to 0.23.0. - [Release notes](https://github.com/apache/thrift/releases) - [Changelog](https://github.com/apache/thrift/blob/master/CHANGES.md) - [Commits](https://github.com/apache/thrift/compare/v0.21.0...v0.23.0) --- updated-dependencies: - dependency-name: github.com/apache/thrift dependency-version: 0.23.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index a73c8f3d44f2..2a71fc56884a 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -145,7 +145,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect - github.com/apache/thrift v0.21.0 // indirect + github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 074c3122592a..e7e6c39e0d3b 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -186,8 +186,8 @@ github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcy github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= -github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/apache/thrift v0.23.0 h1:wKR6YnefQSEnxpEfmgTPuJibNG4bF0p2TK34tHLWi3s= +github.com/apache/thrift v0.23.0/go.mod h1:zPt6WxgvTOM6hF92y8C+MkEM5LMxZuk4JcQOiU4Esvs= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= From aa104707918a7ab3d7f8c507b0b3716340e2d038 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 6 May 2026 21:05:37 -0400 Subject: [PATCH 087/490] update excluded path to relative path (#38378) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c1dc085df00d..0d6860fd1680 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,7 +24,7 @@ updates: schedule: interval: "daily" exclude-paths: - - "/sdks/python/container/**" + - "container/**" - package-ecosystem: "gradle" directory: "/" # Location of package manifests schedule: From 3d4998f4550126f7af6ec6a57e2adc491d531cc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 18:59:14 -0700 Subject: [PATCH 088/490] Bump cloud.google.com/go/bigquery from 1.74.0 to 1.77.0 in /sdks (#38385) Bumps [cloud.google.com/go/bigquery](https://github.com/googleapis/google-cloud-go) from 1.74.0 to 1.77.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/spanner/v1.74.0...spanner/v1.77.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/bigquery dependency-version: 1.77.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 12 ++++++------ sdks/go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 2a71fc56884a..1f4904f33376 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -25,13 +25,13 @@ go 1.26.0 toolchain go1.26.2 require ( - cloud.google.com/go/bigquery v1.74.0 + cloud.google.com/go/bigquery v1.77.0 cloud.google.com/go/bigtable v1.42.0 cloud.google.com/go/datastore v1.22.0 cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.1 cloud.google.com/go/spanner v1.88.0 - cloud.google.com/go/storage v1.59.2 + cloud.google.com/go/storage v1.62.0 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 @@ -85,8 +85,8 @@ require ( filippo.io/edwards25519 v1.1.1 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect @@ -140,8 +140,8 @@ require ( require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.8.0 // indirect + cloud.google.com/go/iam v1.7.0 // indirect + cloud.google.com/go/longrunning v0.9.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index e7e6c39e0d3b..d6baa5b40793 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -44,8 +44,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.74.0 h1:Q6bAMv+eyvufOpIrfrYxhM46qq1D3ZQTdgUDQqKS+n8= -cloud.google.com/go/bigquery v1.74.0/go.mod h1:iViO7Cx3A/cRKcHNRsHB3yqGAMInFBswrE9Pxazsc90= +cloud.google.com/go/bigquery v1.77.0 h1:L5AW3jhzEKpFVg4i0mVHxKpxogrqT7dczWBSr4m9MKU= +cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= @@ -54,8 +54,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= -cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= +cloud.google.com/go/datacatalog v1.27.0 h1:AnghhtHKCqYIe62gTPHcn9nJr5jtxjZHV4D/Fob23gg= +cloud.google.com/go/datacatalog v1.27.0/go.mod h1:YTI11pFlC5HCj4CphEf+qWCy/z9udd7o0HVN6c2Povg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.22.0 h1:FOyx2Ag6ibD2wFkz9S8EiNrmBugia8pQOfpyJxi2yqA= @@ -64,16 +64,16 @@ cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= @@ -99,8 +99,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw= -cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/storage v1.62.0 h1:w2pQJhpUqVerMON45vatE2FpCYsNTf7OHjkn6ux5mMU= +cloud.google.com/go/storage v1.62.0/go.mod h1:T5hz3qzcpnxZ5LdKc7y8Tw7lh4v9zeeVyrD/cLJAzZU= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= @@ -165,12 +165,12 @@ github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -883,8 +883,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bT go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= From a8b1843e3daeff2c44575f07d1001eba5662169a Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Wed, 6 May 2026 19:24:29 -0700 Subject: [PATCH 089/490] Add Staged Artifact validations for RunnerV2 (#37974) --- sdks/go/container/boot.go | 4 + sdks/go/pkg/beam/artifact/materialize.go | 45 ++++++- sdks/go/pkg/beam/artifact/materialize_test.go | 110 ++++++++++++++++++ sdks/go/pkg/beam/artifact/options.go | 48 ++++++++ sdks/go/pkg/beam/artifact/options_test.go | 78 +++++++++++++ sdks/java/container/boot.go | 3 + .../runners/dataflow/internal/apiclient.py | 5 +- .../dataflow/internal/apiclient_test.py | 42 ++++--- sdks/python/container/boot.go | 3 + sdks/typescript/container/boot.go | 3 + 10 files changed, 316 insertions(+), 25 deletions(-) create mode 100644 sdks/go/pkg/beam/artifact/options.go create mode 100644 sdks/go/pkg/beam/artifact/options_test.go diff --git a/sdks/go/container/boot.go b/sdks/go/container/boot.go index ab2da3169319..469285821f7e 100644 --- a/sdks/go/container/boot.go +++ b/sdks/go/container/boot.go @@ -149,6 +149,7 @@ func main() { log.Fatalf("Endpoint not set: %v", err) } logger := &tools.Logger{Endpoint: *loggingEndpoint} + log.SetOutput(tools.NewBufferedLoggerWithFlushInterval(ctx, logger, 0)) logger.Printf(ctx, "Initializing Go harness: %v", strings.Join(os.Args, " ")) // (1) Obtain the pipeline options @@ -158,6 +159,9 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Inject artifact validation enabled state into context + ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + // (2) Retrieve the staged files. // // The Go SDK harness downloads the worker binary and invokes diff --git a/sdks/go/pkg/beam/artifact/materialize.go b/sdks/go/pkg/beam/artifact/materialize.go index 624e30efcd2b..db624f3776af 100644 --- a/sdks/go/pkg/beam/artifact/materialize.go +++ b/sdks/go/pkg/beam/artifact/materialize.go @@ -51,6 +51,23 @@ const ( NoArtifactsStaged = "__no_artifacts_staged__" ) +type validationKey string + +const artifactValidationKey validationKey = "artifact_validation_enabled" + +// WithArtifactValidation returns a new context carrying the artifact validation enabled state. +func WithArtifactValidation(ctx context.Context, enabled bool) context.Context { + return context.WithValue(ctx, artifactValidationKey, enabled) +} + +// isArtifactValidationEnabled parses pipeline options to check if "disable_integrity_checks" is enabled. +func isArtifactValidationEnabled(ctx context.Context) bool { + if val, ok := ctx.Value(artifactValidationKey).(bool); ok { + return val + } + return true +} + // Materialize is a convenience helper for ensuring that all artifacts are // present and uncorrupted. It interprets each artifact name as a relative // path under the dest directory. It does not retrieve valid artifacts already @@ -131,6 +148,7 @@ func newMaterializeWithClient(ctx context.Context, client jobpb.ArtifactRetrieva RoleUrn: URNStagingTo, RolePayload: rolePayload, }, + expectedSha256: filePayload.Sha256, }) } @@ -183,8 +201,9 @@ func MustExtractFilePayload(artifact *pipepb.ArtifactInformation) (string, strin } type artifact struct { - client jobpb.ArtifactRetrievalServiceClient - dep *pipepb.ArtifactInformation + client jobpb.ArtifactRetrievalServiceClient + dep *pipepb.ArtifactInformation + expectedSha256 string } func (a artifact) retrieve(ctx context.Context, dest string) error { @@ -231,7 +250,19 @@ func (a artifact) retrieve(ctx context.Context, dest string) error { stat, _ := fd.Stat() log.Printf("Downloaded: %v (sha256: %v, size: %v)", filename, sha256Hash, stat.Size()) - return fd.Close() + if err := fd.Close(); err != nil { + return err + } + + if isArtifactValidationEnabled(ctx) { + if a.expectedSha256 == "" { + log.Printf("WARN: Artifact validation skipped for file: %v", filename) + } else if sha256Hash != a.expectedSha256 { + return errors.Errorf("bad SHA256 for %v: %v, want %v", filename, sha256Hash, a.expectedSha256) + } + } + + return nil } func writeChunks(stream jobpb.ArtifactRetrievalService_GetArtifactClient, w io.Writer) (string, error) { @@ -442,8 +473,12 @@ func retrieve(ctx context.Context, client jobpb.LegacyArtifactRetrievalServiceCl } // Artifact Sha256 hash is an optional field in metadata so we should only validate when its present. - if a.Sha256 != "" && sha256Hash != a.Sha256 { - return errors.Errorf("bad SHA256 for %v: %v, want %v", filename, sha256Hash, a.Sha256) + if isArtifactValidationEnabled(ctx) { + if a.Sha256 == "" { + log.Printf("WARN: Artifact validation skipped for file: %v", filename) + } else if sha256Hash != a.Sha256 { + return errors.Errorf("bad SHA256 for %v: %v, want %v", filename, sha256Hash, a.Sha256) + } } return nil } diff --git a/sdks/go/pkg/beam/artifact/materialize_test.go b/sdks/go/pkg/beam/artifact/materialize_test.go index 31890ed045cc..bf27e13e8a89 100644 --- a/sdks/go/pkg/beam/artifact/materialize_test.go +++ b/sdks/go/pkg/beam/artifact/materialize_test.go @@ -82,6 +82,52 @@ func TestMultiRetrieve(t *testing.T) { } } +func TestRetrieveWithBadShaFails(t *testing.T) { + cc := startServer(t) + defer cc.Close() + + ctx := grpcx.WriteWorkerID(context.Background(), "idA") + keys := []string{"foo"} + st := "whatever" + rt, artifacts := populate(ctx, cc, t, keys, 300, st) + + dst := makeTempDir(t) + defer os.RemoveAll(dst) + + client := jobpb.NewLegacyArtifactRetrievalServiceClient(cc) + for _, a := range artifacts { + a.Sha256 = "badhash" // mutate hash + if err := Retrieve(ctx, client, a, rt, dst); err == nil { + t.Errorf("expected materialization to fail due to bad sha256 mismatch") + } + } +} + +func TestRetrieveWithBadShaAndExperimentSucceeds(t *testing.T) { + cc := startServer(t) + defer cc.Close() + + ctx := WithArtifactValidation(grpcx.WriteWorkerID(context.Background(), "idA"), false) + keys := []string{"foo"} + st := "whatever" + rt, artifacts := populate(ctx, cc, t, keys, 300, st) + + dst := makeTempDir(t) + defer os.RemoveAll(dst) + + client := jobpb.NewLegacyArtifactRetrievalServiceClient(cc) + for _, a := range artifacts { + originalHash := a.Sha256 + a.Sha256 = "badhash" // mutate hash + filename := makeFilename(dst, a.Name) + if err := Retrieve(ctx, client, a, rt, dst); err != nil { + t.Errorf("materialize failed but should have succeeded because validation was disabled via experiment: %v", err) + continue + } + verifySHA256(t, filename, originalHash) + } +} + // populate stages a set of artifacts with the given keys, each with // slightly different sizes and chucksizes. func populate(ctx context.Context, cc *grpc.ClientConn, t *testing.T, keys []string, size int, st string) (string, []*jobpb.ArtifactMetadata) { @@ -266,6 +312,55 @@ func TestNewRetrieveWithResolution(t *testing.T) { checkStagedFiles(mds, dest, expected, t) } +func TestIsArtifactValidationEnabled(t *testing.T) { + ctx := context.Background() + if !isArtifactValidationEnabled(ctx) { + t.Errorf("empty context should have validation enabled") + } + + ctx2 := WithArtifactValidation(ctx, false) + if isArtifactValidationEnabled(ctx2) { + t.Errorf("context with validation disabled should have validation disabled") + } +} + +func TestNewRetrieveWithBadShaFails(t *testing.T) { + expected := map[string]string{"a.txt": "a"} + client := &fakeRetrievalService{artifacts: expected} + dest := makeTempDir(t) + defer os.RemoveAll(dest) + ctx := grpcx.WriteWorkerID(context.Background(), "worker") + + _, err := newMaterializeWithClient(ctx, client, client.fileArtifactsWithBadSha(), dest) + if err == nil { + t.Fatalf("expected materialization to fail due to bad sha256 mismatch") + } +} + +func TestNewRetrieveWithBadShaAndExperimentSucceeds(t *testing.T) { + expected := map[string]string{"a.txt": "a"} + client := &fakeRetrievalService{artifacts: expected} + dest := makeTempDir(t) + defer os.RemoveAll(dest) + + ctx := WithArtifactValidation(grpcx.WriteWorkerID(context.Background(), "worker"), false) + + mds, err := newMaterializeWithClient(ctx, client, client.fileArtifactsWithBadSha(), dest) + if err != nil { + t.Fatalf("materialize failed but should have succeeded because validation was disabled via experiment: %v", err) + } + + generated := make(map[string]string) + for _, md := range mds { + name, _ := MustExtractFilePayload(md) + payload, _ := proto.Marshal(&pipepb.ArtifactStagingToRolePayload{ + StagedName: name}) + generated[name] = string(payload) + } + + checkStagedFiles(mds, dest, generated, t) +} + func checkStagedFiles(mds []*pipepb.ArtifactInformation, dest string, expected map[string]string, t *testing.T) { if len(mds) != len(expected) { t.Errorf("wrong number of artifacts staged %v vs %v", len(mds), len(expected)) @@ -323,6 +418,21 @@ func (fake *fakeRetrievalService) fileArtifactsWithoutStagingTo() []*pipepb.Arti return artifacts } +func (fake *fakeRetrievalService) fileArtifactsWithBadSha() []*pipepb.ArtifactInformation { + var artifacts []*pipepb.ArtifactInformation + for name := range fake.artifacts { + payload, _ := proto.Marshal(&pipepb.ArtifactFilePayload{ + Path: filepath.Join("/tmp", name), + Sha256: "badhash", + }) + artifacts = append(artifacts, &pipepb.ArtifactInformation{ + TypeUrn: URNFileArtifact, + TypePayload: payload, + }) + } + return artifacts +} + func (fake *fakeRetrievalService) urlArtifactsWithoutStagingTo() []*pipepb.ArtifactInformation { var artifacts []*pipepb.ArtifactInformation for name := range fake.artifacts { diff --git a/sdks/go/pkg/beam/artifact/options.go b/sdks/go/pkg/beam/artifact/options.go new file mode 100644 index 000000000000..47356433161c --- /dev/null +++ b/sdks/go/pkg/beam/artifact/options.go @@ -0,0 +1,48 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package artifact + +import ( + structpb "google.golang.org/protobuf/types/known/structpb" +) + +// GetExperiments extracts a list of experiments from the pipeline options. +func GetExperiments(options *structpb.Struct) []string { + if options == nil { + return nil + } + + var exps []string + // Try legacy style + for _, v := range options.GetFields()["options"].GetStructValue().GetFields()["experiments"].GetListValue().GetValues() { + exps = append(exps, v.GetStringValue()) + } + // Try URN style + for _, v := range options.GetFields()["beam:option:experiments:v1"].GetListValue().GetValues() { + exps = append(exps, v.GetStringValue()) + } + return exps +} + +// HasExperiment checks if a specific experiment is enabled in the pipeline options. +func HasExperiment(options *structpb.Struct, experiment string) bool { + for _, exp := range GetExperiments(options) { + if exp == experiment { + return true + } + } + return false +} diff --git a/sdks/go/pkg/beam/artifact/options_test.go b/sdks/go/pkg/beam/artifact/options_test.go new file mode 100644 index 000000000000..a9f0e4bb7e35 --- /dev/null +++ b/sdks/go/pkg/beam/artifact/options_test.go @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package artifact + +import ( + "testing" + + structpb "google.golang.org/protobuf/types/known/structpb" +) + +func TestGetExperiments_Nil(t *testing.T) { + if got := GetExperiments(nil); got != nil { + t.Errorf("GetExperiments(nil) = %v, want nil", got) + } +} + +func TestGetExperiments_Legacy(t *testing.T) { + options, _ := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "experiments": []interface{}{"exp1", "exp2"}, + }, + }) + exps := GetExperiments(options) + if len(exps) != 2 || exps[0] != "exp1" || exps[1] != "exp2" { + t.Errorf("GetExperiments() = %v, want [exp1 exp2]", exps) + } +} + +func TestGetExperiments_URN(t *testing.T) { + urnOptions, _ := structpb.NewStruct(map[string]interface{}{ + "beam:option:experiments:v1": []interface{}{"expA", "expB"}, + }) + expsURN := GetExperiments(urnOptions) + if len(expsURN) != 2 || expsURN[0] != "expA" || expsURN[1] != "expB" { + t.Errorf("GetExperiments() = %v, want [expA expB]", expsURN) + } +} + +func TestHasExperiment(t *testing.T) { + options, _ := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "experiments": []interface{}{"exp1", "exp2"}, + }, + }) + + if !HasExperiment(options, "exp1") { + t.Errorf("HasExperiment(exp1) = false, want true") + } + if HasExperiment(options, "exp3") { + t.Errorf("HasExperiment(exp3) = true, want false") + } +} + +func TestGetExperiments_Combined(t *testing.T) { + options, _ := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "experiments": []interface{}{"exp1", "exp2"}, + }, + "beam:option:experiments:v1": []interface{}{"expA", "expB"}, + }) + exps := GetExperiments(options) + if len(exps) != 4 || exps[0] != "exp1" || exps[1] != "exp2" || exps[2] != "expA" || exps[3] != "expB" { + t.Errorf("GetExperiments() = %v, want [exp1 exp2 expA expB]", exps) + } +} diff --git a/sdks/java/container/boot.go b/sdks/java/container/boot.go index 8c918f231797..3ce79e4927eb 100644 --- a/sdks/java/container/boot.go +++ b/sdks/java/container/boot.go @@ -105,6 +105,9 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Inject artifact validation enabled state into context + ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + // (2) Retrieve the staged user jars. We ignore any disk limit, // because the staged jars are mandatory. diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index f38a2ee34bbf..097523a5131c 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -600,8 +600,9 @@ def _stage_resources(self, pipeline, options): else: remote_name = os.path.basename(type_payload.path) is_staged_role = False - - if self._enable_caching and not type_payload.sha256: + # compute sha256 even if caching is disabled. + # This is used to check the payload integrity along with caching. + if not type_payload.sha256: type_payload.sha256 = self._compute_sha256(type_payload.path) if type_payload.sha256 and type_payload.sha256 in staged_hashes: diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index 43f4c8a21513..43f51d0b39fd 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -1375,13 +1375,19 @@ def test_stage_resources(self): ]) })) client = apiclient.DataflowApplicationClient(pipeline_options) - with mock.patch.object(apiclient._LegacyDataflowStager, - 'stage_job_resources') as mock_stager: - client._stage_resources(pipeline, pipeline_options) + with mock.patch.object(apiclient.DataflowApplicationClient, + '_compute_sha256', + side_effect=lambda path: 'hash' + path): + with mock.patch.object(apiclient._LegacyDataflowStager, + 'stage_job_resources') as mock_stager: + client._stage_resources(pipeline, pipeline_options) mock_stager.assert_called_once_with( - [('/tmp/foo1', 'foo1', ''), ('/tmp/bar1', 'bar1', ''), - ('/tmp/baz', 'baz1', ''), ('/tmp/renamed1', 'renamed1', 'abcdefg'), - ('/tmp/foo2', 'foo2', ''), ('/tmp/bar2', 'bar2', '')], + [('/tmp/foo1', 'foo1', 'hash/tmp/foo1'), + ('/tmp/bar1', 'bar1', 'hash/tmp/bar1'), + ('/tmp/baz', 'baz1', 'hash/tmp/baz'), + ('/tmp/renamed1', 'renamed1', 'abcdefg'), + ('/tmp/foo2', 'foo2', 'hash/tmp/foo2'), + ('/tmp/bar2', 'bar2', 'hash/tmp/bar2')], staging_location='gs://test-location/staging') pipeline_expected = beam_runner_api_pb2.Pipeline( @@ -1392,8 +1398,8 @@ def test_stage_resources(self): beam_runner_api_pb2.ArtifactInformation( type_urn=common_urns.artifact_types.URL.urn, type_payload=beam_runner_api_pb2.ArtifactUrlPayload( - url='gs://test-location/staging/foo1' - ).SerializeToString(), + url='gs://test-location/staging/foo1', + sha256='hash/tmp/foo1').SerializeToString(), role_urn=common_urns.artifact_roles.STAGING_TO.urn, role_payload=beam_runner_api_pb2. ArtifactStagingToRolePayload( @@ -1401,8 +1407,8 @@ def test_stage_resources(self): beam_runner_api_pb2.ArtifactInformation( type_urn=common_urns.artifact_types.URL.urn, type_payload=beam_runner_api_pb2.ArtifactUrlPayload( - url='gs://test-location/staging/bar1'). - SerializeToString(), + url='gs://test-location/staging/bar1', + sha256='hash/tmp/bar1').SerializeToString(), role_urn=common_urns.artifact_roles.STAGING_TO.urn, role_payload=beam_runner_api_pb2. ArtifactStagingToRolePayload( @@ -1410,8 +1416,8 @@ def test_stage_resources(self): beam_runner_api_pb2.ArtifactInformation( type_urn=common_urns.artifact_types.URL.urn, type_payload=beam_runner_api_pb2.ArtifactUrlPayload( - url='gs://test-location/staging/baz1'). - SerializeToString(), + url='gs://test-location/staging/baz1', + sha256='hash/tmp/baz').SerializeToString(), role_urn=common_urns.artifact_roles.STAGING_TO.urn, role_payload=beam_runner_api_pb2. ArtifactStagingToRolePayload( @@ -1431,8 +1437,8 @@ def test_stage_resources(self): beam_runner_api_pb2.ArtifactInformation( type_urn=common_urns.artifact_types.URL.urn, type_payload=beam_runner_api_pb2.ArtifactUrlPayload( - url='gs://test-location/staging/foo2'). - SerializeToString(), + url='gs://test-location/staging/foo2', + sha256='hash/tmp/foo2').SerializeToString(), role_urn=common_urns.artifact_roles.STAGING_TO.urn, role_payload=beam_runner_api_pb2. ArtifactStagingToRolePayload( @@ -1440,8 +1446,8 @@ def test_stage_resources(self): beam_runner_api_pb2.ArtifactInformation( type_urn=common_urns.artifact_types.URL.urn, type_payload=beam_runner_api_pb2.ArtifactUrlPayload( - url='gs://test-location/staging/bar2'). - SerializeToString(), + url='gs://test-location/staging/bar2', + sha256='hash/tmp/bar2').SerializeToString(), role_urn=common_urns.artifact_roles.STAGING_TO.urn, role_payload=beam_runner_api_pb2. ArtifactStagingToRolePayload( @@ -1449,8 +1455,8 @@ def test_stage_resources(self): beam_runner_api_pb2.ArtifactInformation( type_urn=common_urns.artifact_types.URL.urn, type_payload=beam_runner_api_pb2.ArtifactUrlPayload( - url='gs://test-location/staging/baz1'). - SerializeToString(), + url='gs://test-location/staging/baz1', + sha256='hash/tmp/baz').SerializeToString(), role_urn=common_urns.artifact_roles.STAGING_TO.urn, role_payload=beam_runner_api_pb2. ArtifactStagingToRolePayload( diff --git a/sdks/python/container/boot.go b/sdks/python/container/boot.go index a2655903a4b1..f5a37c9cca0a 100644 --- a/sdks/python/container/boot.go +++ b/sdks/python/container/boot.go @@ -184,6 +184,9 @@ func launchSDKProcess() error { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Inject artifact validation enabled state into context + ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + experiments := getExperiments(options) logger.Printf(ctx, "Experiments=%v", experiments) diff --git a/sdks/typescript/container/boot.go b/sdks/typescript/container/boot.go index 44f94f804330..95e26124facc 100644 --- a/sdks/typescript/container/boot.go +++ b/sdks/typescript/container/boot.go @@ -91,6 +91,9 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Inject artifact validation enabled state into context + ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + // (2) Retrieve and install the staged packages. dir := filepath.Join(*semiPersistDir, *id, "staged") From 5640d50bfd395e7cb46c72c841ba84e86bc19f86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:38:41 -0400 Subject: [PATCH 090/490] Bump hashicorp/setup-terraform from 3 to 4 (#38389) Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from 3 to 4. - [Release notes](https://github.com/hashicorp/setup-terraform/releases) - [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/setup-terraform/compare/v3...v4) --- updated-dependencies: - dependency-name: hashicorp/setup-terraform dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/beam_Infrastructure_UsersPermissions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_Infrastructure_UsersPermissions.yml b/.github/workflows/beam_Infrastructure_UsersPermissions.yml index 52678f3bd4e5..b084eec2a705 100644 --- a/.github/workflows/beam_Infrastructure_UsersPermissions.yml +++ b/.github/workflows/beam_Infrastructure_UsersPermissions.yml @@ -51,7 +51,7 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Install Terraform - uses: hashicorp/setup-terraform@v3 + uses: hashicorp/setup-terraform@v4 with: terraform_version: 1.12.2 - name: Initialize Terraform From e1b911f40a63e6e97982142517c7205d074415c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:40:27 -0400 Subject: [PATCH 091/490] Bump actions/checkout from 4 to 6 (#38393) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/beam_PostCommit_Python_Versions.yml | 2 +- .github/workflows/beam_PreCommit_Java_Dataflow.yml | 2 +- .github/workflows/deploy_release_candidate_pypi.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/beam_PostCommit_Python_Versions.yml b/.github/workflows/beam_PostCommit_Python_Versions.yml index b1fbc18cb6de..b5c4c8c999ea 100644 --- a/.github/workflows/beam_PostCommit_Python_Versions.yml +++ b/.github/workflows/beam_PostCommit_Python_Versions.yml @@ -88,7 +88,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Dataflow.yml index 542672a64a69..33aa194e2bfb 100644 --- a/.github/workflows/beam_PreCommit_Java_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Dataflow.yml @@ -77,7 +77,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java Dataflow Non-portable Worker PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/deploy_release_candidate_pypi.yaml b/.github/workflows/deploy_release_candidate_pypi.yaml index 67438bee43c6..9fed87d42304 100644 --- a/.github/workflows/deploy_release_candidate_pypi.yaml +++ b/.github/workflows/deploy_release_candidate_pypi.yaml @@ -30,7 +30,7 @@ jobs: PYPI_API_TOKEN=$(jq -r '.inputs.PYPI_API_TOKEN' $GITHUB_EVENT_PATH) echo "::add-mask::$PYPI_API_TOKEN" - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-environment-action with: From 7b38beec33a338ac77b3992708dafd7c17486e0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:42:29 -0400 Subject: [PATCH 092/490] Bump crazy-max/ghaction-import-gpg from 6.3.0 to 7.0.0 (#38387) Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 6.3.0 to 7.0.0. - [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases) - [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/e89d40939c28e39f97cf32126055eeae86ba74ec...2dc316deee8e90f13e1a351ab510b4d5bc0c82cd) --- updated-dependencies: - dependency-name: crazy-max/ghaction-import-gpg dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_release_candidate.yml | 8 ++++---- .github/workflows/finalize_release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index 410925229f41..de43a9ae5fb0 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -74,7 +74,7 @@ jobs: 11 - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Auth for nexus @@ -126,7 +126,7 @@ jobs: java-version: '11' - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: stage source @@ -193,7 +193,7 @@ jobs: disable-cache: true - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Install dependencies @@ -455,7 +455,7 @@ jobs: go-version: '1.26' - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Build prism artifacts diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index b6f1fde44567..388c9e61ebed 100644 --- a/.github/workflows/finalize_release.yml +++ b/.github/workflows/finalize_release.yml @@ -139,7 +139,7 @@ jobs: git config user.email actions@"$RUNNER_NAME".local - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Push tags From 3725d2c31f19d346fea065b5e26711bc7c62b81d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:43:49 -0400 Subject: [PATCH 093/490] Bump cloud.google.com/go/bigtable from 1.42.0 to 1.47.0 in /sdks (#38396) Bumps [cloud.google.com/go/bigtable](https://github.com/googleapis/google-cloud-go) from 1.42.0 to 1.47.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/documentai/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.42.0...pubsub/v1.47.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/bigtable dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 1f4904f33376..a4a40de44785 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -26,7 +26,7 @@ toolchain go1.26.2 require ( cloud.google.com/go/bigquery v1.77.0 - cloud.google.com/go/bigtable v1.42.0 + cloud.google.com/go/bigtable v1.47.0 cloud.google.com/go/datastore v1.22.0 cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.1 @@ -79,7 +79,7 @@ require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/monitoring v1.25.0 // indirect cloud.google.com/go/pubsub/v2 v2.0.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect @@ -97,7 +97,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index d6baa5b40793..d81f09d33004 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -46,8 +46,8 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.77.0 h1:L5AW3jhzEKpFVg4i0mVHxKpxogrqT7dczWBSr4m9MKU= cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= -cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= -cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= +cloud.google.com/go/bigtable v1.47.0 h1:NGLgDSr/i79BTGCjxH/maPKxyvl5q8/SsBsyLK52kdI= +cloud.google.com/go/bigtable v1.47.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= @@ -76,8 +76,8 @@ cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8 cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= -cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= -cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= +cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc= cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI= cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -359,8 +359,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= -github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= -github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= From 423f1cb67a3ddc20defffb8f17f590578ab1498c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:45:22 -0400 Subject: [PATCH 094/490] Bump docker/login-action from 4.0.0 to 4.1.0 (#38394) Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/b45d80f862d83dbcd57f89517bcf500b2ab88fb2...4907a6ddec9925e35a0a9e82d7399ccc52663121) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_release_candidate.yml | 2 +- .github/workflows/finalize_release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index de43a9ae5fb0..3304c189361d 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -293,7 +293,7 @@ jobs: # settings.xml file run: rm ~/.m2/settings.xml || true - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 388c9e61ebed..5979d91517f0 100644 --- a/.github/workflows/finalize_release.yml +++ b/.github/workflows/finalize_release.yml @@ -41,7 +41,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} From 1475c02a1a5bd2f9eb7d57e57dc5e84cc2b45e49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:45:53 -0400 Subject: [PATCH 095/490] Bump docker/build-push-action from 7.0.0 to 7.1.0 (#38391) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_runner_image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index 34f816949b5b..7990ce0a4859 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - name: Build and Load to docker - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: context: ${{ env.working-directory }} load: true @@ -57,7 +57,7 @@ jobs: - name: Push Docker image if: github.ref == 'refs/heads/master' id: docker_build - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: context: ${{ env.working-directory }} push: true From f4445840fa2c14dd1f500d7fb8e891102e0d6c45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 22:46:28 -0400 Subject: [PATCH 096/490] Bump google.golang.org/api from 0.276.0 to 0.278.0 in /sdks (#38395) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.276.0 to 0.278.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.276.0...v0.278.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.278.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 22 +++++++++++----------- sdks/go.sum | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index a4a40de44785..f85d9df93573 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -55,12 +55,12 @@ require ( github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/text v0.35.0 - google.golang.org/api v0.276.0 + golang.org/x/sys v0.43.0 + golang.org/x/text v0.36.0 + google.golang.org/api v0.278.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 @@ -133,7 +133,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect - golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect golang.org/x/time v0.15.0 // indirect ) @@ -175,8 +175,8 @@ require ( github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.21.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -202,10 +202,10 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/tools v0.43.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index d81f09d33004..0e4d8db3d92f 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -545,15 +545,15 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= -github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -939,8 +939,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -993,8 +993,8 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1050,8 +1050,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1174,10 +1174,10 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1186,8 +1186,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1202,8 +1202,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1278,8 +1278,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1340,8 +1340,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= -google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= +google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= +google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1439,8 +1439,8 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From ae13a80be1e4956bf168516e077df1b6143d1324 Mon Sep 17 00:00:00 2001 From: bambadiouf1 Date: Wed, 6 May 2026 12:03:08 -0700 Subject: [PATCH 097/490] Fix FHIR search method signature mismatch after google.golang.org/api update --- sdks/go/pkg/beam/io/fhirio/common.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go/pkg/beam/io/fhirio/common.go b/sdks/go/pkg/beam/io/fhirio/common.go index 504e65270d58..9f7e39bf0efb 100644 --- a/sdks/go/pkg/beam/io/fhirio/common.go +++ b/sdks/go/pkg/beam/io/fhirio/common.go @@ -127,12 +127,12 @@ func (c *fhirStoreClientImpl) search(storePath, resourceType string, queries map queryParams = append(queryParams, googleapi.QueryParameter(pageTokenParameterKey, pageToken)) } - // Pass nil as the body because search parameters are passed via queryParams, + // Pass an empty reader as the body because search parameters are passed via queryParams, // and the new API expects an io.Reader instead of a specific struct. if resourceType == "" { - return c.fhirService().Search(storePath, nil).Do(queryParams...) + return c.fhirService().Search(storePath, strings.NewReader("")).Do(queryParams...) } - return c.fhirService().SearchType(storePath, resourceType, nil).Do(queryParams...) + return c.fhirService().SearchType(storePath, resourceType, strings.NewReader("")).Do(queryParams...) } func (c *fhirStoreClientImpl) deidentify(srcStorePath, dstStorePath string, deidConfig *healthcare.DeidentifyConfig) (operationResults, error) { From f66955f812fd278012d641252be0655ce5f56f70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:10:32 -0400 Subject: [PATCH 098/490] Bump actions/github-script from 8 to 9 (#38402) Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v8...v9) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/assign_milestone.yml | 2 +- .github/workflows/self-assign.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/assign_milestone.yml b/.github/workflows/assign_milestone.yml index 300b990bf99c..542d2a551f64 100644 --- a/.github/workflows/assign_milestone.yml +++ b/.github/workflows/assign_milestone.yml @@ -35,7 +35,7 @@ jobs: with: fetch-depth: 2 - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: script: | const fs = require('fs') diff --git a/.github/workflows/self-assign.yml b/.github/workflows/self-assign.yml index 13459bbfa986..17bf72c1722e 100644 --- a/.github/workflows/self-assign.yml +++ b/.github/workflows/self-assign.yml @@ -25,7 +25,7 @@ jobs: if: ${{ !github.event.issue.pull_request }} runs-on: ubuntu-latest steps: - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: script: | const body = context.payload.comment.body.replace( /\r\n/g, " " ).replace( /\n/g, " " ).split(' '); From 4ced82d2186d47173d40d2f62bc94f5d01b49072 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:11:00 -0400 Subject: [PATCH 099/490] Bump actions/download-artifact from 7 to 8 (#38401) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index bdee24af7afa..4db227385c77 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -202,7 +202,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' steps: - name: Download compressed sources from artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: source_zip path: source/ @@ -231,13 +231,13 @@ jobs: py_version: ["cp310-", "cp311-", "cp312-", "cp313-", "cp314-"] steps: - name: Download python source distribution from artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: source path: apache-beam-source - name: Download Python SDK RC source distribution from artifacts if: ${{ needs.build_source.outputs.is_rc == 1 }} - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: source_rc${{ needs.build_source.outputs.rc_num }} path: apache-beam-source-rc @@ -314,7 +314,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' && github.event_name != 'pull_request' steps: - name: Download wheels from artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: wheelhouse-* merge-multiple: true From fc48fe1351d808534ac7d072cafa779276b21de3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:11:29 -0400 Subject: [PATCH 100/490] Bump actions/upload-artifact from 4 to 7 (#38400) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/beam_PostCommit_Python_Versions.yml | 2 +- .github/workflows/beam_PreCommit_Java_Dataflow.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/beam_PostCommit_Python_Versions.yml b/.github/workflows/beam_PostCommit_Python_Versions.yml index b5c4c8c999ea..c0f79faefdda 100644 --- a/.github/workflows/beam_PostCommit_Python_Versions.yml +++ b/.github/workflows/beam_PostCommit_Python_Versions.yml @@ -134,7 +134,7 @@ jobs: -PpythonVersion=${{ matrix.python_version }} \ -Pposargs="--ignore=apache_beam/ml/" \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Java_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Dataflow.yml index 33aa194e2bfb..53044970d117 100644 --- a/.github/workflows/beam_PreCommit_Java_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Dataflow.yml @@ -99,7 +99,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -113,7 +113,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -125,7 +125,7 @@ jobs: name: Publish SpotBugs path: '**/build/reports/spotbugs/*.html' - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' From d48ec7543ab32ae9b138597a420ea9bfc61835cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:12:10 -0400 Subject: [PATCH 101/490] Bump google.golang.org/grpc from 1.80.0 to 1.81.0 in /sdks (#38399) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.80.0 to 1.81.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.81.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.81.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 8 ++++---- sdks/go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index f85d9df93573..345b316fa601 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -62,7 +62,7 @@ require ( golang.org/x/text v0.36.0 google.golang.org/api v0.278.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 - google.golang.org/grpc v1.80.0 + google.golang.org/grpc v1.81.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -123,7 +123,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.einride.tech/aip v0.73.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect @@ -162,11 +162,11 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/docker/docker v28.5.2+incompatible // but required to resolve issue docker has with go1.20 github.com/docker/go-units v0.5.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 0e4d8db3d92f..3ffeb652d9a6 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -305,8 +305,8 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/colinmarc/hdfs/v2 v2.1.1/go.mod h1:M3x+k8UKKmxtFu++uAZ0OtDU8jR3jnaZIAc6yK4Ue0c= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -364,8 +364,8 @@ github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= -github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -871,8 +871,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= @@ -1470,8 +1470,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From d195200e32b9732338ad9f63a1967a81e8c7f8d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:12:48 -0400 Subject: [PATCH 102/490] Bump github.com/lib/pq from 1.11.1 to 1.12.3 in /sdks (#38398) Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.11.1 to 1.12.3. - [Release notes](https://github.com/lib/pq/releases) - [Changelog](https://github.com/lib/pq/blob/master/CHANGELOG.md) - [Commits](https://github.com/lib/pq/compare/v1.11.1...v1.12.3) --- updated-dependencies: - dependency-name: github.com/lib/pq dependency-version: 1.12.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 345b316fa601..cf0ba9f8c629 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -44,7 +44,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 - github.com/lib/pq v1.11.1 + github.com/lib/pq v1.12.3 github.com/linkedin/goavro/v2 v2.15.0 github.com/nats-io/nats-server/v2 v2.12.6 github.com/nats-io/nats.go v1.49.0 diff --git a/sdks/go.sum b/sdks/go.sum index 3ffeb652d9a6..13d19d59d665 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -666,8 +666,8 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= -github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolMjgVI= github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= From 393b8fd7273ce3a79414a4c1ac7f40158b41918b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:13:33 -0400 Subject: [PATCH 103/490] Bump github.com/nats-io/nats-server/v2 from 2.12.6 to 2.14.0 in /sdks (#38397) Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.12.6 to 2.14.0. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md) - [Commits](https://github.com/nats-io/nats-server/compare/v2.12.6...v2.14.0) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 10 +++++----- sdks/go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index cf0ba9f8c629..1735d283fcf3 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -46,8 +46,8 @@ require ( github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 github.com/lib/pq v1.12.3 github.com/linkedin/goavro/v2 v2.15.0 - github.com/nats-io/nats-server/v2 v2.12.6 - github.com/nats-io/nats.go v1.49.0 + github.com/nats-io/nats-server/v2 v2.14.0 + github.com/nats-io/nats.go v1.51.0 github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 github.com/testcontainers/testcontainers-go v0.40.0 @@ -87,7 +87,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect - github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect + github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect @@ -104,7 +104,7 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect - github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect + github.com/minio/highwayhash v1.0.4 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/sys/user v0.4.0 // indirect @@ -180,7 +180,7 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/patternmatcher v0.6.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 13d19d59d665..cc331b6238a7 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -177,8 +177,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= -github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/apache/arrow/go/arrow v0.0.0-20200730104253-651201b0f516/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= @@ -640,8 +640,8 @@ github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= @@ -682,8 +682,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= -github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= -github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.34/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= @@ -722,10 +722,10 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8= -github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo= -github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= -github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= +github.com/nats-io/nats-server/v2 v2.14.0 h1:+8q0HrDFotwLLcGH/legOEOnowunhK+aZ4GYBIWpQlM= +github.com/nats-io/nats-server/v2 v2.14.0/go.mod h1:ImVUUDvfClJbb6cuJQRc1VmgDCXKM5ds0OoiG9MVOKo= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= From 4a5672a94cb310462623413d426b75e9756bffff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 09:54:31 -0400 Subject: [PATCH 104/490] Bump cloud.google.com/go/datastore from 1.22.0 to 1.23.0 in /sdks (#38392) Bumps [cloud.google.com/go/datastore](https://github.com/googleapis/google-cloud-go) from 1.22.0 to 1.23.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/documentai/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/kms/v1.22.0...kms/v1.23.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/datastore dependency-version: 1.23.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 1735d283fcf3..4f3c5fc483a8 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -27,7 +27,7 @@ toolchain go1.26.2 require ( cloud.google.com/go/bigquery v1.77.0 cloud.google.com/go/bigtable v1.47.0 - cloud.google.com/go/datastore v1.22.0 + cloud.google.com/go/datastore v1.23.0 cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.1 cloud.google.com/go/spanner v1.88.0 diff --git a/sdks/go.sum b/sdks/go.sum index cc331b6238a7..30822a755a7b 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -58,8 +58,8 @@ cloud.google.com/go/datacatalog v1.27.0 h1:AnghhtHKCqYIe62gTPHcn9nJr5jtxjZHV4D/F cloud.google.com/go/datacatalog v1.27.0/go.mod h1:YTI11pFlC5HCj4CphEf+qWCy/z9udd7o0HVN6c2Povg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.22.0 h1:FOyx2Ag6ibD2wFkz9S8EiNrmBugia8pQOfpyJxi2yqA= -cloud.google.com/go/datastore v1.22.0/go.mod h1:aopSX+Whx0lHspWWBj+AjWt68/zjYsPfDe3LjWtqZg8= +cloud.google.com/go/datastore v1.23.0 h1:mAlWN3tnQe1OqVM3UtYBIbWTz9aU83RgW4hXOrfm9P8= +cloud.google.com/go/datastore v1.23.0/go.mod h1:bOvQQekv4VACRJmH/MBy12MT6M3udfTuCyxw+tzY+8s= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= From 63c7bf39e433d55e9f163775cd9451316f7e6158 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 7 May 2026 17:52:55 +0200 Subject: [PATCH 105/490] Stabilize StorageApiDataTriggeredSchemaUpdateIT assertion (#38339) * Stabilize StorageApiDataTriggeredSchemaUpdateIT assertion * Stabilize GcsMatchIT and StorageApiDataTriggeredSchemaUpdateIT on Dataflow * Stabilize Dataflow schema update and GCS match ITs * Harden GcsMatchIT sum bounds with explicit long arithmetic --- .../beam_PostCommit_Java_DataflowV1.json | 2 +- ...StorageApiDataTriggeredSchemaUpdateIT.java | 26 +++++++++++++++++-- .../beam/sdk/io/gcp/storage/GcsMatchIT.java | 8 ++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json b/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json index 5e7fbb916f4b..ae6cb268ff68 100644 --- a/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json +++ b/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json @@ -3,6 +3,6 @@ "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", "https://github.com/apache/beam/pull/35177": "Introducing WindowedValueReceiver to runners", "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 4, + "modification": 9, "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" } diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java index b9ab25ec5810..1d35fea44966 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java @@ -19,6 +19,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects.firstNonNull; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import com.google.api.services.bigquery.model.Table; import com.google.api.services.bigquery.model.TableFieldSchema; @@ -46,6 +47,7 @@ import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.transforms.WithKeys; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -264,7 +266,8 @@ private void runTest(Write.Method method) throws Exception { write = write .withTriggeringFrequency(Duration.standardSeconds(1)) - .withNumStorageWriteApiStreams(2); + // One stream — same as other Storage Write ITs here, fewer ordering surprises. + .withNumStorageWriteApiStreams(1); } SequenceRowsDoFn doFn = new SequenceRowsDoFn(5, 20); @@ -281,7 +284,9 @@ private void runTest(Write.Method method) throws Exception { .apply( MapElements.into(TypeDescriptor.of(TableRow.class)) .via(BigQueryStorageApiInsertError::getRow)); - PAssert.that(failedInserts).containsInAnyOrder(doFn.getRow(20)); + // Schema upgrades can race with evolved rows; allow extra DLQ rows but require the + // intentionally malformed row shape to appear. + PAssert.that(failedInserts).satisfies(new VerifyContainsMalformedReqRow()); p.run().waitUntilFinish(); @@ -329,6 +334,23 @@ abstract static class VerificationInfo { abstract int getExpectedCount(); } + private static final class VerifyContainsMalformedReqRow + implements SerializableFunction, Void> { + @Override + public Void apply(Iterable rows) { + boolean sawBadReqShape = false; + for (TableRow row : rows) { + Object reqValue = row.get("req"); + if (reqValue instanceof List && ((List) reqValue).size() == 2) { + sawBadReqShape = true; + break; + } + } + assertTrue("DLQ should include the malformed req row", sawBadReqShape); + return null; + } + } + private void verifyDataWritten(String tableSpec, List verifications) throws IOException, InterruptedException { for (VerificationInfo verification : verifications) { diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java index 332aa067b0eb..4c73e3e2a68f 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java @@ -134,8 +134,12 @@ public Void apply(Iterable input) { assertEquals(1, countFirst); // file "second" is expected to appear more than once assertEquals(true, countSecond > 1); - // file "second" is expected to appear in growing sizes each time by one byte - assertEquals((maxSecondSize * 2L - countSecond + 1) * countSecond / 2, sumSecondSize); + // Continuous matching sometimes skips a middle size on Dataflow; sum should still fit + // between "all sizes seen" and "every size from 1..maxSecondSize". + long minPossibleSum = (countSecond - 1) * countSecond / 2L + maxSecondSize; + long maxPossibleContiguousSum = (maxSecondSize * 2L - countSecond + 1) * countSecond / 2L; + assertEquals(true, sumSecondSize <= maxPossibleContiguousSum); + assertEquals(true, sumSecondSize >= minPossibleSum); return null; } From 73da8d41efe2defd9808b601c519efac14576274 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Thu, 7 May 2026 13:09:18 -0400 Subject: [PATCH 106/490] Consolidate linting and type checking configs into pyproject.toml (#38306) * Consolidate linting and type checking configs into pyproject.toml * Restore dropped comments * add builtins check back --- sdks/python/.isort.cfg | 58 ------------- sdks/python/pyproject.toml | 172 +++++++++++++++++++++++++++++++++++++ sdks/python/pyrefly.toml | 60 ------------- sdks/python/ruff.toml | 96 --------------------- 4 files changed, 172 insertions(+), 214 deletions(-) delete mode 100644 sdks/python/.isort.cfg delete mode 100644 sdks/python/pyrefly.toml delete mode 100644 sdks/python/ruff.toml diff --git a/sdks/python/.isort.cfg b/sdks/python/.isort.cfg deleted file mode 100644 index a29f98cc90be..000000000000 --- a/sdks/python/.isort.cfg +++ /dev/null @@ -1,58 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the License); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an AS IS BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -[settings] -py_version=310 -line_length=120 -old_finders=true -order_by_type=true -force_single_line=true -combine_star=true -src_paths=apache_beam -extra_standard_library=dataclasses -known_third_party=yaml -skip=apiclient.py, - avroio_test.py, - cloudpickle.py, - datastore_wordcount.py, - datastoreio_test.py, - doctests_test.py, - fast_coders_test.py, - hadoopfilesystem.py, - iobase_test.py, - main_test.py, - model.py, - preprocess.py, - process_tfma.py, - render_test.py, - slow_coders_test.py, - taxi.py, - tfdv_analyze_and_validate.py, - yaml/main.py, - main_test.py, - yaml_testing_test.py, - bigquery_v2_client.py, - bigquery_v2_messages.py, - dataflow_v1b3_client.py, - dataflow_v1b3_messages.py, - storage_v1_client.py, - storage_v1_messages.py, - proto2_coder_test_messages_pb2.py, - cloudbuild_v1_client.py, - cloudbuild_v1_messages.py, - boto3_client.py, -skip_glob=*.pxd,*.pyx,*pb2*.py,**/examples/**/*.py,**/portability/api/**/*.py,**/portability/api/__init__.py \ No newline at end of file diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index c764298ebae3..636849f54c9e 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -44,3 +44,175 @@ requires = [ # legacy installation is needed to generate `apache_beam.portability.api` package. build-backend = "setuptools.build_meta" + +[tool.isort] +py_version = 310 +line_length = 120 +old_finders = true +order_by_type = true +force_single_line = true +combine_star = true +src_paths = ["apache_beam"] +extra_standard_library = ["dataclasses"] +known_third_party = ["yaml"] +skip = [ + "apiclient.py", + "avroio_test.py", + "cloudpickle.py", + "datastore_wordcount.py", + "datastoreio_test.py", + "doctests_test.py", + "fast_coders_test.py", + "hadoopfilesystem.py", + "iobase_test.py", + "main_test.py", + "model.py", + "preprocess.py", + "process_tfma.py", + "render_test.py", + "slow_coders_test.py", + "taxi.py", + "tfdv_analyze_and_validate.py", + "yaml/main.py", + "yaml_testing_test.py", + "bigquery_v2_client.py", + "bigquery_v2_messages.py", + "dataflow_v1b3_client.py", + "dataflow_v1b3_messages.py", + "storage_v1_client.py", + "storage_v1_messages.py", + "proto2_coder_test_messages_pb2.py", + "cloudbuild_v1_client.py", + "cloudbuild_v1_messages.py", + "boto3_client.py", +] +skip_glob = [ + "*.pxd", + "*.pyx", + "*pb2*.py", + "**/examples/**/*.py", + "**/portability/api/**/*.py", + "**/portability/api/__init__.py", +] + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "*.pxd", + "*.pyx", + "*pb2*.py", + "**/examples/**/*.py", + "**/examples/**/*.ipynb", + "**/portability/api/**/*.py", + "**/portability/api/__init__.py", +] +target-version = "py310" +src = ["apache_beam"] + +[tool.ruff.lint] +select = ["E9", "PL", "F821", "F822", "F823", "UP006"] +ignore = [ + # Ignored Pylint Checks + "PLC0415", # import-outside-toplevel + "PLR2004", # magic-value-comparison + "PLR0913", # too-many-arguments + "PLR0912", # too-many-branches + "PLW0108", # unnecessary-lambda + "PLW2901", # redefined-loop-name + "PLR0915", # too-many-statements + "PLR1714", # repeated-equality-comparison + "PLR0911", # too-many-return-statements + "PLR5501", # collapsible-else-if + "PLW0603", # global-statement + "PLR1730", # if-stmt-min-max + "PLW1641", # eq-without-hash + "PLW0602", # global-variable-not-assigned + "PLC1802", # len-test + "PLC3002", # unnecessary-direct-lambda-call + "PLW0642", # self-or-cls-assignment + "PLR1733", # unnecessary-dict-index-lookup + "PLR0402", # manual-from-import + "PLC0132", # type-param-name-mismatch + "PLC0206", # dict-index-missing-items + "PLC0207", # missing-maxsplit-arg + "PLR1704", # redefined-argument-from-local + "PLR1711", # useless-return + "PLW0406", # import-self + "PLW3301", # nested-min-max + "PLR2044", # empty-comment +] + +# Allow fix for all enabled rules (when --fix) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.pyrefly] +project-includes = ["apache_beam"] +python-version = "3.10.0" +ignore-missing-imports = ["*"] +untyped-def-behavior = "skip-and-infer-return-any" + +[tool.pyrefly.errors] +missing-attribute = "ignore" +invalid-annotation = "ignore" +redundant-condition = "ignore" +untyped-import = "ignore" +# Beam-specific supressions to keep pyrefly green +# These are in descending order of instances. +# +# TODO(https://github.com/apache/beam/issues/37699): Reduce the number of +# ignored checks. +bad-param-name-override = "ignore" +bad-override = "ignore" +bad-argument-type = "ignore" +unsupported-operation = "ignore" +implicit-import = "ignore" +bad-function-definition = "ignore" +bad-return = "ignore" +unbound-name = "ignore" +no-matching-overload = "ignore" +bad-assignment = "ignore" +missing-argument = "ignore" +bad-index = "ignore" +invalid-type-var = "ignore" +unknown-name = "ignore" +not-a-type = "ignore" +deprecated = "ignore" +not-callable = "ignore" +invalid-argument = "ignore" +invalid-inheritance = "ignore" +not-iterable = "ignore" +unexpected-keyword = "ignore" +bad-specialization = "ignore" +bad-context-manager = "ignore" +invalid-yield = "ignore" +bad-argument-count = "ignore" +bad-typed-dict-key = "ignore" diff --git a/sdks/python/pyrefly.toml b/sdks/python/pyrefly.toml deleted file mode 100644 index 6247d79264a6..000000000000 --- a/sdks/python/pyrefly.toml +++ /dev/null @@ -1,60 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -project-includes = ["apache_beam"] -python-version = "3.10.0" -ignore-missing-imports = ["*"] -untyped-def-behavior = "skip-and-infer-return-any" - -[errors] -missing-attribute = "ignore" -invalid-annotation = "ignore" -redundant-condition = "ignore" -untyped-import = "ignore" -# Beam-specific supressions to keep pyrefly green -# These are in descending order of instances, e.g there are -# fewer violations for the check on line 40 than for the check on line 35. -# violations. -# -# TODO(https://github.com/apache/beam/issues/37699): Reduce the number of -# ignored checks. -bad-param-name-override = "ignore" -bad-override = "ignore" -bad-argument-type = "ignore" -unsupported-operation = "ignore" -implicit-import = "ignore" -bad-function-definition = "ignore" -bad-return = "ignore" -unbound-name = "ignore" -no-matching-overload = "ignore" -bad-assignment = "ignore" -missing-argument = "ignore" -bad-index = "ignore" -invalid-type-var = "ignore" -unknown-name = "ignore" -not-a-type = "ignore" -deprecated = "ignore" -not-callable = "ignore" -invalid-argument = "ignore" -invalid-inheritance = "ignore" -not-iterable = "ignore" -unexpected-keyword = "ignore" -bad-specialization = "ignore" -bad-context-manager = "ignore" -invalid-yield = "ignore" -bad-argument-count = "ignore" -bad-typed-dict-key = "ignore" \ No newline at end of file diff --git a/sdks/python/ruff.toml b/sdks/python/ruff.toml deleted file mode 100644 index c0f896c21721..000000000000 --- a/sdks/python/ruff.toml +++ /dev/null @@ -1,96 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "site-packages", - "venv", - "*.pxd", - "*.pyx", - "*pb2*.py", - "**/examples/**/*.py", - "**/examples/**/*.ipynb", - "**/portability/api/**/*.py", - "**/portability/api/__init__.py", -] - -target-version = "py310" - -src = ["apache_beam"] - -[lint] -select = ["E9", "PL", "F821", "F822", "F823", "UP006"] -ignore = [ - # Ignored Pylint Checks - "PLC0415", # import-outside-toplevel - "PLR2004", # magic-value-comparison - "PLR0913", # too-many-arguments - "PLR0912", # too-many-branches - "PLW0108", # unnecessary-lambda - "PLW2901", # redefined-loop-name - "PLR0915", # too-many-statements - "PLR1714", # repeated-equality-comparison - "PLR0911", # too-many-return-statements - "PLR5501", # collapsible-else-if - "PLW0603", # global-statement - "PLR1730", # if-stmt-min-max - "PLW1641", # eq-without-hash - "PLW0602", # global-variable-not-assigned - "PLC1802", # len-test - "PLC3002", # unnecessary-direct-lambda-call - "PLW0642", # self-or-cls-assignment - "PLR1733", # unnecessary-dict-index-lookup - "PLR0402", # manual-from-import - "PLC0132", # type-param-name-mismatch - "PLC0206", # dict-index-missing-items - "PLC0207", # missing-maxsplit-arg - "PLR1704", # redefined-argument-from-local - "PLR1711", # useless-return - "PLW0406", # import-self - "PLW3301", # nested-min-max - "PLR2044", # empty-comment -] - -# Allow fix for all enabled rules (when `--fix`) is provided. -fixable = ["ALL"] -unfixable = [] - -# Allow unused variables when underscore-prefixed. -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" \ No newline at end of file From 2d50cefd925bae79e481da762096f9ef1ca0c58d Mon Sep 17 00:00:00 2001 From: Ganesh Sivakumar Date: Thu, 7 May 2026 23:54:45 +0530 Subject: [PATCH 107/490] BatchElements transform for Java SDK (#38369) * skeleton * calculate batch size * global window batching * window awae batching * unit tests * java api docs * gemini comments * checkstyle * fix spotbug * update doc and changes.md * checkstyle * whitespace --------- Co-authored-by: Ganeshsivakumar --- CHANGES.md | 3 +- .../beam/sdk/transforms/BatchElements.java | 601 ++++++++++++++++++ .../sdk/transforms/BatchElementsTest.java | 598 +++++++++++++++++ 3 files changed, 1201 insertions(+), 1 deletion(-) create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java create mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java diff --git a/CHANGES.md b/CHANGES.md index f9b9f1d28483..922c38ffef37 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -77,6 +77,7 @@ compatible. Both coders can decode encoded bytes from the other coder ([#38139](https://github.com/apache/beam/issues/38139)). * (Python) Added type alias for with_exception_handling to be used for typehints. ([#38173](https://github.com/apache/beam/issues/38173)). +* (Java) BatchElements transform for Java SDK ([#38369](https://github.com/apache/beam/issues/38369)) * Added plugin mechanism to support different Lineage implementations (Java) ([#36790](https://github.com/apache/beam/issues/36790)). * (Python) Supported Python user type in Beam SQL. For example, SQL statements like `SELECT some_field from PCOLLECTION` can now operate a PCollection of Beam Row containing pickable Python user type ([#20738](https://github.com/apache/beam/issues/20738)). * (Python) Introduced `beam.coders.registry.register_row` as preferred API to register a named tuple or dataclass with a Beam Row. At pipelne runtime, the original type associated with the registered row are preserved across the serialization boundary ([#38108](https://github.com/apache/beam/issues/38108)). @@ -2435,4 +2436,4 @@ Schema Options, it will be removed in version `2.23.0`. ([BEAM-9704](https://iss ## Highlights -- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). +- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). \ No newline at end of file diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java new file mode 100644 index 000000000000..35796d1b1385 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java @@ -0,0 +1,601 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.transforms; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import javax.annotation.Nullable; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.WindowingStrategy; + +/** + * A {@link PTransform} that batches elements for amortized processing. + * + *

    This transform is designed to precede operations whose processing cost is of the form: + * + *

    + *   time = fixed_cost + num_elements * per_element_cost
    + * 
    + * + *

    When the per-element cost is significantly smaller than the fixed cost, batching multiple + * elements together can amortize that fixed cost and improve overall throughput. + * + *

    The transform consumes a {@code PCollection} and produces a {@code PCollection>}, + * where each output element is a batch of input elements. + * + *

    This transform dynamically determines an optimal batch size between the configured minimum and + * maximum values by profiling the execution time of downstream (fused) operations. To enforce a + * fixed batch size, set {@code minBatchSize == maxBatchSize}. + * + *

    Elements are batched per window. Each emitted batch belongs to the same window as its elements + * and is assigned a timestamp at the end of that window. + * + *

    Example

    + * + *
    {@code
    + * // With default configuration
    + * pipeline
    + *     .apply("Create", Create.of(range(200)))
    + *     .apply("Batch", BatchElements.withDefaults())
    + *     .apply(...);
    + *
    + * // With custom configuration
    + * BatchElements.BatchConfig config =
    + *     BatchElements.BatchConfig.builder()
    + *         .withMinBatchSize(1)
    + *         .withMaxBatchSize(15)
    + *         .withTargetBatchDurationSecs(10.0)
    + *         .withTargetBatchOverhead(0.05)
    + *         .withVariance(0.0)
    + *         .build();
    + *
    + * pipeline
    + *     .apply("Create", Create.of(range(200)))
    + *     .apply("Batch", BatchElements.withConfig(config))
    + *     .apply(
    + *         "Sizes",
    + *         MapElements.via(
    + *             new SimpleFunction, Integer>() {
    + *               @Override
    + *               public Integer apply(List input) {
    + *                 return input.size();
    + *               }
    + *             }));
    + * }
    + * + * @param the type of input elements + */ +public class BatchElements extends PTransform, PCollection>> { + + private final BatchConfig config; + + private BatchElements(BatchConfig config) { + this.config = config; + } + + /** Batch Elements with default configuration. */ + public static BatchElements withDefaults() { + return withConfig(BatchConfig.defaults()); + } + + public static BatchElements withConfig(BatchConfig config) { + return new BatchElements<>(config); + } + /** + * Configuration for {@link BatchElements}. + * + *

    Controls how batch sizes are selected and adapted over time. + */ + public static final class BatchConfig implements Serializable { + final int minBatchSize; + final int maxBatchSize; + final double targetBatchOverhead; + final double targetBatchDurationSecs; + final double targetBatchDurationSecsWithFixedCost; + final double variance; + + private BatchConfig(Builder builder) { + this.minBatchSize = builder.minBatchSize; + this.maxBatchSize = builder.maxBatchSize; + this.targetBatchOverhead = builder.targetBatchOverhead; + this.targetBatchDurationSecs = builder.targetBatchDurationSecs; + this.targetBatchDurationSecsWithFixedCost = builder.targetBatchDurationSecsWithFixedCost; + this.variance = builder.variance; + } + + static BatchConfig defaults() { + return BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(10000) + .withTargetBatchOverhead(0.05) + .withTargetBatchDurationSecs(10.0) + .withVariance(0.25) + .build(); + } + + /** + * Builder for {@link BatchConfig}. + * + *

    Allows configuring batching constraints and tuning parameters. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private int minBatchSize = 1; + private int maxBatchSize = 10_000; + private double targetBatchOverhead = 0.05; + private double targetBatchDurationSecs = 10.0; + private double targetBatchDurationSecsWithFixedCost = -1; // -1 = unset + private double variance = 0.25; + + private Builder() {} + + /** + * Sets the minimum batch size. + * + * @param minBatchSize minimum number of elements per batch + */ + public Builder withMinBatchSize(int minBatchSize) { + this.minBatchSize = minBatchSize; + return this; + } + + /** + * Sets the maximum batch size. + * + * @param maxBatchSize maximum number of elements per batch + */ + public Builder withMaxBatchSize(int maxBatchSize) { + this.maxBatchSize = maxBatchSize; + return this; + } + + /** + * Sets the target batch overhead ratio. + * + *

    This represents the desired ratio: + * + *

    fixed_cost / total_time + * + *

    Lower values favor larger batches (higher throughput, higher latency). + * + * @param targetBatchOverhead value in (0, 1] + */ + public Builder withTargetBatchOverhead(double targetBatchOverhead) { + this.targetBatchOverhead = targetBatchOverhead; + return this; + } + + /** + * Sets the target batch duration excluding fixed cost. + * + *

    This controls the desired time spent processing elements in a batch, ignoring fixed + * overhead. + * + * @param targetBatchDurationSecs target duration in seconds + */ + public Builder withTargetBatchDurationSecs(double targetBatchDurationSecs) { + this.targetBatchDurationSecs = targetBatchDurationSecs; + return this; + } + /** + * Sets the target batch duration including fixed cost. + * + *

    If set, this provides a stricter upper bound on total batch processing time. + * + * @param value target duration in seconds + */ + public Builder withTargetBatchDurationSecsWithFixedCost(double value) { + this.targetBatchDurationSecsWithFixedCost = value; + return this; + } + + /** + * Sets the allowed variance when selecting batch sizes. + * + *

    This introduces controlled randomness to avoid converging to a single batch size and + * improves robustness of estimation. + * + * @param variance relative deviation (e.g., 0.25 for ±25%) + */ + public Builder withVariance(double variance) { + this.variance = variance; + return this; + } + + public BatchConfig build() { + validate(); + return new BatchConfig(this); + } + + private void validate() { + if (minBatchSize > maxBatchSize) { + throw new IllegalArgumentException( + String.format( + "Minimum (%d) must not be greater than maximum (%d)", + minBatchSize, maxBatchSize)); + } + if (!(targetBatchOverhead > 0 && targetBatchOverhead <= 1)) { + throw new IllegalArgumentException( + String.format( + "targetBatchOverhead (%f) must be between 0 and 1", targetBatchOverhead)); + } + if (targetBatchDurationSecs <= 0) { + throw new IllegalArgumentException( + String.format( + "targetBatchDurationSecs (%f) must be positive", targetBatchDurationSecs)); + } + if (targetBatchDurationSecsWithFixedCost != -1 + && targetBatchDurationSecsWithFixedCost <= 0) { + throw new IllegalArgumentException( + String.format( + "targetBatchDurationSecsWithFixedCost (%f) must be positive", + targetBatchDurationSecsWithFixedCost)); + } + } + } + } + + static class BatchSizeEstimator implements Serializable { + private List data = new ArrayList<>(); + private final BatchConfig config; + private @Nullable Integer replayLastBatchSize = null; // null = no replay pending + private final Map + batchSizeNumSeen; // tracks how many times each batch size seen + private boolean ignoreNextTiming = false; + private final Random random; + + private static final int MAX_DATA_POINTS = 100; + private static final int MAX_GROWTH_FACTOR = 2; + private static final int WARMUP_BATCH_COUNT = 1; + + public BatchSizeEstimator(BatchConfig config) { + this.config = config; + this.data = new ArrayList<>(); + this.random = new Random(); + this.batchSizeNumSeen = new HashMap<>(); + } + + public class Stopwatch implements AutoCloseable { + private final long startTime; + private final int batchSize; + + public Stopwatch(int batchSize) { + this.batchSize = batchSize; + this.startTime = System.currentTimeMillis(); + } + + @Override + public void close() { + long elapsed = System.currentTimeMillis() - startTime; + if (ignoreNextTiming) { + ignoreNextTiming = false; + replayLastBatchSize = Math.min(batchSize, config.maxBatchSize); + } else { + data.add(new long[] {batchSize, elapsed}); + if (data.size() >= MAX_DATA_POINTS) { + thinData(); + } + } + } + } + + public Stopwatch recordTime(int batchSize) { + return new Stopwatch(batchSize); + } + + private void thinData() { + data.remove(random.nextInt(data.size() / 4)); + data.remove(random.nextInt(data.size() / 2)); + } + + public void ignoreNextTiming() { + this.ignoreNextTiming = true; + } + + private double[] linearRegression(double[] xs, double[] ys) { + int n = xs.length; + double xbar = 0, ybar = 0; + for (int i = 0; i < n; i++) { + xbar += xs[i]; + ybar += ys[i]; + } + xbar /= n; + ybar /= n; + + if (xbar == 0) { + return new double[] {ybar, 0}; // a=ybar, b=0 + } + + // all batch sizes identical, can't separate fixed vs per-element cost + boolean allSame = true; + for (double x : xs) { + if (x != xs[0]) { + allSame = false; + break; + } + } + if (allSame) { + return new double[] {0, ybar / xbar}; // a=0, b=avg time per element + } + + // fit the line + double num = 0, den = 0; + for (int i = 0; i < n; i++) { + num += (xs[i] - xbar) * (ys[i] - ybar); + den += (xs[i] - xbar) * (xs[i] - xbar); + } + double b = num / den; + double a = ybar - b * xbar; + return new double[] {a, b}; + } + + private int calculateNextBatchSize() { + + // cold start + if (config.minBatchSize == config.maxBatchSize) { + return config.minBatchSize; + } else if (data.size() < 1) { + return config.minBatchSize; + } else if (data.size() < 2) { + // variety of regression + return (int) + Math.max( + Math.min(config.maxBatchSize, config.minBatchSize * MAX_GROWTH_FACTOR), + config.minBatchSize + 1); + } + + // trim top 20% outliers + List sorted = new ArrayList<>(data); + sorted.sort((p1, p2) -> Long.compare(p1[0], p2[0])); // sort by batch size + int trimSize = Math.max(20, sorted.size() * 4 / 5); + List trimmed = sorted.subList(0, Math.min(trimSize, sorted.size())); + + // find a and b (fixed cost and per element cost) + double[] xs = new double[trimmed.size()]; + double[] ys = new double[trimmed.size()]; + for (int i = 0; i < trimmed.size(); i++) { + xs[i] = trimmed.get(i)[0]; // batch size + ys[i] = trimmed.get(i)[1]; // elapsed ms + } + double[] ab = linearRegression(xs, ys); + double a = Math.max(ab[0], 1e-10); // floor at tiny value + double b = Math.max(ab[1], 1e-20); + + // solve for target batch size + long lastBatchSize = data.get(data.size() - 1)[0]; + int cap = (int) Math.min(lastBatchSize * MAX_GROWTH_FACTOR, config.maxBatchSize); + + double target = config.maxBatchSize; + + // convert to mills + double targetDurationMs = config.targetBatchDurationSecs * 1000.0; + double targetDurationWithFixedMs = config.targetBatchDurationSecsWithFixedCost * 1000.0; + + // 1: a + b*x = targetDurationIncludingFixedCost + if (config.targetBatchDurationSecsWithFixedCost > 0) { + target = Math.min(target, (targetDurationWithFixedMs - a) / b); + } + + // 2: b*x = targetDurationSecs + if (config.targetBatchDurationSecs > 0) { + target = Math.min(target, targetDurationMs / b); + } + + // 3: a / (a + b*x) = targetOverhead + if (config.targetBatchOverhead > 0) { + target = Math.min(target, (a / b) * (1.0 / config.targetBatchOverhead - 1)); + } + + // add jitter to avoid any single batch size + int jitter = data.size() % 2; + if (data.size() > 10) { + target += (int) (target * config.variance * 2 * (random.nextDouble() - 0.5)); + } + + return (int) Math.max(config.minBatchSize + jitter, Math.min(target, cap)); + } + + public int nextBatchSize() { + int result; + + // Check if we should replay a previous batch size due to it not being recorded. + if (replayLastBatchSize != null) { + result = replayLastBatchSize; + replayLastBatchSize = null; + } else { + result = calculateNextBatchSize(); + } + + // track how many times we've seen this batch size + int seenCount = batchSizeNumSeen.getOrDefault(result, 0) + 1; + if (seenCount <= WARMUP_BATCH_COUNT) { + ignoreNextTiming(); + } + batchSizeNumSeen.put(result, seenCount); + + return result; + } + } + + /** A {@link DoFn} that batches elements in the global window. */ + @SuppressWarnings("initialization") + static class GlobalWindowsBatchingDoFn extends DoFn> { + private transient BatchSizeEstimator estimator; + private final BatchConfig config; + private List batch; + private int runningBatchSize; + private int targetBatchSize; + + public GlobalWindowsBatchingDoFn(BatchConfig config) { + this.config = config; + } + + @Setup + public void setup() { + estimator = new BatchSizeEstimator(config); + } + + @StartBundle + public void startBundle() { + batch = new ArrayList<>(); + runningBatchSize = 0; + targetBatchSize = estimator.nextBatchSize(); + } + + @ProcessElement + public void processElement(@Element T element, OutputReceiver> receiver) { + int elementSize = 1; + if (runningBatchSize + elementSize > targetBatchSize) { + if (runningBatchSize > 0 && !batch.isEmpty()) { + try (BatchElements.BatchSizeEstimator.Stopwatch sw = + estimator.recordTime(runningBatchSize)) { + receiver.output(batch); // emit full batch downstream + } + } + batch = new ArrayList<>(); + runningBatchSize = 0; + targetBatchSize = estimator.nextBatchSize(); + } + batch.add(element); + runningBatchSize += elementSize; + } + + @FinishBundle + public void finishBundle(FinishBundleContext context) { + if (!batch.isEmpty()) { + try (BatchElements.BatchSizeEstimator.Stopwatch sw = + estimator.recordTime(runningBatchSize)) { + context.output( // flush leftover elements + batch, + GlobalWindow.INSTANCE.maxTimestamp(), // end of window timestamp + GlobalWindow.INSTANCE // global window + ); + } + } + } + } + + /** + * A {@link DoFn} that batches elements per window. + * + *

    Maintains separate batches for each active window and emits batches when they reach the + * target size or when windows are evicted. + */ + @SuppressWarnings("initialization") + static class WindowAwareBatchingDoFn extends DoFn> { + private transient BatchSizeEstimator estimator; + private final BatchConfig config; + private transient Map> batches; + private int targetBatchSize; + + private static final int MAX_LIVE_WINDOWS = 10; + + private static class SizedBatch implements Serializable { + List elements = new ArrayList<>(); + int size = 0; + } + + private WindowAwareBatchingDoFn(BatchConfig config) { + this.config = config; + } + + @Setup + public void setup() { + estimator = new BatchSizeEstimator(config); + } + + @StartBundle + public void startBundle() { + batches = new HashMap<>(); + targetBatchSize = estimator.nextBatchSize(); + } + + @ProcessElement + public void processElement( + @Element T element, BoundedWindow window, OutputReceiver> receiver) { + + // get or create batch for this window + SizedBatch batch = batches.computeIfAbsent(window, w -> new SizedBatch<>()); + + int elementSize = 1; + + // emit if this window's batch is full + if (batch.size + elementSize > targetBatchSize) { + try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(batch.size)) { + receiver.output(batch.elements); + } + batches.remove(window); + targetBatchSize = estimator.nextBatchSize(); + // create fresh batch for this window after emit + batch = batches.computeIfAbsent(window, w -> new SizedBatch<>()); + } + + batch.elements.add(element); + batch.size += elementSize; + + // evict largest window if too many live windows + if (batches.size() > MAX_LIVE_WINDOWS) { + Map.Entry> largest = + batches.entrySet().stream().max(Comparator.comparingInt(e -> e.getValue().size)).get(); + + BoundedWindow targetWindow = largest.getKey(); + SizedBatch targetBatch = largest.getValue(); + + try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(targetBatch.size)) { + + receiver.outputWithTimestamp(targetBatch.elements, targetWindow.maxTimestamp()); + } + + batches.remove(targetWindow); + targetBatchSize = estimator.nextBatchSize(); + } + } + + @FinishBundle + public void finishBundle(FinishBundleContext context) { + for (Map.Entry> entry : batches.entrySet()) { + BoundedWindow window = entry.getKey(); + SizedBatch batch = entry.getValue(); + if (!batch.elements.isEmpty()) { + try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(batch.size)) { + context.output(batch.elements, window.maxTimestamp(), window); + } + } + } + } + } + + @Override + public PCollection> expand(PCollection input) { + if (input.getWindowingStrategy().equals(WindowingStrategy.globalDefault())) { + return input.apply(ParDo.of(new GlobalWindowsBatchingDoFn<>(config))); + } else { + return input.apply(ParDo.of(new WindowAwareBatchingDoFn<>(config))); + } + } +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java new file mode 100644 index 000000000000..70167b43faf1 --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java @@ -0,0 +1,598 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.transforms; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import org.apache.beam.sdk.testing.NeedsRunner; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.TimestampedValue; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BatchElementsTest implements Serializable { + + @Rule public transient TestPipeline pipeline = TestPipeline.create(); + + @Rule public transient Timeout globalTimeout = Timeout.seconds(120); + + // Helpers + + private static BatchElements.BatchConfig constantConfig(int size) { + return BatchElements.BatchConfig.builder() + .withMinBatchSize(size) + .withMaxBatchSize(size) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + } + + // BatchConfig validation + + @Test + public void testBatchConfigDefaults() { + BatchElements.BatchConfig config = BatchElements.BatchConfig.defaults(); + assertEquals(1, config.minBatchSize); + assertEquals(10_000, config.maxBatchSize); + assertEquals(0.05, config.targetBatchOverhead, 1e-9); + assertEquals(10.0, config.targetBatchDurationSecs, 1e-9); + assertEquals(0.25, config.variance, 1e-9); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigMinGreaterThanMaxThrows() { + BatchElements.BatchConfig.builder().withMinBatchSize(100).withMaxBatchSize(10).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigZeroTargetDurationThrows() { + BatchElements.BatchConfig.builder().withTargetBatchDurationSecs(0.0).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigNegativeTargetDurationThrows() { + BatchElements.BatchConfig.builder().withTargetBatchDurationSecs(-5.0).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigZeroOverheadThrows() { + BatchElements.BatchConfig.builder().withTargetBatchOverhead(0.0).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigOverheadAboveOneThrows() { + BatchElements.BatchConfig.builder().withTargetBatchOverhead(1.5).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigNegativeFixedCostDurationThrows() { + BatchElements.BatchConfig.builder().withTargetBatchDurationSecsWithFixedCost(-5.0).build(); + } + + // BatchSizeEstimator unit tests + + @Test + public void testEstimatorConstantBatchSize() { + BatchElements.BatchConfig config = constantConfig(42); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + // When min == max, always return that size + for (int i = 0; i < 10; i++) { + assertEquals(42, estimator.nextBatchSize()); + } + } + + @Test + public void testEstimatorColdStartReturnsMinBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(5) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + // No timing data yet — should return minBatchSize + assertEquals(5, estimator.nextBatchSize()); + } + + @Test + public void testEstimatorGrowsAfterTimingData() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + // Warm up with some fake recordings + int size = estimator.nextBatchSize(); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(size)) { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + size = estimator.nextBatchSize(); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(size)) { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + int grown = estimator.nextBatchSize(); + assertTrue("Estimator should grow beyond minBatchSize after data, got: " + grown, grown > 1); + } + + @Test + public void testEstimatorNeverExceedsMaxBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(10) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + for (int i = 0; i < 50; i++) { + int next = estimator.nextBatchSize(); + assertTrue("Batch size " + next + " exceeds max of 10", next <= 10); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(next)) { + Thread.sleep(10 + i); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testEstimatorNeverGoesBelowMinBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(7) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + for (int i = 0; i < 20; i++) { + int next = estimator.nextBatchSize(); + assertTrue("Batch size " + next + " is below min of 7", next >= 7); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(next)) { + Thread.sleep(10 + i); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testIgnoreNextTimingReplaysBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + estimator.ignoreNextTiming(); + int first = estimator.nextBatchSize(); + + // After ignoreNextTiming, the stopwatch will set replayLastBatchSize + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(first)) {} + + // Next call should replay the same size + int replayed = estimator.nextBatchSize(); + assertEquals( + "Expected replay of batch size " + first + " but got " + replayed, first, replayed); + } + + // GlobalWindows pipeline tests + @Test + @Category(NeedsRunner.class) + public void testConstantBatchInGlobalWindow() { + // Mirrors Python: test_constant_batch + // Runner bundle boundaries are not fixed, so partial batches may be emitted at bundle end. + PCollection output = + pipeline + .apply("Create", Create.of(range(35))) + .apply("Batch", BatchElements.withConfig(constantConfig(10))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List batch) { + return batch.size(); + } + })); + + PAssert.that(output).satisfies(sizes -> assertBatchSizesWithinLimitAndTotal(sizes, 10, 35)); + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testPipelineRespectsMaxBatchSizeAndPreservesElements() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(15) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + + PCollection sizes = + pipeline + .apply("Create", Create.of(range(200))) + .apply("Batch", BatchElements.withConfig(config)) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes) + .satisfies( + s -> { + int total = 0; + for (int size : s) { + assertTrue("Batch size must be > 0", size > 0); + assertTrue("Batch size " + size + " exceeded maxBatchSize 15", size <= 15); + total += size; + } + assertEquals("All 200 elements must be present", 200, total); + return null; + }); + + pipeline.run().waitUntilFinish(); + } + + // WindowAware pipeline tests + + @Test + @Category(NeedsRunner.class) + public void testWindowedBatches() { + // 47 elements across FixedWindows(30s); runner bundle boundaries may split batches. + List> timestamped = new ArrayList<>(); + for (int i = 0; i < 47; i++) { + timestamped.add(TimestampedValue.of(i, new Instant((long) i * 1000))); + } + + PCollection sizes = + pipeline + .apply("Create", Create.timestamped(timestamped)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(30))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(constantConfig(10))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes).satisfies(s -> assertBatchSizesWithinLimitAndTotal(s, 10, 47)); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testCrossWindowIsolation() { + // Elements from different windows must NEVER appear in the same batch + List> elements = new ArrayList<>(); + for (int i = 0; i < 2000; i++) { + // 4 windows of 2s each, 500 elements per window + long ts = (long) (i / 500) * 2000L + (i % 500); + elements.add(TimestampedValue.of("w" + (i / 500) + "-e" + i, new Instant(ts))); + } + + PCollection isolationChecks = + pipeline + .apply("Create", Create.timestamped(elements)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(2))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(constantConfig(50))) + .apply( + "CheckIsolation", + MapElements.via( + new SimpleFunction, Boolean>() { + @Override + public Boolean apply(List batch) { + // All elements in a batch must share the same window prefix + String firstWindow = batch.get(0).substring(0, 2); + for (String el : batch) { + if (!el.startsWith(firstWindow)) { + throw new AssertionError( + "Cross-window contamination: " + + el + + " in batch starting with " + + firstWindow); + } + } + return true; + } + })); + + PAssert.that(isolationChecks) + .satisfies( + checks -> { + int count = 0; + for (boolean ok : checks) { + assertTrue(ok); + count++; + } + assertTrue("Expected at least one batch", count > 0); + return null; + }); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testWindowedBatchesPreserveAllElements() { + // Total elements across all windows must equal input count + int numElements = 500; + List> elements = new ArrayList<>(); + for (int i = 0; i < numElements; i++) { + elements.add(TimestampedValue.of(i, new Instant((long) (i / 100) * 5000L + i))); + } + + PCollection sizes = + pipeline + .apply("Create", Create.timestamped(elements)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(5))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(constantConfig(30))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes).satisfies(s -> assertBatchSizesWithinLimitAndTotal(s, 30, numElements)); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testEvictionPreservesWindowMetadata() { + // 1. Use 12 windows to trigger the MAX_LIVE_WINDOWS (10) eviction + List> elements = new ArrayList<>(); + for (int w = 0; w < 12; w++) { + long timestamp = w * 10000L; // Distinct windows + elements.add(TimestampedValue.of(w, new Instant(timestamp))); + } + + PCollection> batched = + pipeline + .apply(Create.timestamped(elements)) + .apply(Window.into(FixedWindows.of(Duration.standardSeconds(5)))) + .apply( + BatchElements.withConfig( + constantConfig(10))); // Large size so they don't flush naturally + + // 2. Use ParDo to capture the ACTUAL window from the context + PCollection>> windowCaptured = + batched.apply( + ParDo.of( + new DoFn, KV>>() { + @ProcessElement + public void process( + @Element List e, + BoundedWindow w, + OutputReceiver>> r) { + r.output(KV.of((IntervalWindow) w, e)); + } + })); + + // 3. Assert that the value inside the batch matches the window it was found in + PAssert.that(windowCaptured) + .satisfies( + items -> { + for (KV> item : items) { + int windowIndex = (int) (item.getKey().start().getMillis() / 10000L); + for (Integer val : item.getValue()) { + assertEquals( + "Element " + val + " found in wrong window!", (Integer) windowIndex, val); + } + } + return null; + }); + + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testWindowedBatchMaxSizeRespected() { + int maxBatch = 20; + List> elements = new ArrayList<>(); + for (int i = 0; i < 300; i++) { + elements.add(TimestampedValue.of(i, new Instant((long) (i / 150) * 3000L + i))); + } + + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(maxBatch) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + + PCollection sizes = + pipeline + .apply("Create", Create.timestamped(elements)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(3))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(config)) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes) + .satisfies( + s -> { + int total = 0; + for (int size : s) { + assertTrue("Batch size must be > 0", size > 0); + assertTrue("Batch size " + size + " exceeded max " + maxBatch, size <= maxBatch); + total += size; + } + assertEquals("All 300 elements must be present", 300, total); + return null; + }); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testWindowRoutingGlobalWindowUsesGlobalDoFn() { + // A plain Create (no windowing) should route through GlobalWindowsBatchingDoFn. + // We validate this indirectly: output is correct and no windowing errors occur. + PCollection sizes = + pipeline + .apply("Create", Create.of(range(25))) + .apply("Batch", BatchElements.withConfig(constantConfig(10))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes).satisfies(s -> assertBatchSizesWithinLimitAndTotal(s, 10, 25)); + + pipeline.run().waitUntilFinish(); + } + + // LinearRegression unit tests + + @Test + public void testLinearRegressionPerfectFit() throws Exception { + double[] ab = linearRegression(new double[] {1, 2, 3, 4, 5}, new double[] {3, 5, 7, 9, 11}); + + assertEquals(1.0, ab[0], 1e-9); + assertEquals(2.0, ab[1], 1e-9); + } + + @Test + public void testLinearRegressionRepeatedXsUsesMeanTimePerElement() throws Exception { + double[] ab = linearRegression(new double[] {5, 5, 5, 5}, new double[] {10, 15, 20, 25}); + + assertEquals(0.0, ab[0], 1e-9); + assertEquals(3.5, ab[1], 1e-9); + } + + // Utility + + private static Void assertBatchSizesWithinLimitAndTotal( + Iterable sizes, int maxBatchSize, int expectedTotal) { + int total = 0; + for (int size : sizes) { + assertTrue("Batch size must be > 0", size > 0); + assertTrue("Batch size " + size + " exceeded max " + maxBatchSize, size <= maxBatchSize); + total += size; + } + assertEquals("All elements must be present", expectedTotal, total); + return null; + } + + private static double[] linearRegression(double[] xs, double[] ys) throws Exception { + BatchElements.BatchSizeEstimator estimator = + new BatchElements.BatchSizeEstimator(BatchElements.BatchConfig.defaults()); + Method method = + BatchElements.BatchSizeEstimator.class.getDeclaredMethod( + "linearRegression", double[].class, double[].class); + method.setAccessible(true); + return (double[]) method.invoke(estimator, xs, ys); + } + + private static List range(int n) { + List list = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + list.add(i); + } + return list; + } +} From 609556f0298ea57a04a91f7a69ca3fa858346372 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Thu, 7 May 2026 14:38:07 -0400 Subject: [PATCH 108/490] [yaml] - Add huggingface model handler (#38110) * change runinference yaml name to vertexai * add huggingface support * add huggingface test * address gemini * fix lint issues * try adding transformers to testenv * revert tox change and apply transformers to only the workflow needed * minor import and tweaks to get to work * fix lint * updated test logic and model logic * add new task with transformer dependency supplied * revert changes * try again without the try except * remove yaml args in pre and post workflows --- .../beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- .../beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- .../yaml/tests/runinference_huggingface.yaml | 62 +++++++++++++++++++ ...erence.yaml => runinference_vertexai.yaml} | 0 sdks/python/apache_beam/yaml/yaml_ml.py | 49 +++++++++++++++ sdks/python/build.gradle | 14 ++++- 6 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml rename sdks/python/apache_beam/yaml/tests/{runinference.yaml => runinference_vertexai.yaml} (100%) diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index ea1c255f7cc9..afa437b64de1 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -80,7 +80,7 @@ jobs: - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 7d17fd2140c9..0b8f4cd63939 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -91,7 +91,7 @@ jobs: - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:yamlIntegrationTests - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml new file mode 100644 index 000000000000..8728a6f544ad --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +pipelines: + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - text: "I love Apache Beam!" + - text: "I hate this error." + - type: RunInference + config: + model_handler: + type: "HuggingFacePipeline" + config: + task: "text-classification" + inference_fn: + callable: | + def real_inference(batch, pipeline, inference_args): + predictions = pipeline(batch, **inference_args) + + # If it's a single dictionary (batch size of 1), wrap it in a list + if isinstance(predictions, dict): + predictions = [predictions] + + return { + 'label': [p['label'] for p in predictions], + 'score': [p['score'] for p in predictions] + } + preprocess: + callable: 'lambda x: x.text' + - type: MapToFields + config: + language: python + fields: + text: text + sentiment: + callable: 'lambda x: x.inference.inference["label"]' + - type: AssertEqual + config: + elements: + - text: "I love Apache Beam!" + sentiment: "POSITIVE" + - text: "I hate this error." + sentiment: "NEGATIVE" + + options: + yaml_experimental_features: ['ML'] diff --git a/sdks/python/apache_beam/yaml/tests/runinference.yaml b/sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml similarity index 100% rename from sdks/python/apache_beam/yaml/tests/runinference.yaml rename to sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml diff --git a/sdks/python/apache_beam/yaml/yaml_ml.py b/sdks/python/apache_beam/yaml/yaml_ml.py index 51f18c733046..05cbed3bd456 100644 --- a/sdks/python/apache_beam/yaml/yaml_ml.py +++ b/sdks/python/apache_beam/yaml/yaml_ml.py @@ -282,6 +282,55 @@ def inference_output_type(self): ('model_id', Optional[str])]) +@ModelHandlerProvider.register_handler_type('HuggingFacePipeline') +class HuggingFacePipelineProvider(ModelHandlerProvider): + def __init__( + self, + task: Optional[str] = None, + model: Optional[str] = None, + preprocess: Optional[dict[str, str]] = None, + postprocess: Optional[dict[str, str]] = None, + device: Optional[Any] = None, + inference_fn: Optional[dict[str, str]] = None, + load_pipeline_args: Optional[dict[str, Any]] = None, + **kwargs): + try: + from apache_beam.ml.inference.huggingface_inference import HuggingFacePipelineModelHandler + except ImportError: + raise ValueError( + 'Unable to import HuggingFacePipelineModelHandler. Please ' + 'install transformers dependencies.') + + kwargs = {k: v for k, v in kwargs.items() if not k.startswith('_')} + + inference_fn_obj = self.parse_processing_transform( + inference_fn, 'inference_fn') if inference_fn else None + + handler_kwargs = {} + if inference_fn_obj: + handler_kwargs['inference_fn'] = inference_fn_obj + + _handler = HuggingFacePipelineModelHandler( + task=task, + model=model, + device=device, + load_pipeline_args=load_pipeline_args, + **handler_kwargs, + **kwargs) + + super().__init__(_handler, preprocess, postprocess) + + @staticmethod + def validate(config): + if not config.get('task') and not config.get('model'): + raise ValueError( + "HuggingFacePipeline requires either 'task' or " + "'model' to be specified.") + + def inference_output_type(self): + return Any + + @beam.ptransform.ptransform_fn def run_inference( pcoll, diff --git a/sdks/python/build.gradle b/sdks/python/build.gradle index 5f09dff57e8f..e676fd110433 100644 --- a/sdks/python/build.gradle +++ b/sdks/python/build.gradle @@ -124,10 +124,20 @@ tasks.register("generateYamlDocs") { outputs.file "${buildDir}/yaml-examples.html" } +tasks.register("installYamlIntegrationTestDeps") { + dependsOn installGcpTest + doLast { + exec { + executable 'sh' + args '-c', ". ${envdir}/bin/activate && pip install --pre --retries 10 ${buildDir}/apache-beam.tar.gz[ml_test,yaml,transformers]" + } + } +} + tasks.register("yamlIntegrationTests") { description "Runs precommit integration tests for yaml pipelines." - dependsOn installGcpTest + dependsOn installYamlIntegrationTestDeps // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" @@ -146,7 +156,7 @@ tasks.register("yamlIntegrationTests") { tasks.register("postCommitYamlIntegrationTests") { description "Runs postcommit integration tests for yaml pipelines - parameterized by yamlTestSet." - dependsOn installGcpTest + dependsOn installYamlIntegrationTestDeps // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" From 149b324b7e06583fde958ac89411224aa2e979c6 Mon Sep 17 00:00:00 2001 From: bambadiouf1 Date: Thu, 7 May 2026 14:13:48 -0700 Subject: [PATCH 109/490] Add DiskProvisionedIops/ThroughputMibps pipeline options for the Python SDK (#38370) * Restore Java and Go changes for disk provisioned IOPS and throughput * Add CHANGES.md entry for disk provisioned IOPS and throughput * restore go changes * initialize options map in dataflow job to prevent nil pointer exceptions * go fmt * add testDiskProvisionedOptionsConfig unit test * Update pr id in changes.md * add disk_provisioned_iops and disk_provisioned_throughput_mibps options to pipeline configuration * add Python support to disk IOPS and throughput pipeline options in CHANGES.md * revert manual changes * ran gen_client * gen client * trigger postcommit_python * Delete R74 * undo gen_client deletion * delete change to trigger postcom --- CHANGES.md | 3 +- .../apache_beam/options/pipeline_options.py | 18 ++++ .../options/pipeline_options_test.py | 6 ++ .../runners/dataflow/internal/apiclient.py | 5 + .../dataflow/internal/apiclient_test.py | 17 ++++ .../dataflow/dataflow_v1b3_messages.py | 99 +++++++++++-------- 6 files changed, 104 insertions(+), 44 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 922c38ffef37..0db7fddba4f1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,8 +70,7 @@ ## New Features / Improvements -* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Go) ([#38349](https://github.com/apache/beam/issues/38349)). -* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). +* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Go/Python) ([#38349](https://github.com/apache/beam/issues/38349)). * TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to encode finished bitset. SentinelBitSetCoder and BitSetCoder are state compatible. Both coders can decode encoded bytes from the other coder diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index a79bddb21ab9..265313cd013d 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -1402,6 +1402,24 @@ def _add_argparse_args(cls, parser): dest='disk_type', default=None, help=('Specifies what type of persistent disk should be used.')) + parser.add_argument( + '--disk_provisioned_iops', + type=int, + default=None, + dest='disk_provisioned_iops', + help=( + 'The provisioned IOPS of the disk. If not set, the Dataflow service' + ' will choose a reasonable default.'), + ) + parser.add_argument( + '--disk_provisioned_throughput_mibps', + type=int, + default=None, + dest='disk_provisioned_throughput_mibps', + help=( + 'The provisioned throughput of the disk in MiB/s. If not set, the' + ' Dataflow service will choose a reasonable default.'), + ) parser.add_argument( '--worker_region', default=None, diff --git a/sdks/python/apache_beam/options/pipeline_options_test.py b/sdks/python/apache_beam/options/pipeline_options_test.py index 215c44156ea6..901f56b99cb6 100644 --- a/sdks/python/apache_beam/options/pipeline_options_test.py +++ b/sdks/python/apache_beam/options/pipeline_options_test.py @@ -444,12 +444,18 @@ def test_worker_options(self): 'abc', '--disk_type', 'def', + '--disk_provisioned_iops', + '4000', + '--disk_provisioned_throughput_mibps', + '200', '--element_processing_timeout_minutes', '10', ]) worker_options = options.view_as(WorkerOptions) self.assertEqual(worker_options.machine_type, 'abc') self.assertEqual(worker_options.disk_type, 'def') + self.assertEqual(worker_options.disk_provisioned_iops, 4000) + self.assertEqual(worker_options.disk_provisioned_throughput_mibps, 200) self.assertEqual(worker_options.element_processing_timeout_minutes, 10) options = PipelineOptions( diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 097523a5131c..4a7c61901de3 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -208,6 +208,11 @@ def __init__( pool.diskSizeGb = self.worker_options.disk_size_gb if self.worker_options.disk_type: pool.diskType = self.worker_options.disk_type + if self.worker_options.disk_provisioned_iops is not None: + pool.diskProvisionedIops = self.worker_options.disk_provisioned_iops + if self.worker_options.disk_provisioned_throughput_mibps is not None: + pool.diskProvisionedThroughputMibps = ( + self.worker_options.disk_provisioned_throughput_mibps) if self.worker_options.zone: pool.zone = self.worker_options.zone if self.worker_options.network: diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index 43f51d0b39fd..909ce9fd117e 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -644,6 +644,23 @@ def test_number_of_worker_harness_threads(self): FAKE_PIPELINE_URL) self.assertEqual(env.proto.workerPools[0].numThreadsPerWorker, 2) + def test_disk_provisioning_options(self): + pipeline_options = PipelineOptions([ + '--temp_location', + 'gs://any-location/temp', + '--disk_provisioned_iops', + '4000', + '--disk_provisioned_throughput_mibps', + '200' + ]) + env = apiclient.Environment([], + pipeline_options, + '2.0.0', + FAKE_PIPELINE_URL) + self.assertEqual(env.proto.workerPools[0].diskProvisionedIops, 4000) + self.assertEqual( + env.proto.workerPools[0].diskProvisionedThroughputMibps, 200) + @mock.patch( 'apache_beam.runners.dataflow.internal.apiclient.' 'beam_version.__version__', diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py index 0c096e73c1ac..582fb30b57b1 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py @@ -2634,8 +2634,8 @@ class FlexTemplateRuntimeEnvironment(_messages.Message): ipConfiguration: Configuration for VM IPs. kmsKeyName: Name for the Cloud KMS key for the job. Key format is: projects//locations//keyRings//cryptoKeys/ - launcherMachineType: The machine type to use for launching the job. The - default is n1-standard-1. + launcherMachineType: The machine type to use for launching the job. If not + set, Dataflow will select a default machine type. machineType: The machine type to use for the job. Defaults to the value from the template if not specified. maxWorkers: The maximum number of Google Compute Engine instances to be @@ -3209,6 +3209,7 @@ class Job(_messages.Message): attempts to create a job with the same name as an active job that already exists, the attempt returns the existing job. The name must match the regular expression `[a-z]([-a-z0-9]{0,1022}[a-z0-9])?` + pausable: Output only. Indicates whether the job can be paused. pipelineDescription: Preliminary field: The format of this data may change at any time. A description of the user pipeline and stages through which it is executed. Created by Cloud Dataflow service. Only retrieved with @@ -3498,22 +3499,23 @@ class AdditionalProperty(_messages.Message): labels = _messages.MessageField('LabelsValue', 10) location = _messages.StringField(11) name = _messages.StringField(12) - pipelineDescription = _messages.MessageField('PipelineDescription', 13) - projectId = _messages.StringField(14) - replaceJobId = _messages.StringField(15) - replacedByJobId = _messages.StringField(16) - requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 17) - runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 18) - satisfiesPzi = _messages.BooleanField(19) - satisfiesPzs = _messages.BooleanField(20) - serviceResources = _messages.MessageField('ServiceResources', 21) - stageStates = _messages.MessageField('ExecutionStageState', 22, repeated=True) - startTime = _messages.StringField(23) - steps = _messages.MessageField('Step', 24, repeated=True) - stepsLocation = _messages.StringField(25) - tempFiles = _messages.StringField(26, repeated=True) - transformNameMapping = _messages.MessageField('TransformNameMappingValue', 27) - type = _messages.EnumField('TypeValueValuesEnum', 28) + pausable = _messages.BooleanField(13) + pipelineDescription = _messages.MessageField('PipelineDescription', 14) + projectId = _messages.StringField(15) + replaceJobId = _messages.StringField(16) + replacedByJobId = _messages.StringField(17) + requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 18) + runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 19) + satisfiesPzi = _messages.BooleanField(20) + satisfiesPzs = _messages.BooleanField(21) + serviceResources = _messages.MessageField('ServiceResources', 22) + stageStates = _messages.MessageField('ExecutionStageState', 23, repeated=True) + startTime = _messages.StringField(24) + steps = _messages.MessageField('Step', 25, repeated=True) + stepsLocation = _messages.StringField(26) + tempFiles = _messages.StringField(27, repeated=True) + transformNameMapping = _messages.MessageField('TransformNameMappingValue', 28) + type = _messages.EnumField('TypeValueValuesEnum', 29) class JobExecutionDetails(_messages.Message): @@ -5342,8 +5344,14 @@ class RuntimeUpdatableParams(_messages.Message): during job creation. Fields: - acceptableBacklogDuration: Optional. The backlog threshold duration in - seconds for autoscaling. Value must be non-negative. + acceptableBacklogDuration: Optional. Deprecated: Use `latency_tier` + instead. The backlog threshold duration in seconds for autoscaling. + Value must be non-negative. + autoscalingTier: Optional. Deprecated: Use `latency_tier` instead. The + backlog threshold tier for autoscaling. Value must be one of "low- + latency", "medium-latency", or "high-latency". + latencyTier: Optional. The backlog threshold tier for autoscaling. Value + must be one of "low-latency", "medium-latency", or "high-latency". maxNumWorkers: The maximum number of workers to cap autoscaling at. This field is currently only supported for Streaming Engine jobs. minNumWorkers: The minimum number of workers to scale down to. This field @@ -5357,9 +5365,11 @@ class RuntimeUpdatableParams(_messages.Message): """ acceptableBacklogDuration = _messages.StringField(1) - maxNumWorkers = _messages.IntegerField(2, variant=_messages.Variant.INT32) - minNumWorkers = _messages.IntegerField(3, variant=_messages.Variant.INT32) - workerUtilizationHint = _messages.FloatField(4) + autoscalingTier = _messages.StringField(2) + latencyTier = _messages.StringField(3) + maxNumWorkers = _messages.IntegerField(4, variant=_messages.Variant.INT32) + minNumWorkers = _messages.IntegerField(5, variant=_messages.Variant.INT32) + workerUtilizationHint = _messages.FloatField(6) class SDKInfo(_messages.Message): @@ -7775,6 +7785,9 @@ class WorkerPool(_messages.Message): defaultPackageSet: The default package set to install. This allows the service to select a default set of packages which are useful to worker harnesses written in a particular language. + diskProvisionedIops: Optional. IOPS provisioned for the root disk for VMs. + diskProvisionedThroughputMibps: Optional. Throughput provisioned for the + root disk for VMs. diskSizeGb: Size of root disk for VMs, in GB. If zero or unspecified, the service will attempt to choose a reasonable default. diskSourceImage: Fully qualified source image for disks. @@ -7938,25 +7951,27 @@ class AdditionalProperty(_messages.Message): autoscalingSettings = _messages.MessageField('AutoscalingSettings', 1) dataDisks = _messages.MessageField('Disk', 2, repeated=True) defaultPackageSet = _messages.EnumField('DefaultPackageSetValueValuesEnum', 3) - diskSizeGb = _messages.IntegerField(4, variant=_messages.Variant.INT32) - diskSourceImage = _messages.StringField(5) - diskType = _messages.StringField(6) - ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 7) - kind = _messages.StringField(8) - machineType = _messages.StringField(9) - metadata = _messages.MessageField('MetadataValue', 10) - network = _messages.StringField(11) - numThreadsPerWorker = _messages.IntegerField(12, variant=_messages.Variant.INT32) - numWorkers = _messages.IntegerField(13, variant=_messages.Variant.INT32) - onHostMaintenance = _messages.StringField(14) - packages = _messages.MessageField('Package', 15, repeated=True) - poolArgs = _messages.MessageField('PoolArgsValue', 16) - sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 17, repeated=True) - subnetwork = _messages.StringField(18) - taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 19) - teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 20) - workerHarnessContainerImage = _messages.StringField(21) - zone = _messages.StringField(22) + diskProvisionedIops = _messages.IntegerField(4) + diskProvisionedThroughputMibps = _messages.IntegerField(5) + diskSizeGb = _messages.IntegerField(6, variant=_messages.Variant.INT32) + diskSourceImage = _messages.StringField(7) + diskType = _messages.StringField(8) + ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 9) + kind = _messages.StringField(10) + machineType = _messages.StringField(11) + metadata = _messages.MessageField('MetadataValue', 12) + network = _messages.StringField(13) + numThreadsPerWorker = _messages.IntegerField(14, variant=_messages.Variant.INT32) + numWorkers = _messages.IntegerField(15, variant=_messages.Variant.INT32) + onHostMaintenance = _messages.StringField(16) + packages = _messages.MessageField('Package', 17, repeated=True) + poolArgs = _messages.MessageField('PoolArgsValue', 18) + sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 19, repeated=True) + subnetwork = _messages.StringField(20) + taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 21) + teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 22) + workerHarnessContainerImage = _messages.StringField(23) + zone = _messages.StringField(24) class WorkerSettings(_messages.Message): From 490451b30ea6b5f24f490326efc9f2d4e0762375 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 7 May 2026 17:32:04 -0400 Subject: [PATCH 110/490] Mitigate test broken after Avro Upgrade due to AVRO-4110 (#38405) --- ...ReadSchemaTransformFormatProviderTest.java | 11 ++++ ...SchemaTransformFormatProviderTestData.java | 61 +++++++++++++++++++ ...ReadSchemaTransformFormatProviderTest.java | 11 ++++ 3 files changed, 83 insertions(+) diff --git a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java index 5725ceff3a12..bfba60fb0c13 100644 --- a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java +++ b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.fileschematransform; import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ALL_PRIMITIVE_DATA_TYPES_SCHEMA; +import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; import static org.apache.beam.sdk.io.fileschematransform.FileReadSchemaTransformProvider.FILEPATTERN_ROW_FIELD_NAME; import static org.apache.beam.sdk.io.fileschematransform.FileWriteSchemaTransformFormatProviderTestData.DATA; import static org.apache.beam.sdk.io.fs.ResolveOptions.StandardResolveOptions.RESOLVE_FILE; @@ -215,6 +216,16 @@ public void testReadWithPCollectionOfFilepatterns() { readPipeline.run(); } + // TODO(AVRO-4110): remove this override when Beam upgraded Avro past 1.12.0 + @Override + public void testArrayPrimitiveDataTypes() { + Schema schema = ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; + List rows = DATA.arrayPrimitiveDataTypesRowsAvro1120; + String filePath = getFilePath(); + + runWriteAndReadTest(schema, rows, filePath, null); + } + private static class TestDynamicDestinations extends DynamicAvroDestinations { final ResourceId baseDir; diff --git a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java index 4f70dca71e38..d92acaa0ac6c 100644 --- a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java +++ b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java @@ -35,6 +35,7 @@ import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.timeContainingToRowFn; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -50,6 +51,7 @@ import org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.SinglyNestedDataTypes; import org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.TimeContaining; import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.joda.time.Instant; /** Shared {@link SchemaAwareJavaBeans} data to be used across various tests. */ @@ -60,6 +62,23 @@ class FileWriteSchemaTransformFormatProviderTestData { /* Prevent instantiation outside this class. */ private FileWriteSchemaTransformFormatProviderTestData() {} + private static class ListPatcher { + private ArrayList list; + + ListPatcher(List list) { + this.list = Lists.newArrayList(list); + } + + ArrayList get() { + return list; + } + + ListPatcher patch(int index, T value) { + list.set(index, value); + return this; + } + } + final List allPrimitiveDataTypesList = Arrays.asList( allPrimitiveDataTypes(false, BigDecimal.valueOf(1L), 1.2345, 1.2345f, 1, 1L, "a"), @@ -188,11 +207,53 @@ private FileWriteSchemaTransformFormatProviderTestData() {} Collections.emptyList(), Collections.emptyList())); + // TODO(AVRO-4110): remove this workaround when Beam upgraded Avro past 1.12.0 + final List arrayPrimitiveDataTypesListAvro1120 = + new ListPatcher<>(arrayPrimitiveDataTypesList) + .patch( + 1, + arrayPrimitiveDataTypes( + Collections.emptyList(), + Collections.singletonList((double) Float.MAX_VALUE), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList())) + .patch( + 6, + arrayPrimitiveDataTypes( + Arrays.asList(false, true, false), + Arrays.asList((double) Float.MIN_VALUE, 0.0, (double) Float.MAX_VALUE), + Arrays.asList(Float.MIN_VALUE, 0.0f, Float.MAX_VALUE), + Arrays.asList(Integer.MIN_VALUE, 0, Integer.MAX_VALUE), + Arrays.asList(Long.MIN_VALUE, 0L, Long.MAX_VALUE), + Arrays.asList( + Stream.generate(() -> "🐤").limit(10).collect(Collectors.joining("")), + Stream.generate(() -> "🐥").limit(10).collect(Collectors.joining("")), + Stream.generate(() -> "🐣").limit(10).collect(Collectors.joining(""))))) + .patch( + 7, + arrayPrimitiveDataTypes( + Stream.generate(() -> true).limit(10).collect(Collectors.toList()), + Stream.generate(() -> (double) Float.MIN_VALUE) + .limit(10) + .collect(Collectors.toList()), + Stream.generate(() -> Float.MIN_VALUE).limit(10).collect(Collectors.toList()), + Stream.generate(() -> Integer.MIN_VALUE).limit(10).collect(Collectors.toList()), + Stream.generate(() -> Long.MIN_VALUE).limit(10).collect(Collectors.toList()), + Stream.generate(() -> "🐿").limit(10).collect(Collectors.toList()))) + .get(); + final List arrayPrimitiveDataTypesRows = arrayPrimitiveDataTypesList.stream() .map(arrayPrimitiveDataTypesToRowFn()::apply) .collect(Collectors.toList()); + final List arrayPrimitiveDataTypesRowsAvro1120 = + arrayPrimitiveDataTypesListAvro1120.stream() + .map(arrayPrimitiveDataTypesToRowFn()::apply) + .collect(Collectors.toList()); + final List singlyNestedDataTypesNoRepeat = allPrimitiveDataTypesList.stream() .map(SchemaAwareJavaBeans::singlyNestedDataTypes) diff --git a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java index b1d6bba06ea9..bbc33698c41f 100644 --- a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java +++ b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.fileschematransform; import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ALL_PRIMITIVE_DATA_TYPES_SCHEMA; +import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; import static org.apache.beam.sdk.io.fileschematransform.FileReadSchemaTransformProvider.FILEPATTERN_ROW_FIELD_NAME; import static org.apache.beam.sdk.io.fileschematransform.FileWriteSchemaTransformFormatProviderTestData.DATA; import static org.apache.beam.sdk.transforms.Contextful.fn; @@ -218,4 +219,14 @@ public void testReadWithPCollectionOfFilepatterns() { PAssert.that(output.get(FileReadSchemaTransformProvider.OUTPUT_TAG)).containsInAnyOrder(rows); readPipeline.run(); } + + // TODO(AVRO-4110): remove this override when Beam upgraded Avro past 1.12.0 + @Override + public void testArrayPrimitiveDataTypes() { + Schema schema = ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; + List rows = DATA.arrayPrimitiveDataTypesRowsAvro1120; + String filePath = getFilePath(); + + runWriteAndReadTest(schema, rows, filePath, null); + } } From e23354863b27883ee8e6cf6ce1ac233204b84ab7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 07:51:49 -0400 Subject: [PATCH 111/490] Bump cloud.google.com/go/pubsub from 1.50.1 to 1.50.2 in /sdks (#38415) Bumps [cloud.google.com/go/pubsub](https://github.com/googleapis/google-cloud-go) from 1.50.1 to 1.50.2. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.50.1...pubsub/v1.50.2) --- updated-dependencies: - dependency-name: cloud.google.com/go/pubsub dependency-version: 1.50.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 4f3c5fc483a8..6a16d8f5d0c7 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -29,7 +29,7 @@ require ( cloud.google.com/go/bigtable v1.47.0 cloud.google.com/go/datastore v1.23.0 cloud.google.com/go/profiler v0.4.3 - cloud.google.com/go/pubsub v1.50.1 + cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.88.0 cloud.google.com/go/storage v1.62.0 github.com/aws/aws-sdk-go-v2 v1.41.5 @@ -80,7 +80,7 @@ require ( cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/monitoring v1.25.0 // indirect - cloud.google.com/go/pubsub/v2 v2.0.0 // indirect + cloud.google.com/go/pubsub/v2 v2.4.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect @@ -121,7 +121,7 @@ require ( github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.einride.tech/aip v0.73.0 // indirect + go.einride.tech/aip v0.83.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 30822a755a7b..15003aff6d48 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -85,10 +85,10 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= -cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= -cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= -cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0= -cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= +cloud.google.com/go/pubsub v1.50.2 h1:54Up97HnThdP4H8jjWJSSQ/mnYG2EKon7ZSNETRq0tM= +cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkNvjj4sp0w= +cloud.google.com/go/pubsub/v2 v2.4.0 h1:oMKNiBQpXImRWnHYla9uSU66ZzByZwBSCJOEs/pTKVg= +cloud.google.com/go/pubsub/v2 v2.4.0/go.mod h1:2lS/XQKq5qtOMs6kHBK+WX1ytUC36kLl2ig3zqsGUx8= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= @@ -854,8 +854,8 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps= -go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ= +go.einride.tech/aip v0.83.0 h1:TI21IdeOnLTwZEJ3BxtImIZk6bsN2Q+sd0x99SLiQ+M= +go.einride.tech/aip v0.83.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= From 2c0f27165486f6e1e225587840bd5d4ff6d632ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 07:52:31 -0400 Subject: [PATCH 112/490] Bump cloud.google.com/go/storage from 1.62.0 to 1.62.1 in /sdks (#38414) Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.62.0 to 1.62.1. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/compute/v1.62.0...storage/v1.62.1) --- updated-dependencies: - dependency-name: cloud.google.com/go/storage dependency-version: 1.62.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 6a16d8f5d0c7..9642547b72da 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -31,7 +31,7 @@ require ( cloud.google.com/go/profiler v0.4.3 cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.88.0 - cloud.google.com/go/storage v1.62.0 + cloud.google.com/go/storage v1.62.1 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 diff --git a/sdks/go.sum b/sdks/go.sum index 15003aff6d48..160eb35a0eea 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -99,8 +99,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.62.0 h1:w2pQJhpUqVerMON45vatE2FpCYsNTf7OHjkn6ux5mMU= -cloud.google.com/go/storage v1.62.0/go.mod h1:T5hz3qzcpnxZ5LdKc7y8Tw7lh4v9zeeVyrD/cLJAzZU= +cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8= +cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= @@ -883,8 +883,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bT go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= From 059e59447a5578e0c4171ed649294b0f7981bfc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 07:53:23 -0400 Subject: [PATCH 113/490] Bump github.com/go-sql-driver/mysql from 1.9.3 to 1.10.0 in /sdks (#38413) Bumps [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) from 1.9.3 to 1.10.0. - [Release notes](https://github.com/go-sql-driver/mysql/releases) - [Changelog](https://github.com/go-sql-driver/mysql/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-sql-driver/mysql/compare/v1.9.3...v1.10.0) --- updated-dependencies: - dependency-name: github.com/go-sql-driver/mysql dependency-version: 1.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 9642547b72da..bd687a93b9be 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -40,7 +40,7 @@ require ( github.com/aws/smithy-go v1.24.2 github.com/docker/go-connections v0.6.0 github.com/dustin/go-humanize v1.0.1 - github.com/go-sql-driver/mysql v1.9.3 + github.com/go-sql-driver/mysql v1.10.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 @@ -82,7 +82,7 @@ require ( cloud.google.com/go/monitoring v1.25.0 // indirect cloud.google.com/go/pubsub/v2 v2.4.0 // indirect dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 160eb35a0eea..de692aa25498 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -111,8 +111,8 @@ contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0Wk dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= @@ -411,8 +411,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= From 2df9b3dcb22bd1b1e7cf80e85e33d57b9b56cacc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 08:59:04 -0400 Subject: [PATCH 114/490] Bump github.com/aws/aws-sdk-go-v2 from 1.41.5 to 1.41.7 in /sdks (#38416) Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.41.5 to 1.41.7. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.5...v1.41.7) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2 dependency-version: 1.41.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index bd687a93b9be..0658ba6d028f 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,12 +32,12 @@ require ( cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.88.0 cloud.google.com/go/storage v1.62.1 - github.com/aws/aws-sdk-go-v2 v1.41.5 + github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 - github.com/aws/smithy-go v1.24.2 + github.com/aws/smithy-go v1.25.1 github.com/docker/go-connections v0.6.0 github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 diff --git a/sdks/go.sum b/sdks/go.sum index de692aa25498..e24e305d123a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -199,8 +199,8 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= -github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= @@ -276,8 +276,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= From f0bc5506898b86d5066f43de32409594d74b6da6 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Fri, 8 May 2026 09:59:07 -0400 Subject: [PATCH 115/490] yaml_transform_test - add some debug logs and increase row count (#38367) * add some debug logs and increase row count * Update sdks/python/apache_beam/yaml/yaml_transform_test.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../apache_beam/yaml/yaml_transform_test.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/sdks/python/apache_beam/yaml/yaml_transform_test.py b/sdks/python/apache_beam/yaml/yaml_transform_test.py index a4da97f7f50e..bbb60b185c01 100644 --- a/sdks/python/apache_beam/yaml/yaml_transform_test.py +++ b/sdks/python/apache_beam/yaml/yaml_transform_test.py @@ -253,17 +253,8 @@ def test_csv_to_json(self): raise unittest.SkipTest('Pandas not available.') with tempfile.TemporaryDirectory() as tmpdir: - data = pd.DataFrame([ - { - 'label': '11a', 'rank': 0 - }, - { - 'label': '37a', 'rank': 1 - }, - { - 'label': '389a', 'rank': 2 - }, - ]) + data = pd.DataFrame([{'label': f'{i}a', 'rank': i} for i in range(1024)]) + input = os.path.join(tmpdir, 'input.csv') output = os.path.join(tmpdir, 'output.json') data.to_csv(input, index=False) @@ -286,9 +277,14 @@ def test_csv_to_json(self): num_shards: 1 - type: LogForTesting ''' % (repr(input), repr(output))) - all_output = list(glob.glob(output + "*")) - self.assertEqual(len(all_output), 1) - output_shard = list(glob.glob(output + "*"))[0] + all_output = list(glob.glob(output + "-*")) + file_and_size = {f: os.path.getsize(f) for f in all_output} + self.assertEqual( + len(all_output), + 1, + msg=f"Expected 1 shard file, but found {len(all_output)}. " + f"Files & sizes (bytes): {file_and_size}") + output_shard = all_output[0] result = pd.read_json( output_shard, orient='records', lines=True).sort_values('rank').reindex() From 729eaa6ed7f9e6c9f3c090f0adeb74ab64b4c631 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Fri, 8 May 2026 21:42:43 +0400 Subject: [PATCH 116/490] Fix test (#38418) --- .../org/apache/beam/sdk/transforms/MetadataPropagationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java index cba13ecb6bf2..72f914caf31e 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java @@ -83,6 +83,7 @@ public void testMetadataPropagationAcrossShuffleParameter() { @Test @Category({ValidatesRunner.class, NeedsRunner.class}) public void testMetadataPropagationParameter() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); PCollection results = pipeline .apply(Create.of(true)) From 2e91508a15e82311dd0bffa77b864e42bfcd0401 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 8 May 2026 19:51:18 +0200 Subject: [PATCH 117/490] fix rrio teardown executor cleanup path (#38417) --- .../apache/beam/io/requestresponse/Call.java | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java b/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java index b515957459be..b318cac17379 100644 --- a/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java +++ b/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java @@ -270,34 +270,35 @@ public void teardown() throws UserCodeExecutionException { Sleeper sleeper = configuration.getSleeperSupplier().get(); backoffIfNeeded(backOff, sleeper); - - if (!configuration.getShouldRepeat()) { - incIfPresent(teardownCounter); - setupTeardown.teardown(); - return; - } - - Repeater repeater = - Repeater.builder() - .setBackOff(backOff) - .setSleeper(sleeper) - .setThrowableFunction( - ignored -> { - incIfPresent(teardownCounter); - setupTeardown.teardown(); - return null; - }) - .build() - .withBackoffCounter(backoffCounter) - .withSleeperCounter(sleeperCounter); - - repeater.apply(null); - - checkStateNotNull(executor).shutdown(); try { - boolean ignored = executor.awaitTermination(3L, TimeUnit.SECONDS); - } catch (InterruptedException ignored) { - // Ignore the interrupt during teardown. + if (!configuration.getShouldRepeat()) { + incIfPresent(teardownCounter); + setupTeardown.teardown(); + return; + } + + Repeater repeater = + Repeater.builder() + .setBackOff(backOff) + .setSleeper(sleeper) + .setThrowableFunction( + ignored -> { + incIfPresent(teardownCounter); + setupTeardown.teardown(); + return null; + }) + .build() + .withBackoffCounter(backoffCounter) + .withSleeperCounter(sleeperCounter); + + repeater.apply(null); + } finally { + checkStateNotNull(executor).shutdown(); + try { + boolean ignored = executor.awaitTermination(3L, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + // Ignore the interrupt during teardown. + } } } From ecee63576c8cad232bc52bfd2ebc52aa525471c2 Mon Sep 17 00:00:00 2001 From: Arran Cudbard-Bell Date: Fri, 8 May 2026 14:13:18 -0400 Subject: [PATCH 118/490] Fix SystemError in _DeferredCall.get() under GC pressure (#38355) CPython builds the argument tuple incrementally via _PyTuple_Resize when unpacking a generator: f(*(gen)). _PyTuple_Resize guards that Py_REFCNT(v) == 1 before resizing a non-empty tuple. Under sustained allocation pressure a GC cycle can run between generator yields and temporarily increment the refcount on the partial tuple, causing _PyTuple_Resize to call PyErr_BadInternalCall(). Change the generator expression to a list comprehension. CPython builds the list first and passes it to CALL_FUNCTION_EX, which takes the PySequence_Fast list path and never calls _PyTuple_Resize. Co-authored-by: Arran Cudbard-Bell --- .../apache_beam/runners/worker/sdk_worker.py | 9 +++++- .../runners/worker/sdk_worker_test.py | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/worker/sdk_worker.py b/sdks/python/apache_beam/runners/worker/sdk_worker.py index e1f17296057a..a79cb0e8de6e 100644 --- a/sdks/python/apache_beam/runners/worker/sdk_worker.py +++ b/sdks/python/apache_beam/runners/worker/sdk_worker.py @@ -1448,7 +1448,14 @@ def wait(self, timeout=None): def get(self, timeout=None): # type: (Optional[float]) -> T - return self._func(*(arg.get(timeout) for arg in self._args)) + # List comprehension, not generator: *(gen) causes CPython to build the + # argument tuple incrementally via _PyTuple_Resize, which asserts + # Py_REFCNT(v)==1. A GC cycle between yields can increment that refcount, + # raising SystemError (Objects/tupleobject.c:927). See + # https://github.com/python/cpython/issues/127058 (fixed in 3.14.0a3+: + # https://github.com/python/cpython/commit/5a23994). *[list] allocates the + # tuple once at its final size, avoiding the resize entirely. + return self._func(*[arg.get(timeout) for arg in self._args]) def set(self, value): # type: (T) -> _Future[T] diff --git a/sdks/python/apache_beam/runners/worker/sdk_worker_test.py b/sdks/python/apache_beam/runners/worker/sdk_worker_test.py index 7b53f274cac2..76e428f06464 100644 --- a/sdks/python/apache_beam/runners/worker/sdk_worker_test.py +++ b/sdks/python/apache_beam/runners/worker/sdk_worker_test.py @@ -704,6 +704,38 @@ def testShortIdAssignment(self): % case.info) +class DeferredCallTest(unittest.TestCase): + """Tests for _DeferredCall.get().""" + def test_get_single_arg(self): + f = sdk_worker._Future().set(42) + call = sdk_worker._DeferredCall(lambda x: x, f) + self.assertEqual(call.get(), 42) + + def test_get_multiple_args(self): + futures = [sdk_worker._Future().set(i) for i in range(5)] + call = sdk_worker._DeferredCall(lambda *args: sum(args), *futures) + self.assertEqual(call.get(), sum(range(5))) + + def test_get_non_future_args_are_wrapped(self): + # __init__ wraps non-Future values in _Future().set(v); get() must work. + call = sdk_worker._DeferredCall(lambda x, y: x * y, 3, 7) + self.assertEqual(call.get(), 21) + + def test_get_mixed_future_and_value_args(self): + a = sdk_worker._Future().set(10) + call = sdk_worker._DeferredCall(lambda x, y: x + y, a, 5) + self.assertEqual(call.get(), 15) + + def test_get_zero_args(self): + call = sdk_worker._DeferredCall(lambda: 99) + self.assertEqual(call.get(), 99) + + def test_get_preserves_return_value_type(self): + f = sdk_worker._Future().set({'key': 'val'}) + call = sdk_worker._DeferredCall(lambda d: d, f) + self.assertEqual(call.get(), {'key': 'val'}) + + def monitoringInfoMetadata(info): return { descriptor.name: value From 7aaa4f747ee5e10209feb604de55d0976b205d85 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 8 May 2026 15:44:02 -0400 Subject: [PATCH 119/490] =?UTF-8?q?Revert=20"Revert=20"Bump=20com.pswiders?= =?UTF-8?q?k.terraform-plugin=20from=201.0.0=20to=201.1.1=20(#325=E2=80=A6?= =?UTF-8?q?"=20(#38420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 72d46ce0caf8daa37f88d2de860b211e4d352c2a. --- learning/tour-of-beam/terraform/build.gradle.kts | 2 +- playground/terraform/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/learning/tour-of-beam/terraform/build.gradle.kts b/learning/tour-of-beam/terraform/build.gradle.kts index 30d30309273c..3080f74a5149 100644 --- a/learning/tour-of-beam/terraform/build.gradle.kts +++ b/learning/tour-of-beam/terraform/build.gradle.kts @@ -21,7 +21,7 @@ import java.io.ByteArrayOutputStream import java.util.regex.Pattern plugins { - id("com.pswidersk.terraform-plugin") version "1.0.0" + id("com.pswidersk.terraform-plugin") version "1.1.1" } terraformPlugin { diff --git a/playground/terraform/build.gradle.kts b/playground/terraform/build.gradle.kts index fb1520e50df3..ea10fa0e94ac 100644 --- a/playground/terraform/build.gradle.kts +++ b/playground/terraform/build.gradle.kts @@ -39,7 +39,7 @@ val licenseText = "############################################################# "################################################################################" plugins { - id("com.pswidersk.terraform-plugin") version "1.0.0" + id("com.pswidersk.terraform-plugin") version "1.1.1" } terraformPlugin { From 41b5c70a54457c3b14979ccafb8e3ab67cb315c8 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Fri, 8 May 2026 18:04:28 -0400 Subject: [PATCH 120/490] Revert "[yaml] - Add huggingface model handler (#38110)" (#38421) This reverts commit 422f6304af199207462127d09d58fddc33398052. --- .../beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- .../beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- ...erence_vertexai.yaml => runinference.yaml} | 0 .../yaml/tests/runinference_huggingface.yaml | 62 ------------------- sdks/python/apache_beam/yaml/yaml_ml.py | 49 --------------- sdks/python/build.gradle | 14 +---- 6 files changed, 4 insertions(+), 125 deletions(-) rename sdks/python/apache_beam/yaml/tests/{runinference_vertexai.yaml => runinference.yaml} (100%) delete mode 100644 sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index afa437b64de1..ea1c255f7cc9 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -80,7 +80,7 @@ jobs: - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} + gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 0b8f4cd63939..7d17fd2140c9 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -91,7 +91,7 @@ jobs: - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:yamlIntegrationTests + gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml b/sdks/python/apache_beam/yaml/tests/runinference.yaml similarity index 100% rename from sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml rename to sdks/python/apache_beam/yaml/tests/runinference.yaml diff --git a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml deleted file mode 100644 index 8728a6f544ad..000000000000 --- a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml +++ /dev/null @@ -1,62 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -pipelines: - - pipeline: - type: chain - transforms: - - type: Create - config: - elements: - - text: "I love Apache Beam!" - - text: "I hate this error." - - type: RunInference - config: - model_handler: - type: "HuggingFacePipeline" - config: - task: "text-classification" - inference_fn: - callable: | - def real_inference(batch, pipeline, inference_args): - predictions = pipeline(batch, **inference_args) - - # If it's a single dictionary (batch size of 1), wrap it in a list - if isinstance(predictions, dict): - predictions = [predictions] - - return { - 'label': [p['label'] for p in predictions], - 'score': [p['score'] for p in predictions] - } - preprocess: - callable: 'lambda x: x.text' - - type: MapToFields - config: - language: python - fields: - text: text - sentiment: - callable: 'lambda x: x.inference.inference["label"]' - - type: AssertEqual - config: - elements: - - text: "I love Apache Beam!" - sentiment: "POSITIVE" - - text: "I hate this error." - sentiment: "NEGATIVE" - - options: - yaml_experimental_features: ['ML'] diff --git a/sdks/python/apache_beam/yaml/yaml_ml.py b/sdks/python/apache_beam/yaml/yaml_ml.py index 05cbed3bd456..51f18c733046 100644 --- a/sdks/python/apache_beam/yaml/yaml_ml.py +++ b/sdks/python/apache_beam/yaml/yaml_ml.py @@ -282,55 +282,6 @@ def inference_output_type(self): ('model_id', Optional[str])]) -@ModelHandlerProvider.register_handler_type('HuggingFacePipeline') -class HuggingFacePipelineProvider(ModelHandlerProvider): - def __init__( - self, - task: Optional[str] = None, - model: Optional[str] = None, - preprocess: Optional[dict[str, str]] = None, - postprocess: Optional[dict[str, str]] = None, - device: Optional[Any] = None, - inference_fn: Optional[dict[str, str]] = None, - load_pipeline_args: Optional[dict[str, Any]] = None, - **kwargs): - try: - from apache_beam.ml.inference.huggingface_inference import HuggingFacePipelineModelHandler - except ImportError: - raise ValueError( - 'Unable to import HuggingFacePipelineModelHandler. Please ' - 'install transformers dependencies.') - - kwargs = {k: v for k, v in kwargs.items() if not k.startswith('_')} - - inference_fn_obj = self.parse_processing_transform( - inference_fn, 'inference_fn') if inference_fn else None - - handler_kwargs = {} - if inference_fn_obj: - handler_kwargs['inference_fn'] = inference_fn_obj - - _handler = HuggingFacePipelineModelHandler( - task=task, - model=model, - device=device, - load_pipeline_args=load_pipeline_args, - **handler_kwargs, - **kwargs) - - super().__init__(_handler, preprocess, postprocess) - - @staticmethod - def validate(config): - if not config.get('task') and not config.get('model'): - raise ValueError( - "HuggingFacePipeline requires either 'task' or " - "'model' to be specified.") - - def inference_output_type(self): - return Any - - @beam.ptransform.ptransform_fn def run_inference( pcoll, diff --git a/sdks/python/build.gradle b/sdks/python/build.gradle index e676fd110433..5f09dff57e8f 100644 --- a/sdks/python/build.gradle +++ b/sdks/python/build.gradle @@ -124,20 +124,10 @@ tasks.register("generateYamlDocs") { outputs.file "${buildDir}/yaml-examples.html" } -tasks.register("installYamlIntegrationTestDeps") { - dependsOn installGcpTest - doLast { - exec { - executable 'sh' - args '-c', ". ${envdir}/bin/activate && pip install --pre --retries 10 ${buildDir}/apache-beam.tar.gz[ml_test,yaml,transformers]" - } - } -} - tasks.register("yamlIntegrationTests") { description "Runs precommit integration tests for yaml pipelines." - dependsOn installYamlIntegrationTestDeps + dependsOn installGcpTest // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" @@ -156,7 +146,7 @@ tasks.register("yamlIntegrationTests") { tasks.register("postCommitYamlIntegrationTests") { description "Runs postcommit integration tests for yaml pipelines - parameterized by yamlTestSet." - dependsOn installYamlIntegrationTestDeps + dependsOn installGcpTest // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" From 7318d4e2c0089641492f951a83e59265038cf923 Mon Sep 17 00:00:00 2001 From: TongruiLi <12992126+TongruiLi@users.noreply.github.com> Date: Fri, 8 May 2026 16:05:44 -0700 Subject: [PATCH 121/490] [Dataflow] Added Portable Runner alias to java runners (#38411) * Added portable runner options to java runner * spotless * Added more tolerance to flaky test * Removed unused experiments * Added experiments back in --- .../beam/runners/dataflow/DataflowRunner.java | 11 +++++---- .../runners/dataflow/DataflowRunnerTest.java | 24 +++++++++++++++---- ...UnboundedScheduledExecutorServiceTest.java | 2 +- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index ecc231ab825e..299e7fa21ed1 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1244,8 +1244,8 @@ public DataflowPipelineJob run(Pipeline pipeline) { // Multi-language pipelines and pipelines that include upgrades should automatically be upgraded // to Runner v2. if (DataflowRunner.isMultiLanguagePipeline(pipeline) || includesTransformUpgrades(pipeline)) { - List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); - if (!experiments.contains("use_runner_v2")) { + if (!useUnifiedWorker(options)) { + List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); LOG.info( "Automatically enabling Dataflow Runner v2 since the pipeline used cross-language" + " transforms or pipeline needed a transform upgrade."); @@ -1256,7 +1256,9 @@ public DataflowPipelineJob run(Pipeline pipeline) { if (useUnifiedWorker(options)) { if (hasExperiment(options, "disable_runner_v2") || hasExperiment(options, "disable_runner_v2_until_2023") - || hasExperiment(options, "disable_prime_runner_v2")) { + || hasExperiment(options, "disable_prime_runner_v2") + || hasExperiment(options, "disable_portable_runner") + || hasExperiment(options, "enable_streaming_java_runner")) { throw new IllegalArgumentException( "Runner V2 both disabled and enabled: at least one of ['beam_fn_api', 'use_unified_worker', 'use_runner_v2', 'use_portable_job_submission'] is set and also one of ['disable_runner_v2', 'disable_runner_v2_until_2023', 'disable_prime_runner_v2'] is set."); } @@ -2729,7 +2731,8 @@ static boolean useUnifiedWorker(DataflowPipelineOptions options) { return hasExperiment(options, "beam_fn_api") || hasExperiment(options, "use_runner_v2") || hasExperiment(options, "use_unified_worker") - || hasExperiment(options, "use_portable_job_submission"); + || hasExperiment(options, "use_portable_job_submission") + || hasExperiment(options, "enable_portable_runner"); } static void verifyDoFnSupported( diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java index 8c33123be6d5..ab3b62a0aa1b 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java @@ -1783,7 +1783,11 @@ public void testSdkHarnessConfigurationPrime() throws IOException { public void testSettingAnyFnApiExperimentEnablesUnifiedWorker() throws Exception { for (String experiment : ImmutableList.of( - "beam_fn_api", "use_runner_v2", "use_unified_worker", "use_portable_job_submission")) { + "beam_fn_api", + "use_runner_v2", + "use_unified_worker", + "use_portable_job_submission", + "enable_portable_runner")) { DataflowPipelineOptions options = buildPipelineOptions(); ExperimentalOptions.addExperiment(options, experiment); Pipeline p = Pipeline.create(options); @@ -1798,7 +1802,11 @@ public void testSettingAnyFnApiExperimentEnablesUnifiedWorker() throws Exception for (String experiment : ImmutableList.of( - "beam_fn_api", "use_runner_v2", "use_unified_worker", "use_portable_job_submission")) { + "beam_fn_api", + "use_runner_v2", + "use_unified_worker", + "use_portable_job_submission", + "enable_portable_runner")) { DataflowPipelineOptions options = buildPipelineOptions(); options.setStreaming(true); ExperimentalOptions.addExperiment(options, experiment); @@ -1822,10 +1830,18 @@ public void testSettingAnyFnApiExperimentEnablesUnifiedWorker() throws Exception public void testSettingConflictingEnableAndDisableExperimentsThrowsException() throws Exception { for (String experiment : ImmutableList.of( - "beam_fn_api", "use_runner_v2", "use_unified_worker", "use_portable_job_submission")) { + "beam_fn_api", + "use_runner_v2", + "use_unified_worker", + "use_portable_job_submission", + "enable_portable_runner")) { for (String disabledExperiment : ImmutableList.of( - "disable_runner_v2", "disable_runner_v2_until_2023", "disable_prime_runner_v2")) { + "disable_runner_v2", + "disable_runner_v2_until_2023", + "disable_prime_runner_v2", + "enable_streaming_java_runner", + "disable_portable_runner")) { DataflowPipelineOptions options = buildPipelineOptions(); ExperimentalOptions.addExperiment(options, experiment); ExperimentalOptions.addExperiment(options, disabledExperiment); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java index efbb03519789..bba10c84fab6 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java @@ -625,7 +625,7 @@ public void testThreadsAreAddedOnlyAsNeededWithContention() throws Exception { LOG.info("Created {} threads to execute at most 100 parallel tasks", largestPool); // Ideally we would never create more than 100, however with contention it is still possible // some extra threads will be created. - assertTrue(largestPool <= 110); + assertTrue(largestPool <= 120); executorService.shutdown(); } } From 40ae0ec22dfe1479735d578a4aa77b929666b5b0 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 8 May 2026 22:21:39 -0400 Subject: [PATCH 122/490] Fix race conditions, error recovery, and exit handlers in job servers (#38423) --- .../runners/portability/job_server.py | 9 +- .../apache_beam/utils/subprocess_server.py | 16 ++- .../utils/subprocess_server_test.py | 124 ++++++++++++++++++ 3 files changed, 140 insertions(+), 9 deletions(-) diff --git a/sdks/python/apache_beam/runners/portability/job_server.py b/sdks/python/apache_beam/runners/portability/job_server.py index 9fdaabd1a177..53688e0be950 100644 --- a/sdks/python/apache_beam/runners/portability/job_server.py +++ b/sdks/python/apache_beam/runners/portability/job_server.py @@ -94,8 +94,13 @@ def start(self): def stop(self): with self._lock: if self._started: - self._job_server.stop() - self._started = False + try: + self._job_server.stop() + finally: + self._started = False + # Unregister the atexit handler to prevent duplicate + # registrations when the server is restarted/reused. + atexit.unregister(self.stop) class SubprocessJobServer(JobServer): diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index 988bd680b923..5752a49dde2e 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -88,11 +88,11 @@ def register(self): return owner def purge(self, owner): - if owner not in self._live_owners: - raise ValueError(f"{owner} not in {self._live_owners}") - self._live_owners.remove(owner) to_delete = [] with self._lock: + if owner not in self._live_owners: + raise ValueError(f"{owner} not in {self._live_owners}") + self._live_owners.remove(owner) for key, entry in list(self._cache.items()): if owner in entry.owners: entry.owners.remove(owner) @@ -255,15 +255,17 @@ def stop(self): def stop_process(self): if self._owner_id is not None: - self._cache.purge(self._owner_id) - self._owner_id = None + try: + self._cache.purge(self._owner_id) + finally: + # Make sure _owner_id is set to None even if purge fails. + self._owner_id = None if self._grpc_channel: try: self._grpc_channel.close() except: # pylint: disable=bare-except _LOGGER.error( - "Could not close the gRPC channel started for the " - "expansion service") + "Could not close the gRPC channel started with cmd %s", self._cmd) finally: self._grpc_channel = None diff --git a/sdks/python/apache_beam/utils/subprocess_server_test.py b/sdks/python/apache_beam/utils/subprocess_server_test.py index c848595db355..0f25d9904f07 100644 --- a/sdks/python/apache_beam/utils/subprocess_server_test.py +++ b/sdks/python/apache_beam/utils/subprocess_server_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +import atexit import glob import os import random @@ -29,7 +30,9 @@ import tempfile import threading import unittest +from unittest.mock import patch +from apache_beam.runners.portability import job_server from apache_beam.utils import subprocess_server @@ -302,6 +305,127 @@ def test_interleaved_owners(self): self.assertNotEqual(cache.get('b'), b) cache.purge(owner3) + def test_destructor_exception_partial_state(self): + # In SubprocessServer.stop_process(), we need to make sure self._owner_id is always + # set to None if it is not already set, even if a destructor exception happens + # during purge(owner_id). + + destructor_calls = [] + + def faulty_destructor(obj): + destructor_calls.append(obj) + raise RuntimeError("Destructor failed") + + custom_cache = subprocess_server._SharedCache( + lambda *args: "process_obj", faulty_destructor) + + class CustomServer(subprocess_server.SubprocessServer): + _cache = custom_cache + + def __init__(self): + super().__init__(lambda channel: None, ["dummy_cmd"], port=12345) + + server = CustomServer() + server.start_process() + owner_id = server._owner_id + self.assertIsNotNone(owner_id) + self.assertIn(owner_id, custom_cache._live_owners) + + # First stop attempt fails in the destructor + with self.assertRaises(RuntimeError): + server.stop_process() + + # Verify fixed state: owner is purged from cache set, AND self._owner_id is successfully cleared to None + self.assertNotIn(owner_id, custom_cache._live_owners) + self.assertIsNone(server._owner_id) + + # Second stop attempt safely does nothing (no ValueError raised) + try: + server.stop_process() + except ValueError: + self.fail("ValueError should not be raised here.") + + def test_duplicate_atexit_registration_on_restart(self): + # Make sure we don't have duplicate atexit registration when reusing a + # StopOnExistJobServer instance. + + class DummyJobServer(job_server.JobServer): + def start(self): + return "localhost:8080" + + def stop(self): + pass + + wrapper = job_server.StopOnExitJobServer(DummyJobServer()) + + registered_callbacks = [] + + def mock_register(cb): + registered_callbacks.append(cb) + + def mock_unregister(cb): + if cb in registered_callbacks: + registered_callbacks.remove(cb) + + with patch('atexit.register', side_effect=mock_register), \ + patch('atexit.unregister', side_effect=mock_unregister, create=True): + # First start registers stop callback + wrapper.start() + self.assertTrue(wrapper._started) + self.assertEqual(len(registered_callbacks), 1) + + # Explicit stop clears _started AND unregisters the callback + wrapper.stop() + self.assertFalse(wrapper._started) + self.assertEqual(len(registered_callbacks), 0) + + # Re-starting registers the callback again, leaving exactly 1 active callback + wrapper.start() + self.assertTrue(wrapper._started) + self.assertEqual(len(registered_callbacks), 1) + + def test_concurrent_purge_race_condition(self): + # Concurrent threads attempting to check memebership and call purge for the same owner. + # Here we explicitly define a synchronized set to mimic the behavior of _live_owners. + # This set will block two threads on __contains__, allowing us to test the race condition. + cache = subprocess_server._SharedCache(lambda x: "obj", lambda x: None) + owner = cache.register() + + barrier = threading.Barrier(2) + exceptions = [] + + class SynchronizedSet(set): + def __contains__(self, item): + res = super().__contains__(item) + try: + # Force both threads to align right after checking membership but before removal + barrier.wait(timeout=0.2) + except threading.BrokenBarrierError: + pass + return res + + cache._live_owners = SynchronizedSet(cache._live_owners) + + def purge_worker(): + try: + cache.purge(owner) + except Exception as e: + exceptions.append(e) + + t1 = threading.Thread(target=purge_worker) + t2 = threading.Thread(target=purge_worker) + + t1.start() + t2.start() + + t1.join() + t2.join() + + # Exactly one thread should raise the expected ValueError because they are cleanly serialized + self.assertEqual(len(exceptions), 1) + self.assertIsInstance(exceptions[0], ValueError) + self.assertNotIsInstance(exceptions[0], KeyError) + if __name__ == '__main__': unittest.main() From 88766b4705cb9b8ab88aa657fbfe00a2c4159c56 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 11 May 2026 04:35:16 -0400 Subject: [PATCH 123/490] Fix PreCommit Java PVR Prism Loopback workflow (#38431) * Remove ValidatesRunner from testMetadataPropagationParameter --- .../beam/sdk/transforms/MetadataPropagationTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java index 72f914caf31e..3e5ff77cb1c4 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java @@ -22,7 +22,6 @@ import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.testing.TestStream; -import org.apache.beam.sdk.testing.ValidatesRunner; import org.apache.beam.sdk.transforms.windowing.FixedWindows; import org.apache.beam.sdk.transforms.windowing.IntervalWindow; import org.apache.beam.sdk.transforms.windowing.Window; @@ -80,8 +79,13 @@ public void testMetadataPropagationAcrossShuffleParameter() { pipeline.run(); } + /** + * Tests metadata propagation for a parameter. Note: PAssert internally introduces a GroupByKey. + * This test works only with DirectRunner and runners that support metadata propagation across + * GroupByKey. See {@link #testMetadataPropagationAcrossGBK} for more information. + */ @Test - @Category({ValidatesRunner.class, NeedsRunner.class}) + @Category(NeedsRunner.class) public void testMetadataPropagationParameter() { WindowedValues.WindowedValueCoder.setMetadataSupported(); PCollection results = From 986c3cde41530028de95104d9a6bbc07e0524295 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 08:00:22 -0400 Subject: [PATCH 124/490] Bump cloud.google.com/go/profiler from 0.4.3 to 0.6.0 in /sdks (#38436) Bumps [cloud.google.com/go/profiler](https://github.com/googleapis/google-cloud-go) from 0.4.3 to 0.6.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/apps/v0.4.3...v0.6.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/profiler dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 0658ba6d028f..58cf13e09951 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -28,7 +28,7 @@ require ( cloud.google.com/go/bigquery v1.77.0 cloud.google.com/go/bigtable v1.47.0 cloud.google.com/go/datastore v1.23.0 - cloud.google.com/go/profiler v0.4.3 + cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.88.0 cloud.google.com/go/storage v1.62.1 @@ -172,7 +172,7 @@ require ( github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v24.12.23+incompatible // indirect - github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index e24e305d123a..a33396d36efb 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -78,8 +78,8 @@ cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TEl cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc= -cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI= -cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= +cloud.google.com/go/profiler v0.6.0 h1:Vwxqgnp8CQwBNcKjO8luwLCh7qblEkcZCtMUvhU9Yik= +cloud.google.com/go/profiler v0.6.0/go.mod h1:cJV7Qfj0o9PAC7q/xQTkM6qn2FO9So3TFk4P5O5yLis= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -530,8 +530,8 @@ github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e h1:FJta/0WsADCe1r9vQjdHbd3KuiLPu7Y9WlyLGwMUNyE= -github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= From 4187338c98f9d6c4bf68632190dec2cb3bad94f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 08:02:36 -0400 Subject: [PATCH 125/490] Bump golang.org/x/sys from 0.43.0 to 0.44.0 in /sdks (#38435) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.43.0 to 0.44.0. - [Commits](https://github.com/golang/sys/compare/v0.43.0...v0.44.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 58cf13e09951..fcf41c709bc0 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -58,7 +58,7 @@ require ( golang.org/x/net v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.43.0 + golang.org/x/sys v0.44.0 golang.org/x/text v0.36.0 google.golang.org/api v0.278.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 diff --git a/sdks/go.sum b/sdks/go.sum index a33396d36efb..36f64860b3bd 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1174,8 +1174,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= From e3e27a4753333f267177f24776601164b91e1505 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 08:03:10 -0400 Subject: [PATCH 126/490] Bump cloud.google.com/go/spanner from 1.88.0 to 1.91.0 in /sdks (#38434) Bumps [cloud.google.com/go/spanner](https://github.com/googleapis/google-cloud-go) from 1.88.0 to 1.91.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/spanner/v1.88.0...spanner/v1.91.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/spanner dependency-version: 1.91.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index fcf41c709bc0..46e1aec0d241 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -30,7 +30,7 @@ require ( cloud.google.com/go/datastore v1.23.0 cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 - cloud.google.com/go/spanner v1.88.0 + cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.1 github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.7 diff --git a/sdks/go.sum b/sdks/go.sum index 36f64860b3bd..25c8f479400d 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -90,8 +90,8 @@ cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkN cloud.google.com/go/pubsub/v2 v2.4.0 h1:oMKNiBQpXImRWnHYla9uSU66ZzByZwBSCJOEs/pTKVg= cloud.google.com/go/pubsub/v2 v2.4.0/go.mod h1:2lS/XQKq5qtOMs6kHBK+WX1ytUC36kLl2ig3zqsGUx8= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= -cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= -cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= +cloud.google.com/go/spanner v1.91.0 h1:XwXfcZ0kc1NT9Uu2IsThFiWtYptB+WgLn/KZEZcyzRg= +cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= From f158cfbcf340974a1458c82314f94e2b4c5cffc3 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 11 May 2026 08:55:13 -0400 Subject: [PATCH 127/490] Fix flaky BigQuery file loads by safely handling concurrent mkdirs (#38426) --- sdks/python/apache_beam/io/gcp/bigquery_file_loads.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py index 738ace67a5f7..4e45d0324ee2 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py @@ -134,7 +134,13 @@ def _make_new_file_writer( directory = fs.FileSystems.join(file_prefix, destination) if not fs.FileSystems.exists(directory): - fs.FileSystems.mkdirs(directory) + try: + fs.FileSystems.mkdirs(directory) + except IOError: + # Concurrent workers may race to create the same directory. + # Ignore the IOError if another worker successfully created it. + if not fs.FileSystems.exists(directory): + raise file_name = str(uuid.uuid4()) file_path = fs.FileSystems.join(file_prefix, destination, file_name) From f65b35ff01c02c60e9ea90e3572a0ba373415b31 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Mon, 11 May 2026 09:34:12 -0400 Subject: [PATCH 128/490] upgrade test containers and fix issues (#38438) --- sdks/go.mod | 19 ++++---- sdks/go.sum | 46 +++++++++++-------- .../internal/containers/containers.go | 3 +- .../integration/io/spannerio/test_helpers.go | 3 +- .../integration/io/xlang/jdbc/jdbc_test.go | 5 +- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 46e1aec0d241..49dcfc834ab4 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -50,7 +50,7 @@ require ( github.com/nats-io/nats.go v1.51.0 github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 - github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go v0.42.0 github.com/tetratelabs/wazero v1.11.0 github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b @@ -96,7 +96,7 @@ require ( github.com/containerd/platforms v0.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -106,7 +106,10 @@ require ( github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/minio/highwayhash v1.0.4 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/nats-io/jwt/v2 v2.8.1 // indirect @@ -115,11 +118,11 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.9.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.einride.tech/aip v0.83.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -183,7 +186,7 @@ require ( github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect @@ -194,7 +197,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pkg/xattr v0.4.10 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 25c8f479400d..43dfdd2ed727 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -324,8 +324,8 @@ github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GK github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -347,8 +347,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -695,10 +695,14 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= @@ -776,15 +780,15 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -799,8 +803,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -814,16 +818,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= -github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= -github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= -github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -1526,6 +1530,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/sdks/go/test/integration/internal/containers/containers.go b/sdks/go/test/integration/internal/containers/containers.go index 0b8ab6e45076..e0473f0416e5 100644 --- a/sdks/go/test/integration/internal/containers/containers.go +++ b/sdks/go/test/integration/internal/containers/containers.go @@ -22,7 +22,6 @@ import ( "time" retry "github.com/avast/retry-go/v4" - "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) @@ -106,7 +105,7 @@ func Port( ctx context.Context, t *testing.T, container testcontainers.Container, - port nat.Port, + port string, ) string { t.Helper() diff --git a/sdks/go/test/integration/io/spannerio/test_helpers.go b/sdks/go/test/integration/io/spannerio/test_helpers.go index 47a27f1a97be..8c170a548991 100644 --- a/sdks/go/test/integration/io/spannerio/test_helpers.go +++ b/sdks/go/test/integration/io/spannerio/test_helpers.go @@ -27,7 +27,6 @@ import ( adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" instance "cloud.google.com/go/spanner/admin/instance/apiv1" instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" - "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go/wait" "google.golang.org/api/option" "google.golang.org/grpc" @@ -59,7 +58,7 @@ func setUpTestContainer(ctx context.Context, t *testing.T) string { containers.WithWaitStrategy(wait.ForLog("Cloud Spanner emulator running")), ) - mappedPort := containers.Port(ctx, t, container, nat.Port(spannerPorts[0])) + mappedPort := containers.Port(ctx, t, container, spannerPorts[0]) hostIP, err := container.Host(ctx) if err != nil { diff --git a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go index 93d9f4c07ffc..4cd3c38a3bf2 100644 --- a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go +++ b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go @@ -30,7 +30,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" "github.com/apache/beam/sdks/v2/go/test/integration/internal/containers" - "github.com/docker/go-connections/nat" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "github.com/testcontainers/testcontainers-go/wait" @@ -60,8 +59,8 @@ func setupTestContainer(ctx context.Context, t *testing.T, dbname, username, pas } hostname := "localhost" - dbURL := func(host string, port nat.Port) string { - return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, port.Port(), dbname) + dbURL := func(host string, port string) string { + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, port, dbname) } waitStrategy := wait.ForSQL(postgresPort, "postgres", dbURL).WithStartupTimeout(time.Second * 5) From 888f580ac197a6f5476644a074f3660f0b4644b3 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Mon, 11 May 2026 10:42:09 -0400 Subject: [PATCH 129/490] Introduce ValueKind to Java and add to WindowedValue (#38315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * introduce ValueKind to Java and add to WindowedValue * map UNSPECIFIED to INSERT; cleanup * rebase remnants * adjust equals and hashcode methods to include valuekind * use existing coder test * compile fixes * compile fixes in dataflow * compile fixes in dataflow * add test to WindmillKeyedWorkItem * compile fixes --- .../core/LateDataDroppingDoFnRunner.java | 3 +- ...oundedSplittableProcessElementInvoker.java | 6 +- .../SplittableParDoViaKeyedWorkItems.java | 3 +- .../runners/dataflow/BatchViewOverrides.java | 6 + .../worker/UngroupedWindmillReader.java | 10 +- .../worker/WindmillKeyedWorkItem.java | 14 +- .../worker/util/ValueInEmptyWindows.java | 6 + .../worker/WindmillKeyedWorkItemTest.java | 43 ++++ .../beam/runners/spark/util/TimerUtils.java | 6 + .../apache/beam/sdk/values/OutputBuilder.java | 2 + .../beam/sdk/values/ValueInSingleWindow.java | 52 +++- .../org/apache/beam/sdk/values/ValueKind.java | 40 +++ .../apache/beam/sdk/values/ValueKindUtil.java | 54 ++++ .../apache/beam/sdk/values/WindowedValue.java | 4 + .../beam/sdk/values/WindowedValues.java | 230 ++++++++++++++---- .../beam/sdk/util/WindowedValueTest.java | 5 +- .../beam/fn/harness/FnApiDoFnRunner.java | 3 +- 17 files changed, 428 insertions(+), 59 deletions(-) create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java index 2ce94533d9e2..41052a76f13e 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java @@ -152,7 +152,8 @@ public Iterable> filter( element.getRecordId(), element.getRecordOffset(), element.causedByDrain(), - element.getOpenTelemetryContext())); + element.getOpenTelemetryContext(), + element.getValueKind())); } } } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java index dc84b15c89f4..ebd88442b211 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java @@ -452,7 +452,8 @@ public void outputWithTimestamp(TupleTag tag, T value, Instant timestamp) element.getRecordId(), element.getRecordOffset(), element.causedByDrain(), - element.getOpenTelemetryContext())); + element.getOpenTelemetryContext(), + element.getValueKind())); } @Override @@ -476,7 +477,8 @@ public void outputWindowedValue( element.getRecordId(), element.getRecordOffset(), element.causedByDrain(), - element.getOpenTelemetryContext())); + element.getOpenTelemetryContext(), + element.getValueKind())); } @Override diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java index 7a0f984479ab..424ea567115f 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java @@ -473,7 +473,8 @@ public String getErrorContext() { read.getRecordId(), read.getRecordOffset(), CausedByDrain.CAUSED_BY_DRAIN, - read.getOpenTelemetryContext()); + read.getOpenTelemetryContext(), + read.getValueKind()); } elementAndRestriction = KV.of(read, restrictionState.read()); watermarkEstimatorStateT = watermarkEstimatorState.read(); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java index e5ac4e99285d..b15b3f3834d2 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java @@ -74,6 +74,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.TupleTagList; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; @@ -1415,6 +1416,11 @@ public PaneInfo getPaneInfo() { return null; } + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public Iterable> explodeWindows() { return Collections.emptyList(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java index 8ef0bf80323a..0b883c048462 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java @@ -38,6 +38,8 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.beam.sdk.values.ValueKindUtil; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; @@ -139,6 +141,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce * drain happened upstream */ CausedByDrain drainingValueFromUpstream = CausedByDrain.NORMAL; + ValueKind valueKind = ValueKind.INSERT; if (WindowedValues.WindowedValueCoder.isMetadataSupported()) { BeamFnApi.Elements.ElementMetadata elementMetadata = WindmillSink.decodeAdditionalMetadata(windowsCoder, message.getMetadata()); @@ -146,6 +149,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); } if (valueCoder instanceof KvCoder) { KvCoder kvCoder = (KvCoder) valueCoder; @@ -164,7 +168,8 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce null, null, drainingValueFromUpstream, - null); + null, + valueKind); } else { notifyElementRead(data.available() + metadata.available()); // todo #37030 parse context from previous stage @@ -176,7 +181,8 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce null, null, drainingValueFromUpstream, - null); + null, + valueKind); } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java index 4c968030eef5..033e4186158d 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java @@ -42,6 +42,8 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.util.common.ElementByteSizeObserver; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.beam.sdk.values.ValueKindUtil; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicate; @@ -148,6 +150,7 @@ public Iterable timersIterable() { * drain happened upstream */ CausedByDrain drainingValueFromUpstream = CausedByDrain.NORMAL; + ValueKind valueKind = ValueKind.INSERT; if (WindowedValues.WindowedValueCoder.isMetadataSupported()) { BeamFnApi.Elements.ElementMetadata elementMetadata = WindmillSink.decodeAdditionalMetadata(windowsCoder, message.getMetadata()); @@ -155,11 +158,20 @@ public Iterable timersIterable() { elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); } InputStream inputStream = message.getData().newInput(); ElemT value = valueCoder.decode(inputStream, Coder.Context.OUTER); return WindowedValues.of( - value, timestamp, windows, paneInfo, null, null, drainingValueFromUpstream, null); + value, + timestamp, + windows, + paneInfo, + null, + null, + drainingValueFromUpstream, + null, + valueKind); } catch (RuntimeException | IOException e) { if (!skipUndecodableElements) { throw new RuntimeException(e); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java index 67ffdc9f3b8b..d5a493577f46 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java @@ -24,6 +24,7 @@ import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.checkerframework.checker.nullness.qual.Nullable; @@ -71,6 +72,11 @@ public CausedByDrain causedByDrain() { return null; } + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public Iterable> explodeWindows() { return Collections.emptyList(); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java index c1568058435b..e6738246b536 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java @@ -46,6 +46,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.PaneInfo.Timing; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; @@ -374,6 +375,48 @@ public void testDrainPropagated() throws Exception { assertThat( keyedWorkItem.timersIterable(), Matchers.contains(makeDrainingTimer(STATE_NAMESPACE_2, 3, TimeDomain.EVENT_TIME))); + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + + @Test + public void testValueKindPropagated() throws Exception { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + Windmill.WorkItem.Builder workItem = + Windmill.WorkItem.newBuilder().setKey(SERIALIZED_KEY).setWorkToken(17); + Windmill.InputMessageBundle.Builder chunk1 = workItem.addMessageBundlesBuilder(); + chunk1.setSourceComputationId("computation"); + addElementWithMetadata( + chunk1, + 5, + "hello", + WINDOW_1, + paneInfo(0), + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setValueKind(BeamFnApi.Elements.ValueKind.Enum.UPDATE_AFTER) + .build()); + addElementWithMetadata( + chunk1, + 7, + "world", + WINDOW_2, + paneInfo(2), + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setValueKind(BeamFnApi.Elements.ValueKind.Enum.DELETE) + .build()); + KeyedWorkItem keyedWorkItem = + new WindmillKeyedWorkItem<>( + KEY, + workItem.build(), + WINDOW_CODER, + WINDOWS_CODER, + VALUE_CODER, + windmillTagEncoding, + true); + + Iterator> iterator = keyedWorkItem.elementsIterable().iterator(); + Assert.assertEquals(ValueKind.UPDATE_AFTER, iterator.next().getValueKind()); + Assert.assertEquals(ValueKind.DELETE, iterator.next().getValueKind()); + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); } private static TimeDomain timerTypeToTimeDomain(Windmill.Timer.Type type) { diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java index b6dda82239ca..2111867d3851 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java @@ -35,6 +35,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowingStrategy; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; @@ -127,6 +128,11 @@ public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; } + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public @Nullable Long getRecordOffset() { return null; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java index ea73a5c35d7b..c08942405721 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java @@ -53,5 +53,7 @@ public interface OutputBuilder extends WindowedValue { OutputBuilder setOpenTelemetryContext(@Nullable Context openTelemetryContext); + OutputBuilder setValueKind(ValueKind valueKind); + void output(); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java index afd5aa845200..ef7b40aabe44 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java @@ -71,6 +71,8 @@ public T getValue() { public abstract @Nullable Context getOpenTelemetryContext(); + public abstract ValueKind getValueKind(); + // todo #33176 specify additional metadata in the future public static ValueInSingleWindow of( T value, @@ -81,6 +83,28 @@ public static ValueInSingleWindow of( @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, @Nullable Context openTelemetryContext) { + return of( + value, + timestamp, + window, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + ValueKind.INSERT); + } + + public static ValueInSingleWindow of( + T value, + Instant timestamp, + BoundedWindow window, + PaneInfo paneInfo, + @Nullable String currentRecordId, + @Nullable Long currentRecordOffset, + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { return new AutoValue_ValueInSingleWindow<>( value, timestamp, @@ -89,12 +113,22 @@ public static ValueInSingleWindow of( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } public static ValueInSingleWindow of( T value, Instant timestamp, BoundedWindow window, PaneInfo paneInfo) { - return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL, null); + return of( + value, + timestamp, + window, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } /** A coder for {@link ValueInSingleWindow}. */ @@ -144,9 +178,9 @@ public void encode( io.opentelemetry.context.Context openTelemetryContext = windowedElem.getOpenTelemetryContext(); if (openTelemetryContext != null) { - OpenTelemetryContextPropagator.set(openTelemetryContext, builder); } + builder.setValueKind(ValueKindUtil.toProto(windowedElem.getValueKind())); BeamFnApi.Elements.ElementMetadata metadata = builder.build(); ByteArrayCoder.of().encode(metadata.toByteArray(), outStream); } @@ -168,6 +202,7 @@ public ValueInSingleWindow decode( PaneInfo paneInfo = PaneInfo.PaneInfoCoder.INSTANCE.decode(inStream); CausedByDrain causedByDrain = CausedByDrain.NORMAL; io.opentelemetry.context.@Nullable Context openTelemetryContext = null; + ValueKind valueKind = ValueKind.INSERT; if (WindowedValues.WindowedValueCoder.isMetadataSupported() && paneInfo.isElementMetadata()) { BeamFnApi.Elements.ElementMetadata elementMetadata = BeamFnApi.Elements.ElementMetadata.parseFrom(ByteArrayCoder.of().decode(inStream)); @@ -176,12 +211,21 @@ public ValueInSingleWindow decode( ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; openTelemetryContext = OpenTelemetryContextPropagator.read(elementMetadata); + valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); } T value = valueCoder.decode(inStream, context); // todo #33176 specify additional metadata in the future return new AutoValue_ValueInSingleWindow<>( - value, timestamp, window, paneInfo, null, null, causedByDrain, openTelemetryContext); + value, + timestamp, + window, + paneInfo, + null, + null, + causedByDrain, + openTelemetryContext, + valueKind); } @Override diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java new file mode 100644 index 000000000000..7a190496ca2c --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.values; + +/** The type of change operation represented by a Change Data Capture (CDC) record. */ +public enum ValueKind { + /** Indicates a new record was created in the source system. */ + INSERT, + + /** + * Indicates the state of a record immediately before an update occurred. This is typically + * used to identify the previous values of modified columns or to locate the record via its + * primary key. + */ + UPDATE_BEFORE, + + /** + * Indicates the state of a record immediately after an update occurred. Represents the + * current, valid state of the record following the change. + */ + UPDATE_AFTER, + + /** Indicates that an existing record was removed from the source system. */ + DELETE +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java new file mode 100644 index 000000000000..46ceb8f6247a --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.values; + +import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; + +/** Utility class for converting between {@link ValueKind} and {@link Elements.ValueKind.Enum}. */ +public final class ValueKindUtil { + public static Elements.ValueKind.Enum toProto(ValueKind valueKind) { + switch (valueKind) { + case INSERT: + return Elements.ValueKind.Enum.INSERT; + case UPDATE_BEFORE: + return Elements.ValueKind.Enum.UPDATE_BEFORE; + case UPDATE_AFTER: + return Elements.ValueKind.Enum.UPDATE_AFTER; + case DELETE: + return Elements.ValueKind.Enum.DELETE; + default: + throw new IllegalArgumentException("Unknown ValueKind: " + valueKind); + } + } + + public static ValueKind fromProto(Elements.ValueKind.Enum proto) { + switch (proto) { + case VALUE_KIND_UNSPECIFIED: + case INSERT: + return ValueKind.INSERT; + case UPDATE_BEFORE: + return ValueKind.UPDATE_BEFORE; + case UPDATE_AFTER: + return ValueKind.UPDATE_AFTER; + case DELETE: + return ValueKind.DELETE; + default: + throw new IllegalArgumentException("Unknown ValueKind: " + proto); + } + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java index 3bf51b266304..13796018d7aa 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java @@ -58,6 +58,10 @@ public interface WindowedValue { CausedByDrain causedByDrain(); + /** Returns the {@link ValueKind} associated with this WindowedValue. */ + @Pure + ValueKind getValueKind(); + /** * A representation of each of the actual values represented by this compressed {@link * WindowedValue}, one per window. diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java index 58595875fd3e..33700a9dc0d2 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java @@ -83,7 +83,8 @@ public static Builder builder(WindowedValue template) { .setRecordOffset(template.getRecordOffset()) .setRecordId(template.getRecordId()) .setCausedByDrain(template.causedByDrain()) - .setOpenTelemetryContext(template.getOpenTelemetryContext()); + .setOpenTelemetryContext(template.getOpenTelemetryContext()) + .setValueKind(template.getValueKind()); } public static class Builder implements OutputBuilder { @@ -107,6 +108,7 @@ public static class Builder implements OutputBuilder { private @Nullable Long recordOffset; private CausedByDrain causedByDrain = CausedByDrain.NORMAL; private @Nullable Context openTelemetryContext; + private ValueKind valueKind = ValueKind.INSERT; @Override public Builder setValue(T value) { @@ -163,6 +165,13 @@ public Builder setOpenTelemetryContext(@Nullable Context openTelemetryContext return this; } + @Override + public Builder setValueKind(ValueKind valueKind) { + checkStateNotNull(valueKind, "ValueKind is null"); + this.valueKind = valueKind; + return this; + } + public Builder setReceiver(WindowedValueReceiver receiver) { this.receiver = receiver; return this; @@ -222,6 +231,12 @@ public CausedByDrain causedByDrain() { return causedByDrain; } + @Override + public ValueKind getValueKind() { + checkStateNotNull(valueKind, "ValueKind not set"); + return valueKind; + } + @Override public Collection> explodeWindows() { throw new UnsupportedOperationException( @@ -258,7 +273,8 @@ public WindowedValue build() { getRecordId(), getRecordOffset(), causedByDrain(), - getOpenTelemetryContext()); + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -269,6 +285,7 @@ public String toString() { .add("windows", getWindows()) .add("paneInfo", getPaneInfo()) .add("causedByDrain", causedByDrain()) + .add("valueKind", getValueKind()) .add("receiver", receiver) .toString(); } @@ -276,7 +293,16 @@ public String toString() { public static WindowedValue of( T value, Instant timestamp, Collection windows, PaneInfo paneInfo) { - return of(value, timestamp, windows, paneInfo, null, null, CausedByDrain.NORMAL, null); + return of( + value, + timestamp, + windows, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } /** Returns a {@code WindowedValue} with the given value, timestamp, and windows. */ @@ -288,10 +314,12 @@ public static WindowedValue of( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context openTelemetryContext) { + @Nullable Context openTelemetryContext, + ValueKind valueKind) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); checkArgument(windows.size() > 0, "WindowedValue requires windows, but there were none"); checkArgument(causedByDrain != null, "WindowedValue requires CausedByDrain, but it was null"); + checkArgument(valueKind != null, "WindowedValue requires ValueKind, but it was null"); if (windows.size() == 1) { return of( value, @@ -301,7 +329,8 @@ public static WindowedValue of( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } else { return new TimestampedValueInMultipleWindows<>( value, @@ -311,7 +340,8 @@ public static WindowedValue of( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } } @@ -323,7 +353,8 @@ static WindowedValue createWithoutValidation( Collection windows, PaneInfo paneInfo, CausedByDrain causedByDrain, - @Nullable Context openTelemetryContext) { + @Nullable Context openTelemetryContext, + ValueKind valueKind) { if (windows.size() == 1) { return of( value, @@ -333,10 +364,19 @@ static WindowedValue createWithoutValidation( null, null, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } else { return new TimestampedValueInMultipleWindows<>( - value, timestamp, windows, paneInfo, null, null, causedByDrain, openTelemetryContext); + value, + timestamp, + windows, + paneInfo, + null, + null, + causedByDrain, + openTelemetryContext, + valueKind); } } @@ -345,7 +385,16 @@ public static WindowedValue of( T value, Instant timestamp, BoundedWindow window, PaneInfo paneInfo) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); - return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL, null); + return of( + value, + timestamp, + window, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } /** Returns a {@code WindowedValue} with the given value, timestamp, and window. */ @@ -357,9 +406,11 @@ public static WindowedValue of( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context openTelemetryContext) { + @Nullable Context openTelemetryContext, + ValueKind valueKind) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); checkArgument(causedByDrain != null, "WindowedValue requires CausedByDrain, but it was null"); + checkArgument(valueKind != null, "WindowedValue requires ValueKind, but it was null"); boolean isGlobal = GlobalWindow.INSTANCE.equals(window); if (isGlobal && BoundedWindow.TIMESTAMP_MIN_VALUE.equals(timestamp)) { @@ -369,7 +420,8 @@ public static WindowedValue of( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } else if (isGlobal) { return new TimestampedValueInGlobalWindow<>( value, @@ -378,7 +430,8 @@ public static WindowedValue of( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } else { return new TimestampedValueInSingleWindow<>( value, @@ -388,7 +441,8 @@ public static WindowedValue of( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); } } @@ -398,7 +452,7 @@ public static WindowedValue of( */ public static WindowedValue valueInGlobalWindow(T value) { return new ValueInGlobalWindow<>( - value, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL, null); + value, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL, null, ValueKind.INSERT); } /** @@ -406,7 +460,8 @@ public static WindowedValue valueInGlobalWindow(T value) { * default timestamp and the specified pane. */ public static WindowedValue valueInGlobalWindow(T value, PaneInfo paneInfo) { - return new ValueInGlobalWindow<>(value, paneInfo, null, null, CausedByDrain.NORMAL, null); + return new ValueInGlobalWindow<>( + value, paneInfo, null, null, CausedByDrain.NORMAL, null, ValueKind.INSERT); } /** @@ -418,7 +473,14 @@ public static WindowedValue timestampedValueInGlobalWindow(T value, Insta return valueInGlobalWindow(value); } else { return new TimestampedValueInGlobalWindow<>( - value, timestamp, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL, null); + value, + timestamp, + PaneInfo.NO_FIRING, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } } @@ -432,7 +494,7 @@ public static WindowedValue timestampedValueInGlobalWindow( return timestampedValueInGlobalWindow(value, timestamp); } else { return new TimestampedValueInGlobalWindow<>( - value, timestamp, paneInfo, null, null, CausedByDrain.NORMAL, null); + value, timestamp, paneInfo, null, null, CausedByDrain.NORMAL, null, ValueKind.INSERT); } } @@ -450,7 +512,8 @@ public static WindowedValue withValue( windowedValue.getRecordId(), windowedValue.getRecordOffset(), windowedValue.causedByDrain(), - windowedValue.getOpenTelemetryContext()); + windowedValue.getOpenTelemetryContext(), + windowedValue.getValueKind()); } public static boolean equals( @@ -469,7 +532,8 @@ public static boolean equals( return left.getTimestamp().isEqual(right.getTimestamp()) && Objects.equals(left.getValue(), right.getValue()) && Iterables.elementsEqual(left.getWindows(), right.getWindows()) - && Objects.equals(left.getPaneInfo(), right.getPaneInfo()); + && Objects.equals(left.getPaneInfo(), right.getPaneInfo()) + && Objects.equals(left.getValueKind(), right.getValueKind()); } public static int hashCode(WindowedValue windowedValue) { @@ -478,7 +542,8 @@ public static int hashCode(WindowedValue windowedValue) { windowedValue.getValue(), windowedValue.getTimestamp().getMillis(), windowedValue.getWindows(), - windowedValue.getPaneInfo()); + windowedValue.getPaneInfo(), + windowedValue.getValueKind()); } private static final Collection GLOBAL_WINDOWS = @@ -503,6 +568,7 @@ private abstract static class SimpleWindowedValue implements WindowedValue private final @Nullable Long currentRecordOffset; private final CausedByDrain causedByDrain; private final @Nullable Context context; + private final ValueKind valueKind; @Override public @Nullable String getRecordId() { @@ -519,19 +585,26 @@ public CausedByDrain causedByDrain() { return causedByDrain; } + @Override + public ValueKind getValueKind() { + return valueKind; + } + protected SimpleWindowedValue( T value, PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context context) { + @Nullable Context context, + ValueKind valueKind) { this.value = value; this.paneInfo = checkNotNull(paneInfo); this.currentRecordId = currentRecordId; this.currentRecordOffset = currentRecordOffset; this.causedByDrain = causedByDrain; this.context = context; + this.valueKind = checkNotNull(valueKind, "ValueKind is null"); } @Override @@ -586,8 +659,9 @@ public MinTimestampWindowedValue( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context context) { - super(value, pane, currentRecordId, currentRecordOffset, causedByDrain, context); + @Nullable Context context, + ValueKind valueKind) { + super(value, pane, currentRecordId, currentRecordOffset, causedByDrain, context, valueKind); } @Override @@ -606,8 +680,10 @@ public ValueInGlobalWindow( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context context) { - super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context); + @Nullable Context context, + ValueKind valueKind) { + super( + value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context, valueKind); } @Override @@ -628,7 +704,8 @@ public WindowedValue withValue(NewT newValue) { getRecordId(), getRecordOffset(), causedByDrain(), - getOpenTelemetryContext()); + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -668,8 +745,10 @@ public TimestampedWindowedValue( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context context) { - super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context); + @Nullable Context context, + ValueKind valueKind) { + super( + value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context, valueKind); this.timestamp = checkNotNull(timestamp); } @@ -693,9 +772,17 @@ public TimestampedValueInGlobalWindow( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context context) { + @Nullable Context context, + ValueKind valueKind) { super( - value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context); + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + context, + valueKind); } @Override @@ -717,7 +804,8 @@ public WindowedValue withValue(NewT newValue) { getRecordId(), getRecordOffset(), causedByDrain(), - getOpenTelemetryContext()); + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -769,7 +857,8 @@ public TimestampedValueInSingleWindow( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context openTelemetryContext) { + @Nullable Context openTelemetryContext, + ValueKind valueKind) { super( value, timestamp, @@ -777,7 +866,8 @@ public TimestampedValueInSingleWindow( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); this.window = checkNotNull(window); } @@ -791,7 +881,8 @@ public WindowedValue withValue(NewT newValue) { getRecordId(), getRecordOffset(), causedByDrain(), - getOpenTelemetryContext()); + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -850,7 +941,8 @@ public TimestampedValueInMultipleWindows( @Nullable String currentRecordId, @Nullable Long currentRecordOffset, CausedByDrain causedByDrain, - @Nullable Context openTelemetryContext) { + @Nullable Context openTelemetryContext, + ValueKind valueKind) { super( value, timestamp, @@ -858,7 +950,8 @@ public TimestampedValueInMultipleWindows( currentRecordId, currentRecordOffset, causedByDrain, - openTelemetryContext); + openTelemetryContext, + valueKind); this.windows = checkNotNull(windows); } @@ -877,7 +970,8 @@ public WindowedValue withValue(NewT newValue) { getRecordId(), getRecordOffset(), causedByDrain(), - getOpenTelemetryContext()); + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -1047,6 +1141,7 @@ public void encode(WindowedValue windowedElem, OutputStream outStream, Coder. windowedElem.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN ? BeamFnApi.Elements.DrainMode.Enum.DRAINING : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .setValueKind(ValueKindUtil.toProto(windowedElem.getValueKind())) .build(); ByteArrayCoder.of().encode(em.toByteArray(), outStream); @@ -1067,6 +1162,7 @@ public WindowedValue decode(InputStream inStream, Coder.Context context) PaneInfo paneInfo = PaneInfoCoder.INSTANCE.decode(inStream); CausedByDrain causedByDrain = CausedByDrain.NORMAL; io.opentelemetry.context.Context openTelemetryContext = null; + ValueKind valueKind = ValueKind.INSERT; if (isMetadataSupported() && paneInfo.isElementMetadata()) { BeamFnApi.Elements.ElementMetadata elementMetadata = BeamFnApi.Elements.ElementMetadata.parseFrom(ByteArrayCoder.of().decode(inStream)); @@ -1075,13 +1171,14 @@ public WindowedValue decode(InputStream inStream, Coder.Context context) ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; openTelemetryContext = OpenTelemetryContextPropagator.read(elementMetadata); + valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); } T value = valueCoder.decode(inStream, context); // Because there are some remaining (incorrect) uses of WindowedValue with no windows, // we call this deprecated no-validation path when decoding return WindowedValues.createWithoutValidation( - value, timestamp, windows, paneInfo, causedByDrain, openTelemetryContext); + value, timestamp, windows, paneInfo, causedByDrain, openTelemetryContext, valueKind); } @Override @@ -1211,7 +1308,18 @@ public static ParamWindowedValueCoder of( Instant timestamp, Collection windows, PaneInfo paneInfo) { - return new ParamWindowedValueCoder<>(valueCoder, windowCoder, timestamp, windows, paneInfo); + return of(valueCoder, windowCoder, timestamp, windows, paneInfo, ValueKind.INSERT); + } + + public static ParamWindowedValueCoder of( + Coder valueCoder, + Coder windowCoder, + Instant timestamp, + Collection windows, + PaneInfo paneInfo, + ValueKind valueKind) { + return new ParamWindowedValueCoder<>( + valueCoder, windowCoder, timestamp, windows, paneInfo, valueKind); } /** @@ -1226,7 +1334,8 @@ public static ParamWindowedValueCoder of( windowCoder, BoundedWindow.TIMESTAMP_MIN_VALUE, GLOBAL_WINDOWS, - PaneInfo.NO_FIRING); + PaneInfo.NO_FIRING, + ValueKind.INSERT); } /** @@ -1244,15 +1353,31 @@ public static ParamWindowedValueCoder of(Coder valueCoder) { Coder windowCoder, Instant timestamp, Collection windows, - PaneInfo paneInfo) { + PaneInfo paneInfo, + ValueKind valueKind) { super(valueCoder, windowCoder); - this.windowedValuePrototype = WindowedValues.of(EMPTY_BYTES, timestamp, windows, paneInfo); + this.windowedValuePrototype = + WindowedValues.of( + EMPTY_BYTES, + timestamp, + windows, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + valueKind); } @Override public WindowedValueCoder withValueCoder(Coder valueCoder) { return new ParamWindowedValueCoder<>( - valueCoder, getWindowCoder(), getTimestamp(), getWindows(), getPaneInfo()); + valueCoder, + getWindowCoder(), + getTimestamp(), + getWindows(), + getPaneInfo(), + getValueKind()); } @Override @@ -1290,6 +1415,10 @@ public void registerByteSizeObserver(WindowedValue value, ElementByteSizeObse valueCoder.registerByteSizeObserver(value.getValue(), observer); } + public ValueKind getValueKind() { + return windowedValuePrototype.getValueKind(); + } + public Instant getTimestamp() { return windowedValuePrototype.getTimestamp(); } @@ -1309,7 +1438,15 @@ public static byte[] getPayload(ParamWindowedValueCoder from) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); WindowedValue windowedValue = WindowedValues.of( - EMPTY_BYTES, from.getTimestamp(), from.getWindows(), from.getPaneInfo()); + EMPTY_BYTES, + from.getTimestamp(), + from.getWindows(), + from.getPaneInfo(), + null, + null, + CausedByDrain.NORMAL, + null, + from.getValueKind()); WindowedValues.FullWindowedValueCoder windowedValueCoder = WindowedValues.FullWindowedValueCoder.of(ByteArrayCoder.of(), from.getWindowCoder()); try { @@ -1337,7 +1474,8 @@ public static WindowedValues.ParamWindowedValueCoder fromComponents( windowCoder, windowedValue.getTimestamp(), windowedValue.getWindows(), - windowedValue.getPaneInfo()); + windowedValue.getPaneInfo(), + windowedValue.getValueKind()); } catch (IOException e) { throw new RuntimeException( "Unable to decode constant members from payload for ParamWindowedValueCoder: ", e); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java index 830ad654b45c..3689b1be7dba 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java @@ -39,6 +39,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.PaneInfo.Timing; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -110,7 +111,8 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { null, null, CausedByDrain.CAUSED_BY_DRAIN, - context); // drain is persisted as part of metadata + context, + ValueKind.DELETE); // drain is persisted as part of metadata Coder> windowedValueCoder = WindowedValues.getFullCoder(StringUtf8Coder.of(), IntervalWindow.getCoder()); @@ -125,6 +127,7 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { Assert.assertArrayEquals(value.getWindows().toArray(), decodedValue.getWindows().toArray()); Assert.assertEquals(CausedByDrain.CAUSED_BY_DRAIN, value.causedByDrain()); Assert.assertNotNull(value.getOpenTelemetryContext()); + Assert.assertEquals(ValueKind.DELETE, value.getValueKind()); } @Test diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java index 68fdcdbf0a90..0fbc92c1f7d8 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java @@ -2365,7 +2365,8 @@ public void output(TupleTag tag, T output) { null, null, currentTimer.causedByDrain(), - null)); + null, + currentElement.getValueKind())); } @Override From f4f8496a983db39f4de03bdf348cb0b6817791f6 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Mon, 11 May 2026 12:42:53 -0400 Subject: [PATCH 130/490] Reduce number of layers for Beam container images (#38440) --- sdks/go/container/Dockerfile | 10 +++++++--- sdks/java/container/Dockerfile | 31 +++++++++++++++---------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/sdks/go/container/Dockerfile b/sdks/go/container/Dockerfile index b3b2fbbec3a9..3d70e2834b70 100644 --- a/sdks/go/container/Dockerfile +++ b/sdks/go/container/Dockerfile @@ -16,7 +16,12 @@ # limitations under the License. ############################################################################### -FROM gcr.io/distroless/base-nossl-debian12:latest +FROM gcr.io/distroless/base-nossl-debian12:latest AS base_image + +# Distroless base image has many layers. Squash into single layer +FROM scratch +COPY --from=base_image / / + LABEL Author "Apache Beam " ARG TARGETOS @@ -24,8 +29,7 @@ ARG TARGETARCH ADD target/${TARGETOS}_${TARGETARCH}/boot /opt/apache/beam/ -COPY target/LICENSE /opt/apache/beam/ -COPY target/NOTICE /opt/apache/beam/ +COPY target/LICENSE target/NOTICE /opt/apache/beam/ # Add Go licenses. COPY target/go-licenses/* /opt/apache/beam/third_party_licenses/golang/ diff --git a/sdks/java/container/Dockerfile b/sdks/java/container/Dockerfile index c43eb0cb8c02..7a8c9e7e7d7e 100644 --- a/sdks/java/container/Dockerfile +++ b/sdks/java/container/Dockerfile @@ -24,22 +24,21 @@ ARG TARGETARCH ARG pull_licenses -ADD target/slf4j-api.jar /opt/apache/beam/jars/ -ADD target/slf4j-jdk14.jar /opt/apache/beam/jars/ -ADD target/jcl-over-slf4j.jar /opt/apache/beam/jars/ -ADD target/log4j-over-slf4j.jar /opt/apache/beam/jars/ -ADD target/log4j-to-slf4j.jar /opt/apache/beam/jars/ -ADD target/beam-sdks-java-harness.jar /opt/apache/beam/jars/ - -# Required to use jamm as a javaagent to get accurate object size measuring -# COPY fails if file is not found, so use a wildcard for open-module-agent.jar -# since it is only included in Java 9+ containers -COPY target/jamm.jar target/open-module-agent.jar /opt/apache/beam/jars/ - -COPY target/${TARGETOS}_${TARGETARCH}/boot /opt/apache/beam/ - -COPY target/LICENSE /opt/apache/beam/ -COPY target/NOTICE /opt/apache/beam/ +# Dependency jars +COPY target/slf4j-api.jar \ + target/slf4j-jdk14.jar \ + target/jcl-over-slf4j.jar \ + target/log4j-over-slf4j.jar \ + target/log4j-to-slf4j.jar \ + # Required to use jamm as a javaagent to get accurate object size measuring + target/jamm.jar \ + /opt/apache/beam/jars/ + +# Built jars +COPY target/open-module-agent.jar target/beam-sdks-java-harness.jar /opt/apache/beam/jars/ + +# Built binary with licenses +COPY target/${TARGETOS}_${TARGETARCH}/boot target/LICENSE target/NOTICE /opt/apache/beam/ # copy third party licenses ADD target/third_party_licenses /opt/apache/beam/third_party_licenses/ From af73c20a3e1fb3eda89cf8d9dd5ad0c13b16ed7e Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 11 May 2026 13:16:25 -0400 Subject: [PATCH 131/490] Fix deadlock in AsyncWrapper reset_state() (#38427) * Add a test to reproduce hanging. * Fix deadlock between shutdown in main thread and done callback in worker threads. * Address review comments. * Fix format * Modify the test to cover reset_state() hanging in asyncio mode. * Fix the deadlock when asyncio is used. * Fix formatting. * Increase timeout to reduce false-positives. * Revise test function names and some comments. --- .../apache_beam/transforms/async_dofn.py | 29 ++++++++++++--- .../apache_beam/transforms/async_dofn_test.py | 35 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/sdks/python/apache_beam/transforms/async_dofn.py b/sdks/python/apache_beam/transforms/async_dofn.py index 28568bd893c5..ad3d5bc66469 100644 --- a/sdks/python/apache_beam/transforms/async_dofn.py +++ b/sdks/python/apache_beam/transforms/async_dofn.py @@ -153,21 +153,39 @@ def _run_event_loop(): @staticmethod def reset_state(): + event_loop_thread_to_join = None with AsyncWrapper._lock: if AsyncWrapper._event_loop: AsyncWrapper._event_loop.call_soon_threadsafe( AsyncWrapper._event_loop.stop) if AsyncWrapper._event_loop_thread: - AsyncWrapper._event_loop_thread.join() + event_loop_thread_to_join = AsyncWrapper._event_loop_thread AsyncWrapper._event_loop = None AsyncWrapper._event_loop_thread = None if AsyncWrapper._loop_started is not None: AsyncWrapper._loop_started.clear() - for pool in AsyncWrapper._pool.values(): - pool.acquire(AsyncWrapper.initialize_pool(1)).shutdown( - wait=True, cancel_futures=True) + pools = list(AsyncWrapper._pool.values()) + + # We must join the asyncio event loop thread outside of the lock block. + # If joined inside the lock, the waiting thread holds the lock while blocking, + # preventing active coroutines' done callbacks from acquiring the lock on the + # event loop thread, resulting in a deadlock. + if event_loop_thread_to_join: + event_loop_thread_to_join.join() + + # We must acquire and shut down the thread pools outside of the lock block. + # If shutdown(wait=True) is called inside the lock, the caller blocks holding + # the lock, preventing active worker threads from acquiring the lock to run + # their done callbacks, resulting in a deadlock. + pools_to_shutdown = [ + pool.acquire(AsyncWrapper.initialize_pool(1)) for pool in pools + ] + + for pool in pools_to_shutdown: + pool.shutdown(wait=True, cancel_futures=True) + with AsyncWrapper._lock: AsyncWrapper._pool = {} AsyncWrapper._processing_elements = {} @@ -268,7 +286,8 @@ async def _collect(result): def decrement_items_in_buffer(self, future): with AsyncWrapper._lock: - AsyncWrapper._items_in_buffer[self._uuid] -= 1 + if self._uuid in AsyncWrapper._items_in_buffer: + AsyncWrapper._items_in_buffer[self._uuid] -= 1 def schedule_if_room(self, element, ignore_buffer=False, *args, **kwargs): """Schedules an item to be processed asynchronously if there is room. diff --git a/sdks/python/apache_beam/transforms/async_dofn_test.py b/sdks/python/apache_beam/transforms/async_dofn_test.py index 81c7b8e163ff..39901d791fb9 100644 --- a/sdks/python/apache_beam/transforms/async_dofn_test.py +++ b/sdks/python/apache_beam/transforms/async_dofn_test.py @@ -16,6 +16,7 @@ # import logging +import multiprocessing import random import time import unittest @@ -487,6 +488,40 @@ def add_item(i): self.check_output(results[i], expected_outputs['key' + str(i)]) self.assertEqual(bag_states['key' + str(i)].items, []) + @staticmethod + def _run_reset_state_concurrent_teardown(use_asyncio): + dofn = BasicDofn(sleep_time=0.5) + async_dofn = async_lib.AsyncWrapper(dofn, use_asyncio=use_asyncio) + async_dofn.setup() + fake_bag_state = FakeBagState([]) + fake_timer = FakeTimer(0) + + # Start processing an item. This starts a worker thread/coroutine sleeping for 0.5s. + async_dofn.process(('key1', 1), to_process=fake_bag_state, timer=fake_timer) + time.sleep(0.05) + + # Verify that calling reset_state() while background tasks are actively running + # completes cleanly without causing lock-ordering deadlocks. + async_lib.AsyncWrapper.reset_state() + + def test_reset_state_concurrent_teardown(self): + # Verify concurrent teardown safety in a separate process to prevent any potential + # regressions from freezing the main pytest process at exit. + p = multiprocessing.Process( + target=AsyncTest._run_reset_state_concurrent_teardown, + args=(self.use_asyncio, )) + p.start() + p.join(timeout=10.0) + + if p.is_alive(): + p.terminate() + p.join() + self.fail( + "reset_state() deadlocked/hung waiting for active threads/tasks to finish" + ) + else: + self.assertEqual(p.exitcode, 0) + if __name__ == '__main__': unittest.main() From 3b0a5c9f18518328b618dd3d09c9fd7674b386c3 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Mon, 11 May 2026 13:51:37 -0400 Subject: [PATCH 132/490] Make Beartype use the default behavior in is_consistent_with() (#38275) * Make Beartype use the default behavior in is_consistent_with() * CHANGES.md callout * review comment * Update CHANGES.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * linting --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- CHANGES.md | 1 + .../apache_beam/options/pipeline_options.py | 5 ++++ .../python/apache_beam/typehints/typehints.py | 12 ++++++++- .../apache_beam/typehints/typehints_test.py | 27 +++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0db7fddba4f1..6deb653ffefb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -83,6 +83,7 @@ ## Breaking Changes +* (Python) Made Beartype the default fallback type checking tool. This can be disabled with the `--disable_beartype` pipeline option. ([#38275](https://github.com/apache/beam/issues/38275)) * X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). ## Deprecations diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index 265313cd013d..2533083f7e7e 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -883,6 +883,11 @@ def _add_argparse_args(cls, parser): action='store_false', help='Disable type checking at pipeline construction ' 'time') + parser.add_argument( + '--disable_beartype', + default=False, + action='store_true', + help='Disable the use of beartype for type checking.') parser.add_argument( '--runtime_type_check', default=False, diff --git a/sdks/python/apache_beam/typehints/typehints.py b/sdks/python/apache_beam/typehints/typehints.py index eec9ea86bd4c..6dc88a93dd39 100644 --- a/sdks/python/apache_beam/typehints/typehints.py +++ b/sdks/python/apache_beam/typehints/typehints.py @@ -1486,7 +1486,8 @@ def normalize(x, none_as_type=False): }) -def is_consistent_with(sub, base, use_beartype: bool = False) -> bool: +def is_consistent_with( + sub, base, use_beartype: typing.Optional[bool] = None) -> bool: """Checks whether sub a is consistent with base. This is according to the terminology of PEP 483/484. This relationship is @@ -1495,6 +1496,15 @@ def is_consistent_with(sub, base, use_beartype: bool = False) -> bool: relation, but also handles the special Any type as well as type parameterization. """ + if use_beartype is None: + from apache_beam.options.pipeline_options_context import get_pipeline_options + options = get_pipeline_options() + if options: + from apache_beam.options.pipeline_options import TypeOptions + use_beartype = not options.view_as(TypeOptions).disable_beartype + else: + use_beartype = True + from apache_beam.pvalue import Row from apache_beam.typehints.row_type import RowTypeConstraint if sub == base: diff --git a/sdks/python/apache_beam/typehints/typehints_test.py b/sdks/python/apache_beam/typehints/typehints_test.py index a335ab05f1b7..b097a01fed42 100644 --- a/sdks/python/apache_beam/typehints/typehints_test.py +++ b/sdks/python/apache_beam/typehints/typehints_test.py @@ -1614,6 +1614,33 @@ def test_hint_helper_pipe_union(self): self.assertTrue(is_consistent_with(int, pipe_union_2)) self.assertTrue(is_consistent_with(float, pipe_union_2)) + def test_is_consistent_with_disable_beartype(self): + import unittest.mock + + from apache_beam.options.pipeline_options import PipelineOptions + from apache_beam.options.pipeline_options_context import scoped_pipeline_options + + with unittest.mock.patch( + 'apache_beam.typehints.typehints.is_subhint') as mock_is_subhint: + mock_is_subhint.return_value = True + + class A: + pass + + class B(A): + pass + + options = PipelineOptions([]) + with scoped_pipeline_options(options): + typehints.is_consistent_with(B, A) + self.assertTrue(mock_is_subhint.called) + mock_is_subhint.reset_mock() + + options = PipelineOptions(['--disable_beartype']) + with scoped_pipeline_options(options): + typehints.is_consistent_with(B, A) + self.assertFalse(mock_is_subhint.called) + def test_positional_arg_hints(self): self.assertEqual(typehints.Any, _positional_arg_hints('x', {})) self.assertEqual(int, _positional_arg_hints('x', {'x': int})) From b14dbb822ebf15df5d91bf87eb95760826376ff8 Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Mon, 11 May 2026 20:50:17 +0200 Subject: [PATCH 133/490] [runners-spark] Add Spark 4 runner (#38255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: add Spark 4.0.2 version property and Scala 2.13 support Add spark4_version (4.0.2) to BeamModulePlugin alongside the existing spark3_version. Update spark_runner.gradle to conditionally select the correct Scala library (2.13 vs 2.12), Jackson module, Kafka test dependency, and require Java 17 when building against Spark 4. Register the new :runners:spark:4 module in settings.gradle.kts. These changes are purely additive — all conditionals gate on spark_version.startsWith("4") or spark_scala_version == '2.13', leaving the Spark 3 build path untouched. Co-Authored-By: Claude Sonnet 4.6 * refactor: make shared Spark source compatible with Scala 2.12 and 2.13 Co-Authored-By: Claude Opus 4.6 * build: add runners/spark/4/ build configuration Add the Gradle build file for the Spark 4 structured streaming runner. The module mirrors runners/spark/3/ — it inherits the shared RDD-base source from runners/spark/src/ via copySourceBase and adds its own Structured Streaming implementation in src/main/java. Key differences from the Spark 3 build: - Uses spark4_version (4.0.2) with Scala 2.13. - Excludes DStream-based streaming tests (Spark 4 supports only structured streaming batch). - Unconditionally adds --add-opens JVM flags required by Kryo on Java 17 (Spark 4's minimum). - Binds Spark driver to 127.0.0.1 for macOS compatibility. Co-Authored-By: Claude Sonnet 4.6 * feat: add Spark 4 structured streaming runner source Add the Spark 4 structured streaming runner implementation and tests. Most files are adapted from the Spark 3 structured streaming runner with targeted changes for Spark 4 / Scala 2.13 API compatibility. Key Spark 4-specific changes (diff against runners/spark/3/src/): EncoderFactory — Replaced the direct ExpressionEncoder constructor (removed in Spark 4) with BeamAgnosticEncoder, a named class implementing both AgnosticExpressionPathEncoder (for expression delegation via toCatalyst/fromCatalyst) and AgnosticEncoders .StructEncoder (so Dataset.select(TypedColumn) creates an N-attribute plan, preventing FIELD_NUMBER_MISMATCH). The toCatalyst/fromCatalyst methods substitute the provided input expression via transformUp, enabling correct nesting inside composite encoders like Encoders.tuple(). EncoderHelpers — Added toExpressionEncoder() helper to handle Spark 4 built-in encoders that are AgnosticEncoder subclasses rather than ExpressionEncoder. GroupByKeyTranslatorBatch — Migrated from internal catalyst Expression API (CreateNamedStruct, Literal$) to public Column API (struct(), lit(), array()), as required by Spark 4. BoundedDatasetFactory — Use classic.Dataset$.MODULE$.ofRows() as Dataset moved to org.apache.spark.sql.classic in Spark 4. ScalaInterop — Replace WrappedArray.ofRef (removed in Scala 2.13) with JavaConverters.asScalaBuffer().toList() in seqOf(). GroupByKeyHelpers, CombinePerKeyTranslatorBatch — Replace TraversableOnce with IterableOnce (Scala 2.13 rename). SparkStructuredStreamingPipelineResult — Replace sparkproject.guava with Beam's vendored Guava. Co-Authored-By: Claude Sonnet 4.6 * ci: add Spark 4 PreCommit and PostCommit workflows Add GitHub Actions workflows for the Spark 4 runner module: - beam_PreCommit_Java_Spark4_Versions: runs sparkVersionsTest on changes to runners/spark/**. Currently a no-op (the sparkVersions map is empty) but scaffolds future patch version coverage. - beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming: runs the structured streaming test suite on Java 17. Co-Authored-By: Claude Sonnet 4.6 * Add PreCommit Java Spark4 Versions workflow * Add cancellation support to Spark pipeline execution * Remove unused endOfData() call in close method Remove endOfData() call in close method. * build: add Spark 4 job-server and container modules Add job-server and container build configurations for Spark 4, mirroring the existing Spark 3 job-server setup. The container uses eclipse-temurin:17 (Spark 4 requires Java 17). The shared spark_job_server.gradle gains a requireJavaVersion conditional for Spark 4 parent projects. Co-Authored-By: Claude Opus 4.6 * build: remove spark.driver.host workaround from Spark 4 build The hostname binding hack is no longer needed now that the local machine resolves its hostname to 127.0.0.1 via /etc/hosts. Co-Authored-By: Claude Opus 4.6 * docs: add Spark 4 runner entry to CHANGES.md Called out in /ultrareview as a missing contributor checklist item. Adds a Highlight line and a New Features / Improvements entry under the 2.74.0 Unreleased section, referencing issue #36841. * docs: explain classic.SparkSession downcast in BoundedDatasetFactory Per /ultrareview feedback: the one-line comment didn't make clear why the cast is safe. Expand it to note that SparkSession.builder() always returns a classic.SparkSession at runtime, which is why the downcast avoids reflection. * fix: log warning when neither WrappedArray nor ArraySeq class is found Per /ultrareview feedback: the fallback branch silently swallowed the second ClassNotFoundException. In practice one of the two classes is always present (Scala 2.12 vs 2.13 stdlib), but a silent skip could mask a broken classpath. Emit a LOG.warn instead. * build: compare spark_version numerically via isSparkAtLeast helper Per /ultrareview feedback: the five `"$spark_version" >= "3.5.0"` checks were lexicographic string comparisons. They happened to work for 3.5.0 and 4.0.2 only because '4' > '3' as chars — a future "3.10.0" release would compare less than "3.5.0" and silently drop the Spark 3.5+ dependencies and exclusions. Introduce an `isSparkAtLeast` closure that tokenizes on `.` and `-`, keeps numeric parts, and compares component-by-component. Replace all five call sites. * [Spark Runner] Slim Spark 4 to override-only files With spark_runner.gradle now layering per-major source overrides on top of the shared base, runners/spark/4/src/ no longer needs to duplicate 62 byte-identical structured-streaming files. Keep only the 11 files that actually differ for Spark 4 / Scala 2.13. Switch the build.gradle to spark_major = '4' (the new mechanism) and bump spark_versions to 3,4. Compiled output unchanged — the deleted files are reproduced identically inside build/source-overrides by the Copy task. * [Spark Runner] Use java.io.Serializable in DoFnRunnerFactory base scala.Serializable was removed in Scala 2.13. java.io.Serializable works identically on both Scala 2.12 and 2.13, so this can live in the shared base instead of needing a Spark-4-only override file. * [Spark Runner] Null-guard error message logging in EvaluationContext base Wrap Throwables.getRootCause(e).getMessage() in String.valueOf(...) to make the error logging robust to a null root-cause message. The behaviour change applies equally to Spark 3 and Spark 4, so the fix lives in the shared base and the Spark-4 override is dropped. * [Spark Runner] Cancel execution future and use Beam-vendored Guava in PipelineResult Two changes that previously lived only in the Spark-4 override and are equally valid for Spark 3: 1. cancel() now actually cancels the executing future (pipelineExecution.cancel(true)) in addition to setting the state to CANCELLED. Without this, calling cancel() left the pipeline running silently — a real bug, not a Spark-4 specific concern. 2. Switch from Spark's shaded guava (org.sparkproject.guava) to the Beam-vendored guava that is already on the classpath. Spark 4 no longer exposes the sparkproject guava package; using the vendored one removes the version coupling for both runners. * ci: re-trigger to clear flaky UnboundedScheduledExecutorServiceTest Empty commit to re-run CI. The only failure on the prior head was UnboundedScheduledExecutorServiceTest.testThreadsAreAddedOnlyAsNeededWithContention, a known flake (apache/beam#31590) — the test itself acknowledges contention-induced extra threads in its inline comment. Squash or drop on rebase before merge. * [Spark Runner] Fix maxTimestamp to handle multi-window values Iterables.getOnlyElement(windows) crashes with IllegalArgumentException when a WindowedValue is associated with more than one window (e.g. after a sliding window assignment). Compute the max maxTimestamp() across all associated windows instead, falling back to a clear error if the iterable is unexpectedly empty. Applied identically to the shared base and the Spark 4 override. Flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Drop unchecked cast in BoundedDatasetFactory.split source.split returns List>, which already satisfies the subsequent stream usage. The cast was unchecked and would trip heap-pollution warnings. Applied identically to the shared base and the Spark 4 override. Flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Drop redundant Iterator cast in Spark 4 GroupByKeyTranslatorBatch The (Iterator) cast inside fun2 is redundant: fun2's signature infers the iterator type. The shared base translator at the analogous call site already calls iterableOnce(it) without a cast. Flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Spark 4 EncoderFactory: stable constructor lookup + document trait setter Replace getConstructors()[0] (JVM-defined ordering, not stable) with a helper that picks the widest public constructor. The downstream switch already dispatches on parameter count to pick the right argument shape per Spark version, so this just makes the choice deterministic. Also document the org$apache$spark...$_setter_$isStruct_$eq method — it is the synthetic setter the Scala compiler emits for trait val fields, required when implementing AgnosticEncoders.StructEncoder from Java. Both flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Fix Javadoc/comment typos flagged by Gemini Three trivial typos flagged on PR #38255 round 2 review, applied identically to the shared base and the Spark 4 override: - CombinePerKeyTranslatorBatch: "other there other missing features?" -> "are there other missing features?" - GroupByKeyTranslatorBatch: "build-in" -> "built-in" - EncoderHelpers: PRIMITIV_TYPES -> PRIMITIVE_TYPES (constant + caller) * [Spark Runner] Switch EncoderFactory.invoke on the right constructor In EncoderFactory.invoke(Expression obj, ...), the switch was keyed on STATIC_INVOKE_CONSTRUCTOR.getParameterCount() but the body actually calls INVOKE_CONSTRUCTOR. This worked by coincidence: across the supported Spark 3.x versions both constructors happen to share the same parameter counts at the same dispatch points. A future Spark release where the two diverge would silently pick the wrong branch. Switch on INVOKE_CONSTRUCTOR.getParameterCount() to match the constructor that is actually invoked, and align with the convention used by newInstance() further down. In the Spark 4 override this also lets us collapse the `case 8: case 9:` fallthrough back to a single `case 8:`, since INVOKE_CONSTRUCTOR remains 8 params in Spark 4 even though STATIC_INVOKE_CONSTRUCTOR grew to 9. Applied identically to the shared base and the Spark 4 override. Flagged by Gemini Code Assist on PR #38255. * Update CHANGES.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [Spark 4] Drop redundant Collection cast in GroupByKeyHelpers WindowedValue#getWindows() returns Collection, which is already an Iterable and can be passed straight to ScalaInterop.scalaIterator(...). The intermediate local variable and the unchecked cast to Collection were redundant. Applied in both the shared base and the Spark 4 override. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [Spark 4] Add module README with slf4j-jdk14 known-issue note Documents the Spark 4 runner's requirements (Java 17, Scala 2.13, Spark 4.0.x, batch-only) and the slf4j-jdk14 ↔ jul-to-slf4j conflict that is the Spark 4 manifestation of #26985 (fixed for Spark 3 in #27001). The shared spark_runner.gradle already excludes slf4j-jdk14 for in-tree builds; this note tells downstream consumers to mirror the exclude when assembling their own runtime classpath against beam-runners-spark-4. * [runners-spark] Address Gemini nits: use encoder terminology in exception messages * Trigger Build * ci: re-trigger to clear flaky FlinkRequiresStableInputTest The PreCommit Java failure on the previous run was a single timeout in FlinkRequiresStableInputTest.testParDoRequiresStableInputPortable (:runners:flink:1.17:test) — known flake tracked in #21333. This PR does not touch any Flink code. Squash or drop on rebase before merge. * ci: re-trigger to clear flaky SqsIOWriteBatchesTest.testWriteBatchesToDynamicWithStrictTimeout Wall-clock-timing test (100ms inter-message + 150ms strict batch timeout) in sdks/java/io/amazon-web-services2 SQS — unrelated to this PR (no AWS2/SQS/Direct-runner files touched), and master is green for the same PreCommit on 6106b308. * ci: re-trigger to clear Maven Central 403 on Windows wordcount `Java Wordcount Direct Runner (windows-latest)` failed at the :buildSrc configure step with HTTP 403 fetching legacy Spotless 5.6.1 transitive deps from repo.maven.apache.org (spotless-lib:2.7.0, durian-*:1.2.0, jgit:5.8.0). Network/infra flake — PR doesn't touch examples or buildSrc, master 'Java Tests' workflow consistently green. * ci: re-trigger to clear flaky Spotless + GCP IO Direct PreCommits Both checks failed on the prior empty retry commit (e19b80c4). Reproduced locally at e19b80c4: spotlessCheck and Spark checkStyleMain/Test all pass. PR doesn't touch any GCP IO code, and both checks were green on the immediately preceding branch commits (5abbb21d, 604037f5) and on master (6106b308, e01f7114). Treating as infra flakes; squash before merge. * [runners-spark] Cover Scala-array fallback and EvaluationContext error paths Address codecov/patch on PR #38255 by exercising the new branches added for Scala 2.13 / null-safe error logging: - Refactor SparkRunnerKryoRegistrator's nested Scala-array Class.forName fallback into a small @VisibleForTesting findFirstAvailableClass helper and add unit tests for first-hit, fallback, no-match, and empty-input paths. - Add EvaluationContextTest covering the catch (RuntimeException) / catch (Exception) blocks in evaluate() and collect(), including the null-message path that motivated the String.valueOf wrap. * [runners-spark] spotless: inline two findFirstAvailableClass calls in test * flaky SqsIOWriteBatchesTest retry * flaky ExampleEchoPipelineTest retry * rebase cleanup: drop duplicate isSparkAtLeast helper now in master via #38324 * Address @Abacn 2026-05-07 review - SparkRunnerKryoRegistrator: throw IllegalStateException instead of LOG.warn when neither ArraySeq$ofRef (Scala 2.13) nor WrappedArray$ofRef (Scala 2.12) is on the classpath, so the missing class isn't silently ignored. Drops the now-unused Logger field and slf4j imports. - spark_runner.gradle: declare org.apache.spark:spark-connect-shims_2.13 as a provided dep gated on isSparkAtLeast("4.0.0"). Spark 4 splits the Connect shim classes out of spark-sql; with enableStrictDependencies this surfaced as analyzeClassesDependencies usedUndeclaredArtifacts. The artifact does not exist for Spark 3, so the gate prevents Spark 3 resolution failures. - runners/spark/4/build.gradle: drop the empty sparkVersions test scaffolding (no additional Spark 4.x patch versions to test against yet) and delete the now-unused .github/workflows/beam_PreCommit_Java_Spark4_Versions.yml workflow + its README.md row. - EncoderFactory (shared base): revert the line 94 switch to STATIC_INVOKE_CONSTRUCTOR.getParameterCount(), keeping Spark 3 behavior byte-for-byte unchanged. Spark 4's complete EncoderFactory override under runners/spark/4/src/.../EncoderFactory.java is unaffected. - CHANGES.md: drop the Highlights line for Spark 4. Will re-add when ValidatesRunner tests are set up and confirmed working, matching the Phase 1 #38324 pattern. - runners/spark/4/job-server/container: delete the entire module (build.gradle + Dockerfile) and remove its include() from settings.gradle.kts. Per @Abacn's offer to defer the container module to portable runner support later. The fat-jar :runners:spark:4:job-server module is kept. --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- ...datesRunner_Spark4StructuredStreaming.json | 3 + .../beam_PreCommit_Java_Spark4_Versions.json | 3 + .github/workflows/README.md | 1 + ...idatesRunner_Spark4StructuredStreaming.yml | 97 +++ CHANGES.md | 1 - gradle.properties | 2 +- runners/spark/4/README.md | 73 +++ runners/spark/4/build.gradle | 53 ++ runners/spark/4/job-server/build.gradle | 31 + .../io/BoundedDatasetFactory.java | 332 ++++++++++ .../batch/CombinePerKeyTranslatorBatch.java | 162 +++++ .../translation/batch/GroupByKeyHelpers.java | 105 +++ .../batch/GroupByKeyTranslatorBatch.java | 298 +++++++++ .../translation/helpers/EncoderFactory.java | 333 ++++++++++ .../translation/helpers/EncoderHelpers.java | 617 ++++++++++++++++++ .../translation/utils/ScalaInterop.java | 114 ++++ .../helpers/EncoderHelpersTest.java | 298 +++++++++ runners/spark/spark_runner.gradle | 23 +- .../coders/SparkRunnerKryoRegistrator.java | 28 +- .../SparkGroupAlsoByWindowViaWindowSet.java | 4 +- ...parkStructuredStreamingPipelineResult.java | 1 + .../io/BoundedDatasetFactory.java | 2 +- .../translation/EvaluationContext.java | 10 +- .../batch/CombinePerKeyTranslatorBatch.java | 2 +- .../translation/batch/GroupByKeyHelpers.java | 4 +- .../batch/GroupByKeyTranslatorBatch.java | 2 +- .../translation/helpers/EncoderHelpers.java | 21 +- .../SparkRunnerKryoRegistratorTest.java | 49 ++ .../translation/EvaluationContextTest.java | 84 +++ settings.gradle.kts | 2 + 30 files changed, 2730 insertions(+), 25 deletions(-) create mode 100644 .github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json create mode 100644 .github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json create mode 100644 .github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml create mode 100644 runners/spark/4/README.md create mode 100644 runners/spark/4/build.gradle create mode 100644 runners/spark/4/job-server/build.gradle create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java create mode 100644 runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/utils/ScalaInterop.java create mode 100644 runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpersTest.java create mode 100644 runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json new file mode 100644 index 000000000000..c4edaa85a89d --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json @@ -0,0 +1,3 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run" +} diff --git a/.github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json b/.github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json new file mode 100644 index 000000000000..c4edaa85a89d --- /dev/null +++ b/.github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json @@ -0,0 +1,3 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run" +} diff --git a/.github/workflows/README.md b/.github/workflows/README.md index a6539d18f360..c3c73c0317a9 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -370,6 +370,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner SparkStructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml?query=event%3Aschedule) | +| [ PostCommit Java ValidatesRunner Spark4StructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Twister2 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Twister2.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner ULR ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_ULR.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml?query=event%3Aschedule) | | [ PostCommit Java ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml) | N/A |`beam_PostCommit_Java.json`| [![.github/workflows/beam_PostCommit_Java.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml new file mode 100644 index 000000000000..b595afe6f42c --- /dev/null +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: PostCommit Java ValidatesRunner Spark4 StructuredStreaming + +on: + schedule: + - cron: '45 4/6 * * *' + pull_request_target: + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json'] + workflow_dispatch: + +#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +permissions: + actions: write + pull-requests: write + checks: write + contents: read + deployments: read + id-token: none + issues: write + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + +jobs: + beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming: + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 120 + strategy: + matrix: + job_name: [beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming] + job_phrase: [Run Spark4 StructuredStreaming ValidatesRunner] + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event.comment.body == 'Run Spark4 StructuredStreaming ValidatesRunner' + steps: + - uses: actions/checkout@v4 + - name: Setup repository + uses: ./.github/actions/setup-action + with: + comment_phrase: ${{ matrix.job_phrase }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + java-version: '17' + - name: run validatesStructuredStreamingRunnerBatch script + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :runners:spark:4:validatesStructuredStreamingRunnerBatch + arguments: | + -PtestJavaVersion=17 \ + -PdisableSpotlessCheck=true \ + - name: Archive JUnit Test Results + uses: actions/upload-artifact@v4 + if: ${{ !success() }} + with: + name: JUnit Test Results + path: "**/build/reports/tests/" + - name: Publish JUnit Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + commit: '${{ env.prsha || env.GITHUB_SHA }}' + comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} + files: '**/build/test-results/**/*.xml' + large_files: true diff --git a/CHANGES.md b/CHANGES.md index 6deb653ffefb..2cf7f983fa35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -60,7 +60,6 @@ ## Highlights * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). -* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). ## I/Os diff --git a/gradle.properties b/gradle.properties index 583eb24d8964..fc2813f11fec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -41,6 +41,6 @@ docker_image_default_repo_prefix=beam_ # supported flink versions flink_versions=1.17,1.18,1.19,1.20,2.0 # supported spark versions -spark_versions=3 +spark_versions=3,4 # supported python versions python_versions=3.10,3.11,3.12,3.13,3.14 diff --git a/runners/spark/4/README.md b/runners/spark/4/README.md new file mode 100644 index 000000000000..371a44512657 --- /dev/null +++ b/runners/spark/4/README.md @@ -0,0 +1,73 @@ + +# Apache Beam Spark 4 Runner + +Experimental Beam runner for Apache Spark 4 (batch-only). Built on the shared +`runners/spark` source base via `spark_runner.gradle`'s per-version +source-overrides mechanism: this module contributes the small set of files +under `src/main/java/.../structuredstreaming/` that diverge from the Spark 3 +implementation. See the parent `runners/spark/` module for the bulk of the +runner code. + +## Requirements + +* **Spark 4.0.2** (and other Spark 4.0.x patch releases) +* **Scala 2.13** +* **Java 17** — Spark 4 does not run on earlier JDKs + +## Status + +Batch only. Streaming is tracked in +[#36841](https://github.com/apache/beam/issues/36841). + +## Known issues + +### `StackOverflowError` from `slf4j-jdk14` on the runtime classpath + +Spark 4 ships `org.slf4j:jul-to-slf4j` to route `java.util.logging` records +into SLF4J. If `org.slf4j:slf4j-jdk14` is also resolved at runtime — it routes +the other direction (SLF4J → JUL) — the first log line creates an infinite +loop: + +``` +java.lang.StackOverflowError + at org.slf4j.bridge.SLF4JBridgeHandler.publish(...) + at java.util.logging.Logger.log(...) + at org.slf4j.impl.JDK14LoggerAdapter.log(...) + at org.slf4j.bridge.SLF4JBridgeHandler.publish(...) + ... +``` + +This is the same condition that broke the Spark 3 runner in +[#26985](https://github.com/apache/beam/issues/26985), fixed in +[#27001](https://github.com/apache/beam/pull/27001). + +The shared `spark_runner.gradle` already excludes `slf4j-jdk14` from the +runner module's own `configurations.all`, so in-tree builds are unaffected. +Downstream Gradle consumers that assemble a runtime classpath against +`beam-runners-spark-4` should mirror that exclude: + +```groovy +configurations.all { + exclude group: "org.slf4j", module: "slf4j-jdk14" +} +``` + +For Maven, exclude `org.slf4j:slf4j-jdk14` from any dependency that pulls it +transitively (commonly the Beam SDK harness and several IO connectors). diff --git a/runners/spark/4/build.gradle b/runners/spark/4/build.gradle new file mode 100644 index 000000000000..01fb3680b078 --- /dev/null +++ b/runners/spark/4/build.gradle @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '..' +/* All properties required for loading the Spark build script */ +project.ext { + spark_major = '4' + // Spark 4 version as defined in BeamModulePlugin; requires Scala 2.13 and Java 17 + spark_version = spark4_version + spark_scala_version = '2.13' + archives_base_name = 'beam-runners-spark-4' +} + +// Load the main build script which contains all build logic. +// spark_runner.gradle handles the per-version source-overrides Copy: +// shared base (runners/spark/src/) + previous majors + this module's ./src/ are +// merged into build/source-overrides/src using DuplicatesStrategy.INCLUDE so the +// 11 files under runners/spark/4/src/.../structuredstreaming/ override the +// shared-base versions. +apply from: "$basePath/spark_runner.gradle" + +// Spark 4 always requires Java 17, so unconditionally add the --add-opens flags +// required by Kryo and other libraries that use reflection on JDK internals. +test { + jvmArgs "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED" +} + +// Exclude DStream-based streaming tests from the shared-base copy: the Spark 4 module +// supports only structured streaming (batch) and does not include legacy DStream support. +// Streaming test utilities also depend on kafka.server.KafkaServerStartable which was +// removed in Kafka 2.8.0 (the first Kafka version with a _2.13 artifact). +tasks.named("copyTestSourceOverrides") { + exclude "**/translation/streaming/**" +} + diff --git a/runners/spark/4/job-server/build.gradle b/runners/spark/4/job-server/build.gradle new file mode 100644 index 000000000000..598cf3b4913a --- /dev/null +++ b/runners/spark/4/job-server/build.gradle @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '../../job-server' + +project.ext { + // Look for the source code in the parent module + main_source_dirs = ["$basePath/src/main/java"] + test_source_dirs = ["$basePath/src/test/java"] + main_resources_dirs = ["$basePath/src/main/resources"] + test_resources_dirs = ["$basePath/src/test/resources"] + archives_base_name = 'beam-runners-spark-4-job-server' +} + +// Load the main build script which contains all build logic. +apply from: "$basePath/spark_job_server.gradle" diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java new file mode 100644 index 000000000000..d32dc14eccc0 --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.io; + +import static java.util.stream.Collectors.toList; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.emptyList; +import static org.apache.beam.sdk.values.WindowedValues.timestampedValueInGlobalWindow; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; +import static scala.collection.JavaConverters.asScalaIterator; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntSupplier; +import java.util.function.Supplier; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.beam.sdk.io.BoundedSource; +import org.apache.beam.sdk.io.BoundedSource.BoundedReader; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.AbstractIterator; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.spark.InterruptibleIterator; +import org.apache.spark.Partition; +import org.apache.spark.SparkContext; +import org.apache.spark.TaskContext; +import org.apache.spark.rdd.RDD; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.catalyst.InternalRow; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder.Serializer; +import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan; +import org.apache.spark.sql.classic.Dataset$; +import org.apache.spark.sql.connector.catalog.SupportsRead; +import org.apache.spark.sql.connector.catalog.Table; +import org.apache.spark.sql.connector.catalog.TableCapability; +import org.apache.spark.sql.connector.read.Batch; +import org.apache.spark.sql.connector.read.InputPartition; +import org.apache.spark.sql.connector.read.PartitionReader; +import org.apache.spark.sql.connector.read.PartitionReaderFactory; +import org.apache.spark.sql.connector.read.Scan; +import org.apache.spark.sql.connector.read.ScanBuilder; +import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation; +import org.apache.spark.sql.types.StructType; +import org.apache.spark.sql.util.CaseInsensitiveStringMap; +import scala.Option; +import scala.collection.Iterator; +import scala.reflect.ClassTag; + +public class BoundedDatasetFactory { + private BoundedDatasetFactory() {} + + /** + * Create a {@link Dataset} for a {@link BoundedSource} via a Spark {@link Table}. + * + *

    Unfortunately tables are expected to return an {@link InternalRow}, requiring serialization. + * This makes this approach at the time being significantly less performant than creating a + * dataset from an RDD. + */ + public static Dataset> createDatasetFromRows( + SparkSession session, + BoundedSource source, + Supplier options, + Encoder> encoder) { + Params params = new Params<>(encoder, options, session.sparkContext().defaultParallelism()); + BeamTable table = new BeamTable<>(source, params); + LogicalPlan logicalPlan = DataSourceV2Relation.create(table, Option.empty(), Option.empty()); + // In Spark 4.0+, Dataset$ moved to org.apache.spark.sql.classic and its ofRows() now + // takes the classic SparkSession subclass. The runtime instance returned by + // SparkSession.builder() is always a classic.SparkSession, so the downcast is safe and + // avoids reflection. + return (Dataset>) + Dataset$.MODULE$ + .ofRows((org.apache.spark.sql.classic.SparkSession) session, logicalPlan) + .as(encoder); + } + + /** + * Create a {@link Dataset} for a {@link BoundedSource} via a Spark {@link RDD}. + * + *

    This is currently the most efficient approach as it avoid any serialization overhead. + */ + public static Dataset> createDatasetFromRDD( + SparkSession session, + BoundedSource source, + Supplier options, + Encoder> encoder) { + Params params = new Params<>(encoder, options, session.sparkContext().defaultParallelism()); + RDD> rdd = new BoundedRDD<>(session.sparkContext(), source, params); + return session.createDataset(rdd, encoder); + } + + /** An {@link RDD} for a bounded Beam source. */ + private static class BoundedRDD extends RDD> { + final BoundedSource source; + final Params params; + + public BoundedRDD(SparkContext sc, BoundedSource source, Params params) { + super(sc, emptyList(), ClassTag.apply(WindowedValue.class)); + this.source = source; + this.params = params; + } + + @Override + public Iterator> compute(Partition split, TaskContext context) { + return new InterruptibleIterator<>( + context, + asScalaIterator(new SourcePartitionIterator<>((SourcePartition) split, params))); + } + + @Override + public Partition[] getPartitions() { + return SourcePartition.partitionsOf(source, params).toArray(new Partition[0]); + } + } + + /** A Spark {@link Table} for a bounded Beam source supporting batch reads only. */ + private static class BeamTable implements Table, SupportsRead { + final BoundedSource source; + final Params params; + + BeamTable(BoundedSource source, Params params) { + this.source = source; + this.params = params; + } + + public Encoder> getEncoder() { + return params.encoder; + } + + @Override + public ScanBuilder newScanBuilder(CaseInsensitiveStringMap ignored) { + return () -> + new Scan() { + @Override + public StructType readSchema() { + return params.encoder.schema(); + } + + @Override + public Batch toBatch() { + return new BeamBatch<>(source, params); + } + }; + } + + @Override + public String name() { + return "BeamSource<" + source.getClass().getName() + ">"; + } + + @Override + public StructType schema() { + return params.encoder.schema(); + } + + @Override + public Set capabilities() { + return ImmutableSet.of(TableCapability.BATCH_READ); + } + + private static class BeamBatch implements Batch, Serializable { + final BoundedSource source; + final Params params; + + private BeamBatch(BoundedSource source, Params params) { + this.source = source; + this.params = params; + } + + @Override + public InputPartition[] planInputPartitions() { + return SourcePartition.partitionsOf(source, params).toArray(new InputPartition[0]); + } + + @Override + public PartitionReaderFactory createReaderFactory() { + return p -> new BeamPartitionReader<>(((SourcePartition) p), params); + } + } + + private static class BeamPartitionReader implements PartitionReader { + final SourcePartitionIterator iterator; + final Serializer> serializer; + transient @Nullable InternalRow next; + + BeamPartitionReader(SourcePartition partition, Params params) { + iterator = new SourcePartitionIterator<>(partition, params); + serializer = ((ExpressionEncoder>) params.encoder).createSerializer(); + } + + @Override + public boolean next() throws IOException { + if (iterator.hasNext()) { + next = serializer.apply(iterator.next()); + return true; + } + return false; + } + + @Override + public InternalRow get() { + if (next == null) { + throw new IllegalStateException("Next not available"); + } + return next; + } + + @Override + public void close() throws IOException { + next = null; + iterator.close(); + } + } + } + + /** A Spark partition wrapping the partitioned Beam {@link BoundedSource}. */ + private static class SourcePartition implements Partition, InputPartition { + final BoundedSource source; + final int index; + + SourcePartition(BoundedSource source, IntSupplier idxSupplier) { + this.source = source; + this.index = idxSupplier.getAsInt(); + } + + static List> partitionsOf(BoundedSource source, Params params) { + try { + PipelineOptions options = params.options.get(); + long desiredSize = source.getEstimatedSizeBytes(options) / params.numPartitions; + List> split = source.split(desiredSize, options); + IntSupplier idxSupplier = new AtomicInteger(0)::getAndIncrement; + return split.stream().map(s -> new SourcePartition<>(s, idxSupplier)).collect(toList()); + } catch (Exception e) { + throw new RuntimeException( + "Error splitting BoundedSource " + source.getClass().getCanonicalName(), e); + } + } + + @Override + public int index() { + return index; + } + + @Override + public int hashCode() { + return index; + } + } + + /** A partition iterator on a partitioned Beam {@link BoundedSource}. */ + private static class SourcePartitionIterator extends AbstractIterator> + implements Closeable { + BoundedReader reader; + boolean started = false; + + public SourcePartitionIterator(SourcePartition partition, Params params) { + try { + reader = partition.source.createReader(params.options.get()); + } catch (IOException e) { + throw new RuntimeException("Failed to create reader from a BoundedSource.", e); + } + } + + @Override + @SuppressWarnings("nullness") // ok, reader not used any longer + public void close() throws IOException { + if (reader != null) { + try { + reader.close(); + } finally { + reader = null; + } + } + } + + @Override + protected @CheckForNull WindowedValue computeNext() { + try { + if (started ? reader.advance() : start()) { + return timestampedValueInGlobalWindow(reader.getCurrent(), reader.getCurrentTimestamp()); + } else { + close(); + return endOfData(); + } + } catch (IOException e) { + throw new RuntimeException("Failed to start or advance reader.", e); + } + } + + private boolean start() throws IOException { + started = true; + return reader.start(); + } + } + + /** Shared parameters. */ + private static class Params implements Serializable { + final Encoder> encoder; + final Supplier options; + final int numPartitions; + + Params( + Encoder> encoder, Supplier options, int numPartitions) { + checkArgument(numPartitions > 0, "Number of partitions must be greater than zero."); + this.encoder = encoder; + this.options = options; + this.numPartitions = numPartitions; + } + } +} diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java new file mode 100644 index 000000000000..e483c2db0df4 --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.batch; + +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.eligibleForGlobalGroupBy; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.eligibleForGroupByWindow; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.explodeWindowedKey; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.value; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.valueKey; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.valueValue; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.windowedKV; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.fun1; + +import java.util.Collection; +import org.apache.beam.runners.spark.structuredstreaming.translation.TransformTranslator; +import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop; +import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.Fun1; +import org.apache.beam.sdk.coders.CannotProvideCoderException; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.CoderRegistry; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.transforms.Combine; +import org.apache.beam.sdk.transforms.Combine.CombineFn; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.expressions.Aggregator; +import scala.Tuple2; +import scala.collection.IterableOnce; + +/** + * Translator for {@link Combine.PerKey} using {@link Dataset#groupByKey} with a Spark {@link + * Aggregator}. + * + *

      + *
    • When using the default global window, window information is dropped and restored after the + * aggregation. + *
    • For non-merging windows, windows are exploded and moved into a composite key for better + * distribution. After the aggregation, windowed values are restored from the composite key. + *
    • All other cases use an aggregator on windowed values that is optimized for the current + * windowing strategy. + *
    + * + * TODOs: + *
  • combine with context (CombineFnWithContext)? + *
  • combine with sideInputs? + *
  • are there other missing features? + */ +class CombinePerKeyTranslatorBatch + extends TransformTranslator< + PCollection>, PCollection>, Combine.PerKey> { + + CombinePerKeyTranslatorBatch() { + super(0.2f); + } + + @Override + public void translate(Combine.PerKey transform, Context cxt) { + WindowingStrategy windowing = cxt.getInput().getWindowingStrategy(); + CombineFn combineFn = (CombineFn) transform.getFn(); + + KvCoder inputCoder = (KvCoder) cxt.getInput().getCoder(); + KvCoder outputCoder = (KvCoder) cxt.getOutput().getCoder(); + + Encoder keyEnc = cxt.keyEncoderOf(inputCoder); + Encoder> inputEnc = cxt.encoderOf(inputCoder); + Encoder>> wvOutputEnc = cxt.windowedEncoder(outputCoder); + Encoder accumEnc = accumEncoder(combineFn, inputCoder.getValueCoder(), cxt); + + final Dataset>> result; + + boolean globalGroupBy = eligibleForGlobalGroupBy(windowing, true); + boolean groupByWindow = eligibleForGroupByWindow(windowing, true); + + if (globalGroupBy || groupByWindow) { + Aggregator, ?, OutT> valueAgg = + Aggregators.value(combineFn, KV::getValue, accumEnc, cxt.valueEncoderOf(outputCoder)); + + if (globalGroupBy) { + // Drop window and group by key globally to run the aggregation (combineFn), afterwards the + // global window is restored + result = + cxt.getDataset(cxt.getInput()) + .groupByKey(valueKey(), keyEnc) + .mapValues(value(), inputEnc) + .agg(valueAgg.toColumn()) + .map(globalKV(), wvOutputEnc); + } else { + Encoder> windowedKeyEnc = + cxt.tupleEncoder(cxt.windowEncoder(), keyEnc); + + // Group by window and key to run the aggregation (combineFn) + result = + cxt.getDataset(cxt.getInput()) + .flatMap(explodeWindowedKey(value()), cxt.tupleEncoder(windowedKeyEnc, inputEnc)) + .groupByKey(fun1(Tuple2::_1), windowedKeyEnc) + .mapValues(fun1(Tuple2::_2), inputEnc) + .agg(valueAgg.toColumn()) + .map(windowedKV(), wvOutputEnc); + } + } else { + // Optimized aggregator for non-merging and session window functions, all others depend on + // windowFn.mergeWindows + Aggregator>, ?, Collection>> aggregator = + Aggregators.windowedValue( + combineFn, + valueValue(), + windowing, + cxt.windowEncoder(), + accumEnc, + cxt.windowedEncoder(outputCoder.getValueCoder())); + result = + cxt.getDataset(cxt.getInput()) + .groupByKey(valueKey(), keyEnc) + .agg(aggregator.toColumn()) + .flatMap(explodeWindows(), wvOutputEnc); + } + + cxt.putDataset(cxt.getOutput(), result); + } + + private static + Fun1>>, IterableOnce>>> + explodeWindows() { + return t -> + ScalaInterop.scalaIterator(t._2).map(wv -> wv.withValue(KV.of(t._1, wv.getValue()))); + } + + private static Fun1, WindowedValue>> globalKV() { + return t -> WindowedValues.valueInGlobalWindow(KV.of(t._1, t._2)); + } + + private Encoder accumEncoder( + CombineFn fn, Coder valueCoder, Context cxt) { + try { + CoderRegistry registry = cxt.getInput().getPipeline().getCoderRegistry(); + return cxt.encoderOf(fn.getAccumulatorCoder(registry, valueCoder)); + } catch (CannotProvideCoderException e) { + throw new RuntimeException(e); + } + } +} diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java new file mode 100644 index 000000000000..f25121e1b478 --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.batch; + +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.tuple; +import static org.apache.beam.sdk.transforms.windowing.PaneInfo.NO_FIRING; +import static org.apache.beam.sdk.transforms.windowing.TimestampCombiner.END_OF_WINDOW; + +import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop; +import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.Fun1; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindows; +import org.apache.beam.sdk.transforms.windowing.TimestampCombiner; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.sdk.values.WindowingStrategy; +import scala.Tuple2; +import scala.collection.IterableOnce; + +/** + * Package private helpers to support translating grouping transforms using `groupByKey` such as + * {@link GroupByKeyTranslatorBatch} or {@link CombinePerKeyTranslatorBatch}. + */ +class GroupByKeyHelpers { + + private GroupByKeyHelpers() {} + + /** + * Checks if it's possible to use an optimized `groupByKey` that also moves the window into the + * key. + * + * @param windowing The windowing strategy + * @param endOfWindowOnly Flag if to limit this optimization to {@link + * TimestampCombiner#END_OF_WINDOW}. + */ + static boolean eligibleForGroupByWindow( + WindowingStrategy windowing, boolean endOfWindowOnly) { + return !windowing.needsMerge() + && (!endOfWindowOnly || windowing.getTimestampCombiner() == END_OF_WINDOW) + && windowing.getWindowFn().windowCoder().consistentWithEquals(); + } + + /** + * Checks if it's possible to use an optimized `groupByKey` for the global window. + * + * @param windowing The windowing strategy + * @param endOfWindowOnly Flag if to limit this optimization to {@link + * TimestampCombiner#END_OF_WINDOW}. + */ + static boolean eligibleForGlobalGroupBy( + WindowingStrategy windowing, boolean endOfWindowOnly) { + return windowing.getWindowFn() instanceof GlobalWindows + && (!endOfWindowOnly || windowing.getTimestampCombiner() == END_OF_WINDOW); + } + + /** + * Explodes a windowed {@link KV} assigned to potentially multiple {@link BoundedWindow}s to a + * traversable of composite keys {@code (BoundedWindow, Key)} and value. + */ + static + Fun1>, IterableOnce, T>>> + explodeWindowedKey(Fun1>, T> valueFn) { + return v -> { + T value = valueFn.apply(v); + K key = v.getValue().getKey(); + return ScalaInterop.scalaIterator(v.getWindows()).map(w -> tuple(tuple(w, key), value)); + }; + } + + static Fun1, V>, WindowedValue>> windowedKV() { + return t -> windowedKV(t._1, t._2); + } + + static WindowedValue> windowedKV(Tuple2 key, V value) { + return WindowedValues.of(KV.of(key._2, value), key._1.maxTimestamp(), key._1, NO_FIRING); + } + + static Fun1, V> value() { + return v -> v.getValue(); + } + + static Fun1>, V> valueValue() { + return v -> v.getValue().getValue(); + } + + static Fun1>, K> valueKey() { + return v -> v.getValue().getKey(); + } +} diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java new file mode 100644 index 000000000000..7caf06cb38fd --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.batch; + +import static org.apache.beam.repackaged.core.org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.eligibleForGlobalGroupBy; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.eligibleForGroupByWindow; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.explodeWindowedKey; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.valueKey; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.valueValue; +import static org.apache.beam.runners.spark.structuredstreaming.translation.batch.GroupByKeyHelpers.windowedKV; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.CoderHelpers.toByteArray; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.collectionEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.encoderOf; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.kvEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.windowedValueEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.concat; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.fun1; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.fun2; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.javaIterator; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.seqOf; +import static org.apache.beam.sdk.transforms.windowing.PaneInfo.NO_FIRING; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; +import static org.apache.spark.sql.functions.col; +import static org.apache.spark.sql.functions.collect_list; +import static org.apache.spark.sql.functions.explode; +import static org.apache.spark.sql.functions.max; +import static org.apache.spark.sql.functions.min; +import static org.apache.spark.sql.functions.struct; + +import java.io.Serializable; +import org.apache.beam.runners.core.InMemoryStateInternals; +import org.apache.beam.runners.core.ReduceFnRunner; +import org.apache.beam.runners.core.StateInternalsFactory; +import org.apache.beam.runners.core.SystemReduceFn; +import org.apache.beam.runners.spark.SparkCommonPipelineOptions; +import org.apache.beam.runners.spark.structuredstreaming.translation.TransformTranslator; +import org.apache.beam.runners.spark.structuredstreaming.translation.batch.functions.GroupAlsoByWindowViaOutputBufferFn; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.transforms.GroupByKey; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo.PaneInfoCoder; +import org.apache.beam.sdk.transforms.windowing.TimestampCombiner; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.TypedColumn; +import org.apache.spark.sql.types.DataType; +import org.apache.spark.sql.types.DataTypes; +import org.checkerframework.checker.nullness.qual.NonNull; +import scala.Tuple2; +import scala.collection.Iterator; +import scala.collection.JavaConverters; +import scala.collection.immutable.List; + +/** + * Translator for {@link GroupByKey} using {@link Dataset#groupByKey} with the built-in aggregation + * function {@code collect_list} when applicable. + * + *

    Note: Using {@code collect_list} isn't any worse than using {@link ReduceFnRunner}. In the + * latter case the entire group (iterator) has to be loaded into memory as well. Either way there's + * a risk of OOM errors. When enabling {@link + * SparkCommonPipelineOptions#getPreferGroupByKeyToHandleHugeValues()}, a more memory sensitive + * iterable is used that can be traversed just once. Attempting to traverse the iterable again will + * throw. + * + *

      + *
    • When using the default global window, window information is dropped and restored after the + * aggregation. + *
    • For non-merging windows, windows are exploded and moved into a composite key for better + * distribution. Though, to keep the amount of shuffled data low, this is only done if values + * are assigned to a single window or if there are only few keys and distributing data is + * important. After the aggregation, windowed values are restored from the composite key. + *
    • All other cases are implemented using the SDK {@link ReduceFnRunner}. + *
    + */ +class GroupByKeyTranslatorBatch + extends TransformTranslator< + PCollection>, PCollection>>, GroupByKey> { + + /** Literal of binary encoded Pane info. */ + private static final Column PANE_NO_FIRING = lit(toByteArray(NO_FIRING, PaneInfoCoder.of())); + + /** Defaults for value in single global window. */ + private static final List GLOBAL_WINDOW_DETAILS = + windowDetails(lit(new byte[][] {EMPTY_BYTE_ARRAY})); + + GroupByKeyTranslatorBatch() { + super(0.2f); + } + + @Override + public void translate(GroupByKey transform, Context cxt) { + WindowingStrategy windowing = cxt.getInput().getWindowingStrategy(); + TimestampCombiner tsCombiner = windowing.getTimestampCombiner(); + + Dataset>> input = cxt.getDataset(cxt.getInput()); + + KvCoder inputCoder = (KvCoder) cxt.getInput().getCoder(); + KvCoder> outputCoder = (KvCoder>) cxt.getOutput().getCoder(); + + Encoder valueEnc = cxt.valueEncoderOf(inputCoder); + Encoder keyEnc = cxt.keyEncoderOf(inputCoder); + + // In batch we can ignore triggering and allowed lateness parameters + final Dataset>>> result; + + boolean useCollectList = + !cxt.getOptions() + .as(SparkCommonPipelineOptions.class) + .getPreferGroupByKeyToHandleHugeValues(); + if (useCollectList && eligibleForGlobalGroupBy(windowing, false)) { + // Collects all values per key in memory. This might be problematic if there's + // few keys only + // or some highly skewed distribution. + result = + input + .groupBy(col("value.key").as("key")) + .agg(collect_list(col("value.value")).as("values"), timestampAggregator(tsCombiner)) + .select( + inGlobalWindow( + keyValue(col("key").as(keyEnc), col("values").as(iterableEnc(valueEnc))), + windowTimestamp(tsCombiner))); + + } else if (eligibleForGlobalGroupBy(windowing, true)) { + // Produces an iterable that can be traversed exactly once. However, on the plus + // side, data is + // not collected in memory until serialized or done by the user. + result = + cxt.getDataset(cxt.getInput()) + .groupByKey(valueKey(), keyEnc) + .mapValues(valueValue(), cxt.valueEncoderOf(inputCoder)) + .mapGroups(fun2((k, it) -> KV.of(k, iterableOnce(it))), cxt.kvEncoderOf(outputCoder)) + .map(fun1(WindowedValues::valueInGlobalWindow), cxt.windowedEncoder(outputCoder)); + + } else if (useCollectList + && eligibleForGroupByWindow(windowing, false) + && (windowing.getWindowFn().assignsToOneWindow() || transform.fewKeys())) { + // Using the window as part of the key should help to better distribute the + // data. However, if + // values are assigned to multiple windows, more data would be shuffled around. + // If there's few + // keys only, this is still valuable. + // Collects all values per key & window in memory. + result = + input + .select(explode(col("windows")).as("window"), col("value"), col("timestamp")) + .groupBy(col("value.key").as("key"), col("window")) + .agg(collect_list(col("value.value")).as("values"), timestampAggregator(tsCombiner)) + .select( + inSingleWindow( + keyValue(col("key").as(keyEnc), col("values").as(iterableEnc(valueEnc))), + col("window").as(cxt.windowEncoder()), + windowTimestamp(tsCombiner))); + + } else if (eligibleForGroupByWindow(windowing, true) + && (windowing.getWindowFn().assignsToOneWindow() || transform.fewKeys())) { + // Using the window as part of the key should help to better distribute the + // data. However, if + // values are assigned to multiple windows, more data would be shuffled around. + // If there's few + // keys only, this is still valuable. + // Produces an iterable that can be traversed exactly once. However, on the plus + // side, data is + // not collected in memory until serialized or done by the user. + Encoder> windowedKeyEnc = + cxt.tupleEncoder(cxt.windowEncoder(), keyEnc); + result = + cxt.getDataset(cxt.getInput()) + .flatMap(explodeWindowedKey(valueValue()), cxt.tupleEncoder(windowedKeyEnc, valueEnc)) + .groupByKey(fun1(t -> t._1()), windowedKeyEnc) + .mapValues(fun1(t -> t._2()), valueEnc) + .mapGroups( + fun2((wKey, it) -> windowedKV(wKey, iterableOnce(it))), + cxt.windowedEncoder(outputCoder)); + + } else { + // Collects all values per key in memory. This might be problematic if there's + // few keys only + // or some highly skewed distribution. + + // FIXME Revisit this case, implementation is far from ideal: + // - iterator traversed at least twice, forcing materialization in memory + + // group by key, then by windows + result = + input + .groupByKey(valueKey(), keyEnc) + .flatMapGroups( + new GroupAlsoByWindowViaOutputBufferFn<>( + windowing, + (SerStateInternalsFactory) key -> InMemoryStateInternals.forKey(key), + SystemReduceFn.buffering(inputCoder.getValueCoder()), + cxt.getOptionsSupplier()), + cxt.windowedEncoder(outputCoder)); + } + + cxt.putDataset(cxt.getOutput(), result); + } + + /** Serializable In-memory state internals factory. */ + private interface SerStateInternalsFactory extends StateInternalsFactory, Serializable {} + + private Encoder> iterableEnc(Encoder enc) { + // safe to use list encoder with collect list + return (Encoder) collectionEncoder(enc); + } + + private static Column[] timestampAggregator(TimestampCombiner tsCombiner) { + if (tsCombiner.equals(TimestampCombiner.END_OF_WINDOW)) { + return new Column[0]; // no aggregation needed + } + Column agg = + tsCombiner.equals(TimestampCombiner.EARLIEST) + ? min(col("timestamp")) + : max(col("timestamp")); + return new Column[] {agg.as("timestamp")}; + } + + private static Column windowTimestamp(TimestampCombiner tsCombiner) { + if (tsCombiner.equals(TimestampCombiner.END_OF_WINDOW)) { + // null will be set to END_OF_WINDOW by the respective deserializer + return litNull(DataTypes.LongType); + } + return col("timestamp"); + } + + /** + * Java {@link Iterable} from Scala {@link Iterator} that can be iterated just once so that we + * don't have to load all data into memory. + */ + private static Iterable iterableOnce(Iterator it) { + return () -> { + checkState(!it.isEmpty(), "Iterator on values can only be consumed once!"); + return javaIterator(it); + }; + } + + private TypedColumn> keyValue(TypedColumn key, TypedColumn value) { + return struct(key.as("key"), value.as("value")).as(kvEncoder(key.encoder(), value.encoder())); + } + + private static TypedColumn> inGlobalWindow( + TypedColumn value, Column ts) { + List fields = concat(timestampedValue(value, ts), GLOBAL_WINDOW_DETAILS); + Encoder> enc = + windowedValueEncoder(value.encoder(), encoderOf(GlobalWindow.class)); + return (TypedColumn>) + struct(JavaConverters.asJavaCollection(fields).toArray(new Column[0])).as(enc); + } + + public static TypedColumn> inSingleWindow( + TypedColumn value, TypedColumn window, Column ts) { + Column windows = org.apache.spark.sql.functions.array(window); + List fields = concat(timestampedValue(value, ts), windowDetails(windows)); + Encoder> enc = windowedValueEncoder(value.encoder(), window.encoder()); + return (TypedColumn>) + struct(JavaConverters.asJavaCollection(fields).toArray(new Column[0])).as(enc); + } + + private static List timestampedValue(Column value, Column ts) { + return seqOf(value.as("value"), ts.as("timestamp")).toList(); + } + + private static List windowDetails(Column windows) { + return seqOf(windows.as("windows"), PANE_NO_FIRING.as("paneInfo")).toList(); + } + + private static Column lit(T t) { + return org.apache.spark.sql.functions.lit(t); + } + + @SuppressWarnings("nullness") // NULL literal + private static Column litNull(DataType dataType) { + return org.apache.spark.sql.functions.lit(null).cast(dataType); + } +} diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java new file mode 100644 index 000000000000..6565c2a01c63 --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.helpers; + +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.emptyList; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.replace; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.seqOf; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import org.apache.spark.sql.catalyst.analysis.GetColumnByOrdinal; +import org.apache.spark.sql.catalyst.encoders.AgnosticEncoder; +import org.apache.spark.sql.catalyst.encoders.AgnosticEncoders; +import org.apache.spark.sql.catalyst.encoders.AgnosticExpressionPathEncoder; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder; +import org.apache.spark.sql.catalyst.expressions.BoundReference; +import org.apache.spark.sql.catalyst.expressions.Expression; +import org.apache.spark.sql.catalyst.expressions.objects.Invoke; +import org.apache.spark.sql.catalyst.expressions.objects.NewInstance; +import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke; +import org.apache.spark.sql.types.DataType; +import org.apache.spark.sql.types.Metadata; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import scala.Option; +import scala.collection.Iterator; +import scala.collection.immutable.Seq; +import scala.reflect.ClassTag; + +public class EncoderFactory { + // Resolve the Scala case-class primary constructor (the one with the most parameters). + // Constructor ordering returned by Class.getConstructors() is JVM-defined and not stable + // across Spark versions, so we pick the widest constructor explicitly and then dispatch on + // parameter count below to pick the right argument shape per Spark version. + private static final Constructor STATIC_INVOKE_CONSTRUCTOR = + primaryConstructor(StaticInvoke.class); + + private static final Constructor INVOKE_CONSTRUCTOR = primaryConstructor(Invoke.class); + + private static final Constructor NEW_INSTANCE_CONSTRUCTOR = + primaryConstructor(NewInstance.class); + + @SuppressWarnings("unchecked") + private static Constructor primaryConstructor(Class cls) { + Constructor[] ctors = cls.getConstructors(); + Constructor widest = ctors[0]; + for (int i = 1; i < ctors.length; i++) { + if (ctors[i].getParameterCount() > widest.getParameterCount()) { + widest = ctors[i]; + } + } + return (Constructor) widest; + } + + @SuppressWarnings({"nullness", "unchecked"}) + static ExpressionEncoder create( + Expression serializer, Expression deserializer, Class clazz) { + AgnosticEncoder agnosticEncoder = new BeamAgnosticEncoder<>(serializer, deserializer, clazz); + return ExpressionEncoder.apply(agnosticEncoder, serializer, deserializer); + } + + /** + * An {@link AgnosticEncoder} that implements both {@link AgnosticExpressionPathEncoder} (so that + * {@code SerializerBuildHelper} / {@code DeserializerBuildHelper} delegate to our pre-built + * expressions) and {@link AgnosticEncoders.StructEncoder} (so that {@code + * Dataset.select(TypedColumn)} creates an N-attribute plan instead of a 1-attribute wrapped plan, + * preventing {@code FIELD_NUMBER_MISMATCH} errors). + * + *

    The {@code toCatalyst} / {@code fromCatalyst} methods substitute the {@code input} + * expression into the pre-built serializer / deserializer via {@code transformUp}, so that when + * this encoder is nested inside a composite encoder (e.g. {@code Encoders.tuple}) the correct + * field-level expression is used in place of the root {@code BoundReference} / {@code + * GetColumnByOrdinal}. + */ + @SuppressWarnings({"nullness", "unchecked", "deprecation"}) + private static final class BeamAgnosticEncoder + implements AgnosticExpressionPathEncoder, AgnosticEncoders.StructEncoder { + + private final Expression serializer; + private final Expression deserializer; + private final Class clazz; + private final Seq encoderFields; + + BeamAgnosticEncoder(Expression serializer, Expression deserializer, Class clazz) { + this.serializer = serializer; + this.deserializer = deserializer; + this.clazz = clazz; + this.encoderFields = buildFields(serializer.dataType()); + } + + private static Seq buildFields(DataType dt) { + if (dt instanceof StructType) { + StructField[] structFields = ((StructType) dt).fields(); + List fields = new ArrayList<>(structFields.length); + for (StructField sf : structFields) { + fields.add( + new AgnosticEncoders.EncoderField( + sf.name(), + new FieldEncoder<>(sf.dataType(), sf.nullable()), + sf.nullable(), + sf.metadata(), + Option.empty(), + Option.empty())); + } + return seqOf(fields.toArray(new AgnosticEncoders.EncoderField[0])); + } else { + // Non-struct: wrap in a single "value" field so StructEncoder sees one field. + return seqOf( + new AgnosticEncoders.EncoderField( + "value", + new FieldEncoder<>(dt, true), + true, + Metadata.empty(), + Option.empty(), + Option.empty())); + } + } + + // --- AgnosticExpressionPathEncoder --- + + @Override + public Expression toCatalyst(Expression input) { + return serializer.transformUp(replace(BoundReference.class, input)); + } + + @Override + public Expression fromCatalyst(Expression input) { + return deserializer.transformUp(replace(GetColumnByOrdinal.class, input)); + } + + // --- AgnosticEncoders.StructEncoder --- + + @Override + public Seq fields() { + return encoderFields; + } + + @Override + public boolean isStruct() { + return true; + } + + /** + * Setter required by the Scala compiler when implementing the {@link + * AgnosticEncoders.StructEncoder} trait from Java. Scala traits with concrete {@code val} + * fields generate a synthetic mangled setter ({@code $_setter__$eq}) that the + * trait's initializer invokes on subclasses. Java cannot declare {@code val} fields, so we + * implement {@link #isStruct()} directly above and accept-but-ignore the trait setter here. The + * mangled name is brittle and tied to Spark's Scala source layout — if Spark removes the {@code + * isStruct} field from {@code StructEncoder}, this method becomes dead code; if Spark renames + * it, compilation will fail and the new mangled name must be substituted. + */ + @Override + public void + org$apache$spark$sql$catalyst$encoders$AgnosticEncoders$StructEncoder$_setter_$isStruct_$eq( + boolean v) { + // no-op: isStruct() is implemented directly above + } + + // --- AgnosticEncoder / Encoder (explicit to resolve default-method ambiguity) --- + + @Override + public boolean isPrimitive() { + return false; + } + + @Override + public StructType schema() { + // Build StructType from fields — mirrors the StructEncoder.schema() default. + List sfs = new ArrayList<>(encoderFields.size()); + Iterator it = encoderFields.iterator(); + while (it.hasNext()) { + sfs.add(it.next().structField()); + } + return new StructType(sfs.toArray(new StructField[0])); + } + + @Override + public DataType dataType() { + return schema(); + } + + @Override + public ClassTag clsTag() { + return (ClassTag) ClassTag.apply(clazz); + } + } + + /** + * Minimal {@link AgnosticEncoder} stub used to carry per-field {@link DataType} metadata inside + * {@link AgnosticEncoders.EncoderField}. The actual serialization / deserialization is handled by + * {@link BeamAgnosticEncoder#toCatalyst} and {@link BeamAgnosticEncoder#fromCatalyst}. + */ + @SuppressWarnings({"nullness", "unchecked"}) + private static final class FieldEncoder implements AgnosticEncoder { + private final DataType fieldDataType; + private final boolean fieldNullable; + + FieldEncoder(DataType dataType, boolean nullable) { + this.fieldDataType = dataType; + this.fieldNullable = nullable; + } + + @Override + public boolean isPrimitive() { + return false; + } + + @Override + public DataType dataType() { + return fieldDataType; + } + + @Override + public StructType schema() { + return new StructType().add("value", fieldDataType, fieldNullable); + } + + @Override + public boolean nullable() { + return fieldNullable; + } + + @Override + public ClassTag clsTag() { + return (ClassTag) ClassTag.apply(Object.class); + } + } + + /** + * Invoke method {@code fun} on Class {@code cls}, immediately propagating {@code null} if any + * input arg is {@code null}. + */ + static Expression invokeIfNotNull(Class cls, String fun, DataType type, Expression... args) { + return invoke(cls, fun, type, true, args); + } + + /** Invoke method {@code fun} on Class {@code cls}. */ + static Expression invoke(Class cls, String fun, DataType type, Expression... args) { + return invoke(cls, fun, type, false, args); + } + + private static Expression invoke( + Class cls, String fun, DataType type, boolean propagateNull, Expression... args) { + try { + // To address breaking interfaces between various versions of Spark, expressions are + // created reflectively. This is fine as it's just needed once to create the query plan. + switch (STATIC_INVOKE_CONSTRUCTOR.getParameterCount()) { + case 6: + // Spark 3.1.x + return STATIC_INVOKE_CONSTRUCTOR.newInstance( + cls, type, fun, seqOf(args), propagateNull, true); + case 7: + // Spark 3.2.0 + return STATIC_INVOKE_CONSTRUCTOR.newInstance( + cls, type, fun, seqOf(args), emptyList(), propagateNull, true); + case 8: + // Spark 3.2.x, 3.3.x + return STATIC_INVOKE_CONSTRUCTOR.newInstance( + cls, type, fun, seqOf(args), emptyList(), propagateNull, true, true); + case 9: + // Spark 4.0.x: added Option> parameter + return STATIC_INVOKE_CONSTRUCTOR.newInstance( + cls, type, fun, seqOf(args), emptyList(), propagateNull, true, true, Option.empty()); + default: + throw new RuntimeException("Unsupported version of Spark"); + } + } catch (IllegalArgumentException | ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + + /** Invoke method {@code fun} on {@code obj} with provided {@code args}. */ + static Expression invoke( + Expression obj, String fun, DataType type, boolean nullable, Expression... args) { + try { + // To address breaking interfaces between various versions of Spark, expressions are + // created reflectively. This is fine as it's just needed once to create the query plan. + switch (INVOKE_CONSTRUCTOR.getParameterCount()) { + case 6: + // Spark 3.1.x + return INVOKE_CONSTRUCTOR.newInstance(obj, fun, type, seqOf(args), false, nullable); + case 7: + // Spark 3.2.0 + return INVOKE_CONSTRUCTOR.newInstance( + obj, fun, type, seqOf(args), emptyList(), false, nullable); + case 8: + // Spark 3.2.x, 3.3.x, 4.0.x: Invoke constructor is 8 params across all these versions + return INVOKE_CONSTRUCTOR.newInstance( + obj, fun, type, seqOf(args), emptyList(), false, nullable, true); + default: + throw new RuntimeException("Unsupported version of Spark"); + } + } catch (IllegalArgumentException | ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + + static Expression newInstance(Class cls, DataType type, Expression... args) { + try { + // To address breaking interfaces between various versions of Spark, expressions are + // created reflectively. This is fine as it's just needed once to create the query plan. + switch (NEW_INSTANCE_CONSTRUCTOR.getParameterCount()) { + case 5: + return NEW_INSTANCE_CONSTRUCTOR.newInstance(cls, seqOf(args), true, type, Option.empty()); + case 6: + // Spark 3.2.x, 3.3.x, 4.0.x: added immutable.Seq parameter + return NEW_INSTANCE_CONSTRUCTOR.newInstance( + cls, seqOf(args), emptyList(), true, type, Option.empty()); + default: + throw new RuntimeException("Unsupported version of Spark"); + } + } catch (IllegalArgumentException | ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java new file mode 100644 index 000000000000..173b4653a19b --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java @@ -0,0 +1,617 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.helpers; + +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderFactory.invoke; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderFactory.invokeIfNotNull; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.match; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.replace; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.seqOf; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.tuple; +import static org.apache.spark.sql.types.DataTypes.BinaryType; +import static org.apache.spark.sql.types.DataTypes.IntegerType; +import static org.apache.spark.sql.types.DataTypes.LongType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow.IntervalWindowCoder; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.transforms.windowing.PaneInfo.PaneInfoCoder; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.Encoders; +import org.apache.spark.sql.catalyst.SerializerBuildHelper; +import org.apache.spark.sql.catalyst.SerializerBuildHelper.MapElementInformation; +import org.apache.spark.sql.catalyst.analysis.GetColumnByOrdinal; +import org.apache.spark.sql.catalyst.encoders.AgnosticEncoder; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder; +import org.apache.spark.sql.catalyst.expressions.BoundReference; +import org.apache.spark.sql.catalyst.expressions.Coalesce; +import org.apache.spark.sql.catalyst.expressions.CreateNamedStruct; +import org.apache.spark.sql.catalyst.expressions.EqualTo; +import org.apache.spark.sql.catalyst.expressions.Expression; +import org.apache.spark.sql.catalyst.expressions.GetStructField; +import org.apache.spark.sql.catalyst.expressions.If; +import org.apache.spark.sql.catalyst.expressions.IsNotNull; +import org.apache.spark.sql.catalyst.expressions.IsNull; +import org.apache.spark.sql.catalyst.expressions.Literal; +import org.apache.spark.sql.catalyst.expressions.Literal$; +import org.apache.spark.sql.catalyst.expressions.MapKeys; +import org.apache.spark.sql.catalyst.expressions.MapValues; +import org.apache.spark.sql.catalyst.expressions.objects.MapObjects$; +import org.apache.spark.sql.catalyst.util.ArrayData; +import org.apache.spark.sql.types.ArrayType; +import org.apache.spark.sql.types.DataType; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.MapType; +import org.apache.spark.sql.types.ObjectType; +import org.apache.spark.sql.types.StructType; +import org.apache.spark.util.MutablePair; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; +import scala.Option; +import scala.Some; +import scala.Tuple2; +import scala.collection.IndexedSeq; +import scala.collection.JavaConverters; +import scala.collection.Seq; + +/** {@link Encoders} utility class. */ +public class EncoderHelpers { + private static final DataType OBJECT_TYPE = new ObjectType(Object.class); + private static final DataType TUPLE2_TYPE = new ObjectType(Tuple2.class); + private static final DataType WINDOWED_VALUE = new ObjectType(WindowedValue.class); + private static final DataType KV_TYPE = new ObjectType(KV.class); + private static final DataType MUTABLE_PAIR_TYPE = new ObjectType(MutablePair.class); + private static final DataType LIST_TYPE = new ObjectType(List.class); + + // Collections / maps of these types can be (de)serialized without (de)serializing each member + private static final Set> PRIMITIVE_TYPES = + ImmutableSet.of( + Boolean.class, + Byte.class, + Short.class, + Integer.class, + Long.class, + Float.class, + Double.class); + + // Default encoders by class + private static final Map, Encoder> DEFAULT_ENCODERS = new ConcurrentHashMap<>(); + + // Factory for default encoders by class + private static @Nullable Encoder encoderFactory(Class cls) { + if (cls.equals(PaneInfo.class)) { + return paneInfoEncoder(); + } else if (cls.equals(GlobalWindow.class)) { + return binaryEncoder(GlobalWindow.Coder.INSTANCE, false); + } else if (cls.equals(IntervalWindow.class)) { + return binaryEncoder(IntervalWindowCoder.of(), false); + } else if (cls.equals(Instant.class)) { + return instantEncoder(); + } else if (cls.equals(String.class)) { + return Encoders.STRING(); + } else if (cls.equals(Boolean.class)) { + return Encoders.BOOLEAN(); + } else if (cls.equals(Integer.class)) { + return Encoders.INT(); + } else if (cls.equals(Long.class)) { + return Encoders.LONG(); + } else if (cls.equals(Float.class)) { + return Encoders.FLOAT(); + } else if (cls.equals(Double.class)) { + return Encoders.DOUBLE(); + } else if (cls.equals(BigDecimal.class)) { + return Encoders.DECIMAL(); + } else if (cls.equals(byte[].class)) { + return Encoders.BINARY(); + } else if (cls.equals(Byte.class)) { + return Encoders.BYTE(); + } else if (cls.equals(Short.class)) { + return Encoders.SHORT(); + } + return null; + } + + @SuppressWarnings({"nullness", "methodref.return"}) // computeIfAbsent allows null returns + private static @Nullable Encoder getOrCreateDefaultEncoder(Class cls) { + return (Encoder) DEFAULT_ENCODERS.computeIfAbsent(cls, EncoderHelpers::encoderFactory); + } + + /** Gets or creates a default {@link Encoder} for {@link T}. */ + public static Encoder encoderOf(Class cls) { + Encoder enc = getOrCreateDefaultEncoder(cls); + if (enc == null) { + throw new IllegalArgumentException("No default encoder available for class " + cls); + } + return enc; + } + + /** + * Creates a Spark {@link Encoder} for {@link T} of {@link DataTypes#BinaryType BinaryType} + * delegating to a Beam {@link Coder} underneath. + * + *

    Note: For common types, if available, default Spark {@link Encoder}s are used instead. + * + * @param coder Beam {@link Coder} + */ + public static Encoder encoderFor(Coder coder) { + Encoder enc = getOrCreateDefaultEncoder(coder.getEncodedTypeDescriptor().getRawType()); + return enc != null ? enc : binaryEncoder(coder, true); + } + + /** + * Creates a Spark {@link Encoder} for {@link T} of {@link StructType} with fields {@code value}, + * {@code timestamp}, {@code window} and {@code pane}. + * + * @param value {@link Encoder} to encode field `{@code value}`. + * @param window {@link Encoder} to encode individual windows in field `{@code window}` + */ + public static Encoder> windowedValueEncoder( + Encoder value, Encoder window) { + Encoder timestamp = encoderOf(Instant.class); + Encoder paneInfo = encoderOf(PaneInfo.class); + Encoder> windows = collectionEncoder(window); + Expression serializer = + serializeWindowedValue(rootRef(WINDOWED_VALUE, true), value, timestamp, windows, paneInfo); + Expression deserializer = + deserializeWindowedValue( + rootCol(serializer.dataType()), value, timestamp, windows, paneInfo); + return EncoderFactory.create(serializer, deserializer, WindowedValue.class); + } + + /** + * Creates a one-of Spark {@link Encoder} of {@link StructType} where each alternative is + * represented as colum / field named by its index with a separate {@link Encoder} each. + * + *

    Externally this is represented as tuple {@code (index, data)} where an index corresponds to + * an {@link Encoder} in the provided list. + * + * @param encoders {@link Encoder}s for each alternative. + */ + public static Encoder> oneOfEncoder(List> encoders) { + Expression serializer = serializeOneOf(rootRef(TUPLE2_TYPE, true), encoders); + Expression deserializer = deserializeOneOf(rootCol(serializer.dataType()), encoders); + return EncoderFactory.create(serializer, deserializer, Tuple2.class); + } + + /** + * Creates a Spark {@link Encoder} for {@link KV} of {@link StructType} with fields {@code key} + * and {@code value}. + * + * @param key {@link Encoder} to encode field `{@code key}`. + * @param value {@link Encoder} to encode field `{@code value}` + */ + public static Encoder> kvEncoder(Encoder key, Encoder value) { + Expression serializer = serializeKV(rootRef(KV_TYPE, true), key, value); + Expression deserializer = deserializeKV(rootCol(serializer.dataType()), key, value); + return EncoderFactory.create(serializer, deserializer, KV.class); + } + + /** + * Creates a Spark {@link Encoder} of {@link ArrayType} for Java {@link Collection}s with nullable + * elements. + * + * @param enc {@link Encoder} to encode collection elements + */ + public static Encoder> collectionEncoder(Encoder enc) { + return collectionEncoder(enc, true); + } + + /** + * Creates a Spark {@link Encoder} of {@link ArrayType} for Java {@link Collection}s. + * + * @param enc {@link Encoder} to encode collection elements + * @param nullable Allow nullable collection elements + */ + public static Encoder> collectionEncoder(Encoder enc, boolean nullable) { + DataType type = new ObjectType(Collection.class); + Expression serializer = serializeSeq(rootRef(type, true), enc, nullable); + Expression deserializer = deserializeSeq(rootCol(serializer.dataType()), enc, nullable, true); + return EncoderFactory.create(serializer, deserializer, Collection.class); + } + + /** + * Creates a Spark {@link Encoder} of {@link MapType} that deserializes to {@link MapT}. + * + * @param key {@link Encoder} to encode keys + * @param value {@link Encoder} to encode values + * @param cls Specific class to use, supported are {@link HashMap} and {@link TreeMap} + */ + public static , K, V> Encoder mapEncoder( + Encoder key, Encoder value, Class cls) { + Expression serializer = mapSerializer(rootRef(new ObjectType(cls), true), key, value); + Expression deserializer = mapDeserializer(rootCol(serializer.dataType()), key, value, cls); + return EncoderFactory.create(serializer, deserializer, cls); + } + + /** + * Creates a Spark {@link Encoder} for Spark's {@link MutablePair} of {@link StructType} with + * fields `{@code _1}` and `{@code _2}`. + * + *

    This is intended to be used in places such as aggregators. + * + * @param enc1 {@link Encoder} to encode `{@code _1}` + * @param enc2 {@link Encoder} to encode `{@code _2}` + */ + public static Encoder> mutablePairEncoder( + Encoder enc1, Encoder enc2) { + Expression serializer = serializeMutablePair(rootRef(MUTABLE_PAIR_TYPE, true), enc1, enc2); + Expression deserializer = deserializeMutablePair(rootCol(serializer.dataType()), enc1, enc2); + return EncoderFactory.create(serializer, deserializer, MutablePair.class); + } + + /** + * Creates a Spark {@link Encoder} for {@link PaneInfo} of {@link DataTypes#BinaryType + * BinaryType}. + */ + private static Encoder paneInfoEncoder() { + DataType type = new ObjectType(PaneInfo.class); + return EncoderFactory.create( + invokeIfNotNull(Utils.class, "paneInfoToBytes", BinaryType, rootRef(type, false)), + invokeIfNotNull(Utils.class, "paneInfoFromBytes", type, rootCol(BinaryType)), + PaneInfo.class); + } + + /** + * Creates a Spark {@link Encoder} for Joda {@link Instant} of {@link DataTypes#LongType + * LongType}. + */ + private static Encoder instantEncoder() { + DataType type = new ObjectType(Instant.class); + Expression instant = rootRef(type, true); + Expression millis = rootCol(LongType); + return EncoderFactory.create( + nullSafe(instant, invoke(instant, "getMillis", LongType, false)), + nullSafe(millis, invoke(Instant.class, "ofEpochMilli", type, millis)), + Instant.class); + } + + /** + * Creates a Spark {@link Encoder} for {@link T} of {@link DataTypes#BinaryType BinaryType} + * delegating to a Beam {@link Coder} underneath. + * + * @param coder Beam {@link Coder} + * @param nullable If to allow nullable items + */ + private static Encoder binaryEncoder(Coder coder, boolean nullable) { + Literal litCoder = lit(coder, Coder.class); + // T could be private, use OBJECT_TYPE for code generation to not risk an IllegalAccessError + return EncoderFactory.create( + invokeIfNotNull( + CoderHelpers.class, + "toByteArray", + BinaryType, + rootRef(OBJECT_TYPE, nullable), + litCoder), + invokeIfNotNull( + CoderHelpers.class, "fromByteArray", OBJECT_TYPE, rootCol(BinaryType), litCoder), + coder.getEncodedTypeDescriptor().getRawType()); + } + + private static Expression serializeWindowedValue( + Expression in, + Encoder valueEnc, + Encoder timestampEnc, + Encoder> windowsEnc, + Encoder paneEnc) { + return serializerObject( + in, + tuple("value", serializeField(in, valueEnc, "getValue")), + tuple("timestamp", serializeField(in, timestampEnc, "getTimestamp")), + tuple("windows", serializeField(in, windowsEnc, "getWindows")), + tuple("paneInfo", serializeField(in, paneEnc, "getPaneInfo"))); + } + + private static Expression serializerObject(Expression in, Tuple2... fields) { + return SerializerBuildHelper.createSerializerForObject(in, seqOf(fields)); + } + + private static Expression deserializeWindowedValue( + Expression in, + Encoder valueEnc, + Encoder timestampEnc, + Encoder> windowsEnc, + Encoder paneEnc) { + Expression value = deserializeField(in, valueEnc, 0, "value"); + Expression windows = deserializeField(in, windowsEnc, 2, "windows"); + Expression timestamp = deserializeField(in, timestampEnc, 1, "timestamp"); + Expression paneInfo = deserializeField(in, paneEnc, 3, "paneInfo"); + // set timestamp to end of window (maxTimestamp) if null + timestamp = + ifNotNull(timestamp, invoke(Utils.class, "maxTimestamp", timestamp.dataType(), windows)); + Expression[] fields = new Expression[] {value, timestamp, windows, paneInfo}; + + return nullSafe(paneInfo, invoke(WindowedValues.class, "of", WINDOWED_VALUE, fields)); + } + + private static Expression serializeMutablePair( + Expression in, Encoder enc1, Encoder enc2) { + return serializerObject( + in, + tuple("_1", serializeField(in, enc1, "_1")), + tuple("_2", serializeField(in, enc2, "_2"))); + } + + private static Expression deserializeMutablePair( + Expression in, Encoder enc1, Encoder enc2) { + Expression field1 = deserializeField(in, enc1, 0, "_1"); + Expression field2 = deserializeField(in, enc2, 1, "_2"); + return invoke(MutablePair.class, "apply", MUTABLE_PAIR_TYPE, field1, field2); + } + + private static Expression serializeKV( + Expression in, Encoder keyEnc, Encoder valueEnc) { + return serializerObject( + in, + tuple("key", serializeField(in, keyEnc, "getKey")), + tuple("value", serializeField(in, valueEnc, "getValue"))); + } + + private static Expression deserializeKV( + Expression in, Encoder keyEnc, Encoder valueEnc) { + Expression key = deserializeField(in, keyEnc, 0, "key"); + Expression value = deserializeField(in, valueEnc, 1, "value"); + return invoke(KV.class, "of", KV_TYPE, key, value); + } + + public static Expression serializeOneOf(Expression in, List> encoders) { + Expression type = invoke(in, "_1", IntegerType, false); + Expression[] args = new Expression[encoders.size() * 2]; + for (int i = 0; i < encoders.size(); i++) { + args[i * 2] = lit(String.valueOf(i)); + args[i * 2 + 1] = serializeOneOfField(in, type, encoders.get(i), i); + } + return new CreateNamedStruct(seqOf(args)); + } + + public static Expression deserializeOneOf(Expression in, List> encoders) { + Expression[] args = new Expression[encoders.size()]; + for (int i = 0; i < encoders.size(); i++) { + args[i] = deserializeOneOfField(in, encoders.get(i), i); + } + return new Coalesce(seqOf(args)); + } + + private static Expression serializeOneOfField( + Expression in, Expression type, Encoder enc, int typeIdx) { + Expression litNull = lit(null, serializedType(enc)); + Expression value = invoke(in, "_2", deserializedType(enc), false); + return new If(new EqualTo(type, lit(typeIdx)), serialize(value, enc), litNull); + } + + private static Expression deserializeOneOfField(Expression in, Encoder enc, int idx) { + GetStructField field = new GetStructField(in, idx, Option.empty()); + Expression litNull = lit(null, TUPLE2_TYPE); + Expression newTuple = + EncoderFactory.newInstance(Tuple2.class, TUPLE2_TYPE, lit(idx), deserialize(field, enc)); + return new If(new IsNull(field), litNull, newTuple); + } + + private static Expression serializeField(Expression in, Encoder enc, String getterName) { + Expression ref = serializer(enc).collect(match(BoundReference.class)).head(); + return serialize(invoke(in, getterName, ref.dataType(), ref.nullable()), enc); + } + + private static Expression deserializeField( + Expression in, Encoder enc, int idx, String name) { + return deserialize(new GetStructField(in, idx, new Some<>(name)), enc); + } + + // Note: Currently this doesn't support nullable primitive values + private static Expression mapSerializer(Expression map, Encoder key, Encoder value) { + DataType keyType = deserializedType(key); + DataType valueType = deserializedType(value); + return SerializerBuildHelper.createSerializerForMap( + map, + new MapElementInformation(keyType, false, e -> serialize(e, key)), + new MapElementInformation(valueType, false, e -> serialize(e, value))); + } + + private static , K, V> Expression mapDeserializer( + Expression in, Encoder key, Encoder value, Class cls) { + Preconditions.checkArgument(cls.isAssignableFrom(HashMap.class) || cls.equals(TreeMap.class)); + Expression keys = deserializeSeq(new MapKeys(in), key, false, false); + Expression values = deserializeSeq(new MapValues(in), value, false, false); + String fn = cls.equals(TreeMap.class) ? "toTreeMap" : "toMap"; + return invoke( + Utils.class, fn, new ObjectType(cls), keys, values, mapItemType(key), mapItemType(value)); + } + + // serialized type for primitive types (avoid boxing!), otherwise the deserialized type + private static Literal mapItemType(Encoder enc) { + return lit(isPrimitiveEnc(enc) ? serializedType(enc) : deserializedType(enc), DataType.class); + } + + private static Expression serializeSeq(Expression in, Encoder enc, boolean nullable) { + if (isPrimitiveEnc(enc)) { + Expression array = invoke(in, "toArray", new ObjectType(Object[].class), false); + return SerializerBuildHelper.createSerializerForGenericArray( + array, serializedType(enc), nullable); + } + Expression seq = invoke(Utils.class, "toSeq", new ObjectType(Seq.class), in); + return MapObjects$.MODULE$.apply( + exp -> serialize(exp, enc), seq, deserializedType(enc), nullable, Option.empty()); + } + + private static Expression deserializeSeq( + Expression in, Encoder enc, boolean nullable, boolean exposeAsJava) { + DataType type = serializedType(enc); // input type is the serializer result type + if (isPrimitiveEnc(enc)) { + // Spark may reuse unsafe array data, if directly exposed it must be copied before + return exposeAsJava + ? invoke(Utils.class, "copyToList", LIST_TYPE, in, lit(type, DataType.class)) + : in; + } + Option> optCls = exposeAsJava ? Option.apply(List.class) : Option.empty(); + // MapObjects will always copy + return MapObjects$.MODULE$.apply(exp -> deserialize(exp, enc), in, type, nullable, optCls); + } + + private static boolean isPrimitiveEnc(Encoder enc) { + return PRIMITIVE_TYPES.contains(enc.clsTag().runtimeClass()); + } + + private static Expression serialize(Expression input, Encoder enc) { + return serializer(enc).transformUp(replace(BoundReference.class, input)); + } + + private static Expression deserialize(Expression input, Encoder enc) { + return deserializer(enc).transformUp(replace(GetColumnByOrdinal.class, input)); + } + + /** + * Wraps an {@link Encoder} as an {@link ExpressionEncoder}. In Spark 4.x, built-in encoders (e.g. + * {@code Encoders.INT()}) are {@link AgnosticEncoder} subclasses rather than {@link + * ExpressionEncoder}s, so we convert them on demand. + */ + @SuppressWarnings("unchecked") + private static ExpressionEncoder toExpressionEncoder(Encoder enc) { + if (enc instanceof ExpressionEncoder) { + return (ExpressionEncoder) enc; + } else if (enc instanceof AgnosticEncoder) { + return ExpressionEncoder.apply((AgnosticEncoder) enc); + } + throw new IllegalArgumentException("Unsupported encoder type: " + enc.getClass()); + } + + private static Expression serializer(Encoder enc) { + return toExpressionEncoder(enc).objSerializer(); + } + + private static Expression deserializer(Encoder enc) { + return toExpressionEncoder(enc).objDeserializer(); + } + + private static DataType serializedType(Encoder enc) { + return toExpressionEncoder(enc).objSerializer().dataType(); + } + + private static DataType deserializedType(Encoder enc) { + return toExpressionEncoder(enc).objDeserializer().dataType(); + } + + private static Expression rootRef(DataType dt, boolean nullable) { + return new BoundReference(0, dt, nullable); + } + + private static Expression rootCol(DataType dt) { + return new GetColumnByOrdinal(0, dt); + } + + private static Expression nullSafe(Expression in, Expression out) { + return new If(new IsNull(in), lit(null, out.dataType()), out); + } + + private static Expression ifNotNull(Expression expr, Expression otherwise) { + return new If(new IsNotNull(expr), expr, otherwise); + } + + private static Expression lit(T t) { + return Literal$.MODULE$.apply(t); + } + + @SuppressWarnings("nullness") // literal NULL is allowed + private static Expression lit(@Nullable T t, DataType dataType) { + return new Literal(t, dataType); + } + + private static Literal lit(T obj, Class cls) { + return Literal.fromObject(obj, new ObjectType(cls)); + } + + /** Encoder / expression utils that are called from generated code. */ + public static class Utils { + + public static PaneInfo paneInfoFromBytes(byte[] bytes) { + return CoderHelpers.fromByteArray(bytes, PaneInfoCoder.of()); + } + + public static byte[] paneInfoToBytes(PaneInfo paneInfo) { + return CoderHelpers.toByteArray(paneInfo, PaneInfoCoder.of()); + } + + /** The maximum {@code maxTimestamp} across all associated windows. */ + public static Instant maxTimestamp(Iterable windows) { + Instant maxTimestamp = null; + for (BoundedWindow window : windows) { + Instant timestamp = window.maxTimestamp(); + if (maxTimestamp == null || timestamp.isAfter(maxTimestamp)) { + maxTimestamp = timestamp; + } + } + return Preconditions.checkNotNull( + maxTimestamp, "WindowedValue must have at least one window"); + } + + public static List copyToList(ArrayData arrayData, DataType type) { + // Note, this could be optimized for primitive arrays (if elements are not nullable) using + // Ints.asList(arrayData.toIntArray()) and similar + return Arrays.asList(arrayData.toObjectArray(type)); + } + + public static Seq toSeq(ArrayData arrayData) { + return arrayData.toSeq(OBJECT_TYPE); + } + + public static Seq toSeq(Collection col) { + if (col instanceof List) { + return JavaConverters.asScalaBuffer((List) col); + } + return JavaConverters.collectionAsScalaIterable(col).toSeq(); + } + + public static TreeMap toTreeMap( + ArrayData keys, ArrayData values, DataType keyType, DataType valueType) { + return toMap(new TreeMap<>(), keys, values, keyType, valueType); + } + + public static HashMap toMap( + ArrayData keys, ArrayData values, DataType keyType, DataType valueType) { + HashMap map = Maps.newHashMapWithExpectedSize(keys.numElements()); + return toMap(map, keys, values, keyType, valueType); + } + + private static > MapT toMap( + MapT map, ArrayData keys, ArrayData values, DataType keyType, DataType valueType) { + IndexedSeq keysSeq = keys.toSeq(keyType); + IndexedSeq valuesSeq = values.toSeq(valueType); + for (int i = 0; i < keysSeq.size(); i++) { + map.put(keysSeq.apply(i), valuesSeq.apply(i)); + } + return map; + } + } +} diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/utils/ScalaInterop.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/utils/ScalaInterop.java new file mode 100644 index 000000000000..175e144d6506 --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/utils/ScalaInterop.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.utils; + +import java.io.Serializable; +import org.checkerframework.checker.nullness.qual.NonNull; +import scala.Function1; +import scala.Function2; +import scala.PartialFunction; +import scala.Tuple2; +import scala.collection.Iterator; +import scala.collection.JavaConverters; +import scala.collection.Seq; +import scala.collection.immutable.List; +import scala.collection.immutable.Nil$; + +/** Utilities for easier interoperability with the Spark Scala API. */ +public class ScalaInterop { + private ScalaInterop() {} + + public static scala.collection.immutable.Seq seqOf(T... t) { + return (scala.collection.immutable.Seq) + JavaConverters.asScalaBuffer(java.util.Arrays.asList(t)).toList(); + } + + public static List concat(List a, List b) { + return b.$colon$colon$colon(a); + } + + public static Seq listOf(T t) { + return emptyList().$colon$colon(t); + } + + public static List emptyList() { + return (List) Nil$.MODULE$; + } + + /** Scala {@link Iterator} of Java {@link Iterable}. */ + public static Iterator scalaIterator(Iterable iterable) { + return scalaIterator(iterable.iterator()); + } + + /** Scala {@link Iterator} of Java {@link java.util.Iterator}. */ + public static Iterator scalaIterator(java.util.Iterator it) { + return JavaConverters.asScalaIterator(it); + } + + /** Java {@link java.util.Iterator} of Scala {@link Iterator}. */ + public static java.util.Iterator javaIterator(Iterator it) { + return JavaConverters.asJavaIterator(it); + } + + public static Tuple2 tuple(T1 t1, T2 t2) { + return new Tuple2<>(t1, t2); + } + + public static PartialFunction replace( + Class clazz, T replace) { + return new PartialFunction() { + + @Override + public boolean isDefinedAt(T x) { + return clazz.isAssignableFrom(x.getClass()); + } + + @Override + public T apply(T x) { + return replace; + } + }; + } + + public static PartialFunction match(Class clazz) { + return new PartialFunction() { + + @Override + public boolean isDefinedAt(T x) { + return clazz.isAssignableFrom(x.getClass()); + } + + @Override + public V apply(T x) { + return (V) x; + } + }; + } + + public static Fun1 fun1(Fun1 fun) { + return fun; + } + + public static Fun2 fun2(Fun2 fun) { + return fun; + } + + public interface Fun1 extends Function1, Serializable {} + + public interface Fun2 extends Function2, Serializable {} +} diff --git a/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpersTest.java b/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpersTest.java new file mode 100644 index 000000000000..48c1e645f6ec --- /dev/null +++ b/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpersTest.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.helpers; + +import static java.util.Arrays.asList; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.collectionEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.encoderFor; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.kvEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.mapEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.oneOfEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.helpers.EncoderHelpers.windowedValueEncoder; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.tuple; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates.notNull; +import static org.apache.spark.sql.types.DataTypes.IntegerType; +import static org.apache.spark.sql.types.DataTypes.StringType; +import static org.apache.spark.sql.types.DataTypes.createStructField; +import static org.apache.spark.sql.types.DataTypes.createStructType; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Function; +import org.apache.beam.runners.spark.structuredstreaming.SparkSessionRule; +import org.apache.beam.sdk.coders.BigDecimalCoder; +import org.apache.beam.sdk.coders.BigEndianIntegerCoder; +import org.apache.beam.sdk.coders.BigEndianLongCoder; +import org.apache.beam.sdk.coders.BigEndianShortCoder; +import org.apache.beam.sdk.coders.BooleanCoder; +import org.apache.beam.sdk.coders.ByteCoder; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.DelegateCoder; +import org.apache.beam.sdk.coders.DoubleCoder; +import org.apache.beam.sdk.coders.FloatCoder; +import org.apache.beam.sdk.coders.InstantCoder; +import org.apache.beam.sdk.coders.ListCoder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.coders.VarIntCoder; +import org.apache.beam.sdk.coders.VarLongCoder; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.TypeDescriptor; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.catalyst.InternalRow; +import org.apache.spark.sql.catalyst.encoders.AgnosticEncoder; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder; +import org.apache.spark.sql.types.DecimalType; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.joda.time.Instant; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import scala.Tuple2; + +/** Test of the wrapping of Beam Coders as Spark ExpressionEncoders. */ +@RunWith(JUnit4.class) +public class EncoderHelpersTest { + @ClassRule public static SparkSessionRule sessionRule = new SparkSessionRule("local[1]"); + + private static final Encoder windowEnc = + EncoderHelpers.encoderOf(GlobalWindow.class); + + private static final Map, List> BASIC_CASES = + ImmutableMap., List>builder() + .put(BooleanCoder.of(), asList(true, false, null)) + .put(ByteCoder.of(), asList((byte) 1, null)) + .put(BigEndianShortCoder.of(), asList((short) 1, null)) + .put(BigEndianIntegerCoder.of(), asList(1, 2, 3, null)) + .put(VarIntCoder.of(), asList(1, 2, 3, null)) + .put(BigEndianLongCoder.of(), asList(1L, 2L, 3L, null)) + .put(VarLongCoder.of(), asList(1L, 2L, 3L, null)) + .put(FloatCoder.of(), asList((float) 1.0, (float) 2.0, null)) + .put(DoubleCoder.of(), asList(1.0, 2.0, null)) + .put(StringUtf8Coder.of(), asList("1", "2", null)) + .put(BigDecimalCoder.of(), asList(bigDecimalOf(1L), bigDecimalOf(2L), null)) + .put(InstantCoder.of(), asList(Instant.ofEpochMilli(1), null)) + .build(); + + private Dataset createDataset(List data, Encoder encoder) { + Dataset ds = sessionRule.getSession().createDataset(data, encoder); + ds.printSchema(); + return ds; + } + + @Test + public void testBeamEncoderMappings() { + BASIC_CASES.forEach( + (coder, data) -> { + Encoder encoder = encoderFor(coder); + serializeAndDeserialize(data.get(0), (Encoder) encoder); + Dataset dataset = createDataset(data, (Encoder) encoder); + assertThat(dataset.collect(), equalTo(data.toArray())); + }); + } + + @Test + public void testBeamEncoderOfPrivateType() { + // Verify concrete types are not used in coder generation. + // In case of private types this would cause an IllegalAccessError. + List data = asList(new PrivateString("1"), new PrivateString("2")); + Dataset dataset = createDataset(data, encoderFor(PrivateString.CODER)); + assertThat(dataset.collect(), equalTo(data.toArray())); + } + + @Test + public void testBeamWindowedValueEncoderMappings() { + BASIC_CASES.forEach( + (coder, data) -> { + List> windowed = + Lists.transform(data, WindowedValues::valueInGlobalWindow); + + Encoder encoder = windowedValueEncoder(encoderFor(coder), windowEnc); + serializeAndDeserialize(windowed.get(0), (Encoder) encoder); + + Dataset dataset = createDataset(windowed, (Encoder) encoder); + assertThat(dataset.collect(), equalTo(windowed.toArray())); + }); + } + + @Test + public void testCollectionEncoder() { + BASIC_CASES.forEach( + (coder, data) -> { + Encoder> encoder = collectionEncoder(encoderFor(coder), true); + Collection collection = Collections.unmodifiableCollection(data); + + Dataset> dataset = createDataset(asList(collection), (Encoder) encoder); + assertThat(dataset.head(), equalTo(data)); + }); + } + + private void testMapEncoder(Class cls, Function, Map> decorator) { + BASIC_CASES.forEach( + (coder, data) -> { + Encoder enc = encoderFor(coder); + Encoder> mapEncoder = mapEncoder(enc, enc, (Class) cls); + Map map = + decorator.apply( + data.stream().filter(notNull()).collect(toMap(identity(), identity()))); + + Dataset> dataset = createDataset(asList(map), mapEncoder); + Map head = dataset.head(); + assertThat(head, equalTo(map)); + assertThat(head, instanceOf(cls)); + }); + } + + @Test + public void testMapEncoder() { + testMapEncoder(Map.class, identity()); + } + + @Test + public void testHashMapEncoder() { + testMapEncoder(HashMap.class, identity()); + } + + @Test + public void testTreeMapEncoder() { + testMapEncoder(TreeMap.class, TreeMap::new); + } + + @Test + public void testBeamBinaryEncoder() { + List> data = asList(asList("a1", "a2", "a3"), asList("b1", "b2"), asList("c1")); + + Encoder> encoder = encoderFor(ListCoder.of(StringUtf8Coder.of())); + serializeAndDeserialize(data.get(0), encoder); + + Dataset> dataset = createDataset(data, encoder); + assertThat(dataset.collect(), equalTo(data.toArray())); + } + + @Test + public void testEncoderForKVCoder() { + List> data = + asList(KV.of(1, "value1"), KV.of(null, "value2"), KV.of(3, null)); + + Encoder> encoder = + kvEncoder(encoderFor(VarIntCoder.of()), encoderFor(StringUtf8Coder.of())); + serializeAndDeserialize(data.get(0), encoder); + + Dataset> dataset = createDataset(data, encoder); + + StructType kvSchema = + createStructType( + new StructField[] { + createStructField("key", IntegerType, true), + createStructField("value", StringType, true) + }); + + assertThat(dataset.schema(), equalTo(kvSchema)); + assertThat(dataset.collectAsList(), equalTo(data)); + } + + @Test + public void testOneOffEncoder() { + List> coders = ImmutableList.copyOf(BASIC_CASES.keySet()); + List> encoders = coders.stream().map(EncoderHelpers::encoderFor).collect(toList()); + + // build oneOf tuples of type index and corresponding value + List> data = + BASIC_CASES.entrySet().stream() + .map(e -> tuple(coders.indexOf(e.getKey()), (Object) e.getValue().get(0))) + .collect(toList()); + + // dataset is a sparse dataset with only one column set per row + Dataset> dataset = createDataset(data, oneOfEncoder((List) encoders)); + assertThat(dataset.collectAsList(), equalTo(data)); + } + + // fix scale/precision to system default to compare using equals + private static BigDecimal bigDecimalOf(long l) { + DecimalType type = DecimalType.SYSTEM_DEFAULT(); + return new BigDecimal(l, new MathContext(type.precision())).setScale(type.scale()); + } + + // test and explicit serialization roundtrip + @SuppressWarnings("unchecked") + private static void serializeAndDeserialize(T data, Encoder enc) { + ExpressionEncoder bound; + if (enc instanceof ExpressionEncoder) { + bound = (ExpressionEncoder) enc; + } else { + bound = ExpressionEncoder.apply((AgnosticEncoder) enc); + } + bound = + bound.resolveAndBind(bound.resolveAndBind$default$1(), bound.resolveAndBind$default$2()); + + InternalRow row = bound.createSerializer().apply(data); + T deserialized = bound.createDeserializer().apply(row); + + assertThat(deserialized, equalTo(data)); + } + + private static class PrivateString { + private static final Coder CODER = + DelegateCoder.of( + StringUtf8Coder.of(), + str -> str.string, + PrivateString::new, + new TypeDescriptor() {}); + + private final String string; + + public PrivateString(String string) { + this.string = string; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PrivateString)) { + return false; + } + PrivateString that = (PrivateString) o; + return Objects.equals(string, that.string); + } + + @Override + public int hashCode() { + return Objects.hash(string); + } + } +} diff --git a/runners/spark/spark_runner.gradle b/runners/spark/spark_runner.gradle index 9d7be01e8825..cd108372fea5 100644 --- a/runners/spark/spark_runner.gradle +++ b/runners/spark/spark_runner.gradle @@ -257,12 +257,23 @@ dependencies { implementation "org.apache.spark:spark-common-utils_$spark_scala_version:$spark_version" implementation "org.apache.spark:spark-sql-api_$spark_scala_version:$spark_version" } + if (isSparkAtLeast("4.0.0")) { + // Spark 4 splits the Connect shims out of spark-sql; classes are referenced directly + // (e.g. via SparkSession builder paths) so strict dependency analysis requires it as + // a declared provided dep. The artifact does not exist for Spark 3. + provided "org.apache.spark:spark-connect-shims_$spark_scala_version:$spark_version" + } permitUnusedDeclared "org.apache.spark:spark-network-common_$spark_scala_version:$spark_version" implementation "io.dropwizard.metrics:metrics-core:4.1.1" // version used by Spark 3.1 - compileOnly "org.scala-lang:scala-library:2.12.15" - runtimeOnly library.java.jackson_module_scala_2_12 - // Force paranamer 2.8 to avoid issues when using Scala 2.12 - runtimeOnly "com.thoughtworks.paranamer:paranamer:2.8" + if (spark_scala_version == '2.13') { + compileOnly "org.scala-lang:scala-library:2.13.15" + runtimeOnly library.java.jackson_module_scala_2_13 + } else { + compileOnly "org.scala-lang:scala-library:2.12.15" + runtimeOnly library.java.jackson_module_scala_2_12 + // Force paranamer 2.8 to avoid issues when using Scala 2.12 + runtimeOnly "com.thoughtworks.paranamer:paranamer:2.8" + } provided "org.apache.hadoop:hadoop-client-api:3.3.1" provided library.java.commons_io provided library.java.hamcrest @@ -277,7 +288,9 @@ dependencies { testImplementation project(path: ":sdks:java:extensions:avro", configuration: "testRuntimeMigration") testImplementation project(":sdks:java:harness") testImplementation library.java.avro - testImplementation "org.apache.kafka:kafka_$spark_scala_version:2.4.1" + // kafka_2.13 artifacts were first published in 2.5.0; use a later version for Scala 2.13 + def kafka_version = (spark_scala_version == '2.13') ? '2.8.0' : '2.4.1' + testImplementation "org.apache.kafka:kafka_$spark_scala_version:$kafka_version" testImplementation library.java.kafka_clients testImplementation library.java.junit testImplementation library.java.mockito_core diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistrator.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistrator.java index 68c602ff7f59..75c33b7dc5b5 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistrator.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistrator.java @@ -28,9 +28,10 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.HashBasedTable; import org.apache.spark.serializer.KryoRegistrator; -import scala.collection.mutable.WrappedArray; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Custom {@link KryoRegistrator}s for Beam's Spark runner needs and registering used class in spark @@ -61,7 +62,18 @@ public void registerClasses(Kryo kryo) { kryo.register(PaneInfo.class); kryo.register(StateAndTimers.class); kryo.register(TupleTag.class); - kryo.register(WrappedArray.ofRef.class); + // Scala 2.12 uses WrappedArray$ofRef, Scala 2.13 renamed it to ArraySeq$ofRef + Class scalaArrayClass = + findFirstAvailableClass( + "scala.collection.mutable.ArraySeq$ofRef", + "scala.collection.mutable.WrappedArray$ofRef"); + if (scalaArrayClass == null) { + throw new IllegalStateException( + "Neither scala.collection.mutable.ArraySeq$ofRef (Scala 2.13) nor " + + "scala.collection.mutable.WrappedArray$ofRef (Scala 2.12) was found on the " + + "classpath. Cannot register Scala wrapped arrays with Kryo."); + } + kryo.register(scalaArrayClass); try { kryo.register( @@ -74,4 +86,16 @@ public void registerClasses(Kryo kryo) { throw new IllegalStateException("Unable to register classes with kryo.", e); } } + + @VisibleForTesting + static @Nullable Class findFirstAvailableClass(String... classNames) { + for (String name : classNames) { + try { + return Class.forName(name); + } catch (ClassNotFoundException ignored) { + // try the next candidate + } + } + return null; + } } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java index 1c7a4c2a2416..be69ee78e51c 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/stateful/SparkGroupAlsoByWindowViaWindowSet.java @@ -522,7 +522,9 @@ JavaDStream>>> groupByKeyAndWindow( Tuple2>>*/ List>>> firedStream = pairDStream.updateStateByKey( - updateFunc, + // Raw cast to AbstractFunction1 suppresses Scala 2.12 (collection.Seq) vs + // Scala 2.13 (immutable.Seq) type difference — safe at runtime due to erasure. + (scala.runtime.AbstractFunction1) updateFunc, pairDStream.defaultPartitioner(pairDStream.defaultPartitioner$default$1()), true, JavaSparkContext$.MODULE$.fakeClassTag()); diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java index 806d838d9bff..9d3419e19473 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java @@ -113,6 +113,7 @@ public MetricResults metrics() { @Override public PipelineResult.State cancel() throws IOException { + pipelineExecution.cancel(true); offerNewState(PipelineResult.State.CANCELLED); return state; } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java index c0d46e77c1d6..d7cdefc929b7 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java @@ -241,7 +241,7 @@ static List> partitionsOf(BoundedSource source, Params try { PipelineOptions options = params.options.get(); long desiredSize = source.getEstimatedSizeBytes(options) / params.numPartitions; - List> split = (List>) source.split(desiredSize, options); + List> split = source.split(desiredSize, options); IntSupplier idxSupplier = new AtomicInteger(0)::getAndIncrement; return split.stream().map(s -> new SourcePartition<>(s, idxSupplier)).collect(toList()); } catch (Exception e) { diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContext.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContext.java index 55c4bbaedd3c..b8448567eafc 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContext.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContext.java @@ -86,7 +86,10 @@ public static void evaluate(String name, Dataset ds) { ds.write().mode("overwrite").format("noop").save(); LOG.info("Evaluated dataset {} in {}", name, durationSince(startMs)); } catch (RuntimeException e) { - LOG.error("Failed to evaluate dataset {}: {}", name, Throwables.getRootCause(e).getMessage()); + LOG.error( + "Failed to evaluate dataset {}: {}", + name, + String.valueOf(Throwables.getRootCause(e).getMessage())); throw new RuntimeException(e); } } @@ -102,7 +105,10 @@ public static void evaluate(String name, Dataset ds) { LOG.info("Collected dataset {} in {} [size: {}]", name, durationSince(startMs), res.length); return res; } catch (Exception e) { - LOG.error("Failed to collect dataset {}: {}", name, Throwables.getRootCause(e).getMessage()); + LOG.error( + "Failed to collect dataset {}: {}", + name, + String.valueOf(Throwables.getRootCause(e).getMessage())); throw new RuntimeException(e); } } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java index 02c56a8081cf..513ef28a5897 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombinePerKeyTranslatorBatch.java @@ -64,7 +64,7 @@ * TODOs: *
  • combine with context (CombineFnWithContext)? *
  • combine with sideInputs? - *
  • other there other missing features? + *
  • are there other missing features? */ class CombinePerKeyTranslatorBatch extends TransformTranslator< diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java index b7c139068d1b..2105fd05d493 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyHelpers.java @@ -21,7 +21,6 @@ import static org.apache.beam.sdk.transforms.windowing.PaneInfo.NO_FIRING; import static org.apache.beam.sdk.transforms.windowing.TimestampCombiner.END_OF_WINDOW; -import java.util.Collection; import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop; import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.Fun1; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; @@ -80,8 +79,7 @@ static boolean eligibleForGlobalGroupBy( return v -> { T value = valueFn.apply(v); K key = v.getValue().getKey(); - Collection windows = (Collection) v.getWindows(); - return ScalaInterop.scalaIterator(windows).map(w -> tuple(tuple(w, key), value)); + return ScalaInterop.scalaIterator(v.getWindows()).map(w -> tuple(tuple(w, key), value)); }; } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java index 46cda3334822..c55781ff84a8 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/GroupByKeyTranslatorBatch.java @@ -81,7 +81,7 @@ import scala.collection.immutable.List; /** - * Translator for {@link GroupByKey} using {@link Dataset#groupByKey} with the build-in aggregation + * Translator for {@link GroupByKey} using {@link Dataset#groupByKey} with the built-in aggregation * function {@code collect_list} when applicable. * *

    Note: Using {@code collect_list} isn't any worse than using {@link ReduceFnRunner}. In the diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java index daf8451faac5..29f01c84c02e 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java @@ -48,7 +48,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.apache.spark.sql.Encoder; import org.apache.spark.sql.Encoders; @@ -98,7 +97,7 @@ public class EncoderHelpers { private static final DataType LIST_TYPE = new ObjectType(List.class); // Collections / maps of these types can be (de)serialized without (de)serializing each member - private static final Set> PRIMITIV_TYPES = + private static final Set> PRIMITIVE_TYPES = ImmutableSet.of( Boolean.class, Byte.class, @@ -154,7 +153,7 @@ public class EncoderHelpers { public static Encoder encoderOf(Class cls) { Encoder enc = getOrCreateDefaultEncoder(cls); if (enc == null) { - throw new IllegalArgumentException("No default coder available for class " + cls); + throw new IllegalArgumentException("No default encoder available for class " + cls); } return enc; } @@ -481,7 +480,7 @@ private static Expression deserializeSeq( } private static boolean isPrimitiveEnc(Encoder enc) { - return PRIMITIV_TYPES.contains(enc.clsTag().runtimeClass()); + return PRIMITIVE_TYPES.contains(enc.clsTag().runtimeClass()); } private static Expression serialize(Expression input, Encoder enc) { @@ -548,9 +547,17 @@ public static byte[] paneInfoToBytes(PaneInfo paneInfo) { return CoderHelpers.toByteArray(paneInfo, PaneInfoCoder.of()); } - /** The end of the only window (max timestamp). */ - public static Instant maxTimestamp(Iterable windows) { - return Iterables.getOnlyElement(windows).maxTimestamp(); + /** The maximum {@code maxTimestamp} across all associated windows. */ + public static Instant maxTimestamp(Iterable windows) { + Instant maxTimestamp = null; + for (BoundedWindow window : windows) { + Instant timestamp = window.maxTimestamp(); + if (maxTimestamp == null || timestamp.isAfter(maxTimestamp)) { + maxTimestamp = timestamp; + } + } + return Preconditions.checkNotNull( + maxTimestamp, "WindowedValue must have at least one window"); } public static List copyToList(ArrayData arrayData, DataType type) { diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java index ddd0e74d1c9e..56eb0dfea5f2 100644 --- a/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java @@ -17,7 +17,10 @@ */ package org.apache.beam.runners.spark.coders; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import com.esotericsoftware.kryo.Kryo; @@ -73,6 +76,52 @@ public void testDefaultSerializerNotCallingKryo() { } } + /** Unit tests for the {@link SparkRunnerKryoRegistrator#findFirstAvailableClass} helper. */ + public static class FindFirstAvailableClassTest { + + @Test + public void returnsFirstWhenAvailable() { + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass( + "java.lang.String", "java.lang.Integer"); + assertSame(String.class, result); + } + + @Test + public void fallsBackWhenFirstMissing() { + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass("does.not.Exist", "java.lang.Integer"); + assertSame(Integer.class, result); + } + + @Test + public void returnsNullWhenNoneAvailable() { + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass("does.not.Exist1", "does.not.Exist2"); + assertNull(result); + } + + @Test + public void returnsNullForEmptyInput() { + assertNull(SparkRunnerKryoRegistrator.findFirstAvailableClass()); + } + + @Test + public void resolvesScalaWrappedArrayClassOnRealClasspath() { + // On any supported Scala version (2.12 ArraySeq$ofRef does not exist; 2.13 it does), at + // least one of the two wrapped-array class names must resolve. This is the production call + // the registrator makes. + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass( + "scala.collection.mutable.ArraySeq$ofRef", + "scala.collection.mutable.WrappedArray$ofRef"); + assertEquals( + "expected one of the Scala wrapped-array classes to be on the classpath", + true, + result != null); + } + } + // Hide TestKryoRegistrator from the Enclosed JUnit runner interface Others { class TestKryoRegistrator extends SparkRunnerKryoRegistrator { diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java new file mode 100644 index 000000000000..d1669e45c182 --- /dev/null +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import org.apache.spark.sql.Dataset; +import org.junit.Test; + +/** + * Unit tests for the static error-path branches of {@link EvaluationContext}. The happy-path + * branches are covered end-to-end by the structured-streaming translation tests; these tests + * specifically exercise the {@code catch} blocks that wrap and rethrow underlying Spark failures. + */ +public class EvaluationContextTest { + + @Test + public void evaluateWrapsAndRethrowsRuntimeException() { + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException("boom"); + doThrow(underlying).when(ds).write(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.evaluate("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } + + @Test + public void evaluateHandlesNullExceptionMessage() { + // Reproduces the original NPE motivation for the String.valueOf wrap: a RuntimeException + // whose root cause carries a null message must not crash the error logger. + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException((String) null); + doThrow(underlying).when(ds).write(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.evaluate("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } + + @Test + public void collectWrapsAndRethrowsException() { + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException("boom"); + doThrow(underlying).when(ds).collect(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.collect("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } + + @Test + public void collectHandlesNullExceptionMessage() { + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException((String) null); + doThrow(underlying).when(ds).collect(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.collect("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 60ff1cf8ce1b..fc5f40c23d17 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -150,6 +150,8 @@ include(":runners:prism:java") include(":runners:spark:3") include(":runners:spark:3:job-server") include(":runners:spark:3:job-server:container") +include(":runners:spark:4") +include(":runners:spark:4:job-server") include(":sdks:go") include(":sdks:go:container") include(":sdks:go:examples") From ff1e17fa977c3fb6576bc63078dd117f98cfac1e Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 11 May 2026 16:21:05 -0400 Subject: [PATCH 134/490] Fix thread leak for LOOPBACK workers in external worker pool (#38432) * Fix thread leak for LOOPBACK workers in external worker pool. * Fix lints. * Add comments. * Fix lints. * Address review comments. --- .../runners/portability/prism_runner_test.py | 62 +++++++++++++++++++ .../runners/worker/worker_pool_main.py | 11 ++++ 2 files changed, 73 insertions(+) diff --git a/sdks/python/apache_beam/runners/portability/prism_runner_test.py b/sdks/python/apache_beam/runners/portability/prism_runner_test.py index a65f9a9960b4..9c1620603fd3 100644 --- a/sdks/python/apache_beam/runners/portability/prism_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/prism_runner_test.py @@ -19,7 +19,10 @@ import argparse import logging import os.path +import queue import shlex +import threading +import time import typing import unittest import zipfile @@ -37,8 +40,10 @@ from apache_beam.options.pipeline_options import PortableOptions from apache_beam.options.pipeline_options import StandardOptions from apache_beam.options.pipeline_options import TypeOptions +from apache_beam.portability.api import beam_fn_api_pb2 from apache_beam.runners.portability import portable_runner_test from apache_beam.runners.portability import prism_runner +from apache_beam.runners.worker import worker_pool_main from apache_beam.testing.util import assert_that from apache_beam.testing.util import equal_to from apache_beam.transforms import trigger @@ -488,6 +493,63 @@ def test_singleton(self, enable_singleton): else: mock_prism_server.assert_called_once() + def test_loopback_worker_daemon_thread_accumulation(self): + """Verifies that in LOOPBACK mode, the external worker pool servicer properly + tracks active thread-based SdkHarness workers and cleanly shuts them down in + StopWorker via sentinel messages. This prevents background daemon threads from + accumulating across sequential pipeline executions and leaking resources. + """ + servicer = worker_pool_main.BeamFnExternalWorkerPoolServicer( + use_process=False, state_cache_size=0, data_buffer_time_limit_ms=0) + + active_workers = [] + mock_responses = queue.Queue() + + def mock_run(self_worker): + active_workers.append(self_worker) + mock_responses.get() + active_workers.remove(self_worker) + + def wait_for_workers(expected_count, timeout=5.0): + start = time.time() + while time.time() - start < timeout: + if len(active_workers) == expected_count: + return + time.sleep(0.01) + self.assertEqual(len(active_workers), expected_count) + + with mock.patch( + 'apache_beam.runners.worker.sdk_worker.SdkHarness') as mock_harness: + mock_harness.return_value._responses = mock_responses + mock_harness.return_value.run = lambda: mock_run(mock_harness) + + # Simulate starting Worker 1 for Pipeline 1 + req1 = beam_fn_api_pb2.StartWorkerRequest(worker_id="worker_1") + req1.control_endpoint.url = "localhost:12345" + servicer.StartWorker(req1, None) + + wait_for_workers(1) + + # Simulate stopping Worker 1 at the end of Pipeline 1 + stop_req1 = beam_fn_api_pb2.StopWorkerRequest(worker_id="worker_1") + servicer.StopWorker(stop_req1, None) + + # Verify the fix: StopWorker successfully tells the thread harness to shut down, + # completely resolving the thread leak! + wait_for_workers(0) + + # Simulate starting Worker 2 for Pipeline 2 + req2 = beam_fn_api_pb2.StartWorkerRequest(worker_id="worker_2") + req2.control_endpoint.url = "localhost:12345" + servicer.StartWorker(req2, None) + + wait_for_workers(1) + + # Clean up the second worker + servicer.StopWorker( + beam_fn_api_pb2.StopWorkerRequest(worker_id="worker_2"), None) + wait_for_workers(0) + if __name__ == '__main__': # Run the tests. diff --git a/sdks/python/apache_beam/runners/worker/worker_pool_main.py b/sdks/python/apache_beam/runners/worker/worker_pool_main.py index 425a9fc57752..efe927b729c1 100644 --- a/sdks/python/apache_beam/runners/worker/worker_pool_main.py +++ b/sdks/python/apache_beam/runners/worker/worker_pool_main.py @@ -45,6 +45,7 @@ from apache_beam.portability.api import beam_fn_api_pb2_grpc from apache_beam.runners.worker import sdk_worker from apache_beam.utils import thread_pool_executor +from apache_beam.utils.sentinel import Sentinel _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,7 @@ def __init__( self._state_cache_size = state_cache_size self._data_buffer_time_limit_ms = data_buffer_time_limit_ms self._worker_processes: dict[str, subprocess.Popen] = {} + self._worker_threads: dict[str, sdk_worker.SdkHarness] = {} @classmethod def start( @@ -166,6 +168,7 @@ def StartWorker( worker_id=start_worker_request.worker_id, state_cache_size=self._state_cache_size, data_buffer_time_limit_ms=self._data_buffer_time_limit_ms) + self._worker_threads[start_worker_request.worker_id] = worker worker_thread = threading.Thread( name='run_worker_%s' % start_worker_request.worker_id, target=worker.run) @@ -188,6 +191,14 @@ def StopWorker( _LOGGER.info("Stopping worker %s" % stop_worker_request.worker_id) kill_process_gracefully(worker_process) + # applicable for thread mode to ensure thread cleanup by + # unblocking the harness request stream. + worker_thread_harness = self._worker_threads.pop( + stop_worker_request.worker_id, None) + if worker_thread_harness: + _LOGGER.info("Stopping thread worker %s" % stop_worker_request.worker_id) + worker_thread_harness._responses.put(Sentinel.sentinel) + return beam_fn_api_pb2.StopWorkerResponse() From e8535b5496c1751b8d0627770c4a2548582be778 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 20:33:58 -0400 Subject: [PATCH 135/490] Bump urllib3 from 2.6.3 to 2.7.0 in /sdks/python/container/py312 (#38452) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.7.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/container/py312/base_image_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/container/py312/base_image_requirements.txt b/sdks/python/container/py312/base_image_requirements.txt index 356392c82b56..d0948d43a9e6 100644 --- a/sdks/python/container/py312/base_image_requirements.txt +++ b/sdks/python/container/py312/base_image_requirements.txt @@ -187,7 +187,7 @@ typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2025.3 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.46.3 From f1a5520604684c962c6c33fe2f1fbc31eb36640f Mon Sep 17 00:00:00 2001 From: Andrew Crites Date: Tue, 12 May 2026 03:59:27 -0700 Subject: [PATCH 136/490] Adds a new coder translator for Java SchemaCoder. (#37631) * Adds a new coder translator for Java SchemaCoder. Adds PipelineOptions to translation context so we can disable the new translator based on pipeline compatibility version. * Refactors ModelCoderRegistrars so decisions about known coders are made there instead of in the individual CoderTranslators. Removed SdkComponents from getCoderTranslator and getCoderForUrn since it wasn't needed yet. * Adds option to pass environment to SdkComponents. Otherwise, we register a default environment in the constructor, which was preventing DataflowRunner from registering its own custom environment. * Fixup DataflowPipelineTranslatorTest to also use the new SdkComponent constructor. --- CHANGES.md | 5 +- .../dataflow/DataflowPipelineTranslator.java | 2 +- .../beam/runners/dataflow/DataflowRunner.java | 59 ++++++++------ .../DataflowPipelineTranslatorTest.java | 77 ++++++++++++++---- .../control/ProcessBundleDescriptorsTest.java | 6 +- .../util/construction/CoderTranslation.java | 62 ++++++++++++++- .../util/construction/CoderTranslator.java | 1 + .../CoderTranslatorRegistrar.java | 16 ++++ .../util/construction/CoderTranslators.java | 79 +++++++++++++++++++ .../construction/ModelCoderRegistrar.java | 28 ++++++- .../sdk/util/construction/ModelCoders.java | 2 + .../construction/RehydratedComponents.java | 3 +- .../sdk/util/construction/SdkComponents.java | 39 +++++---- .../construction/CoderTranslationTest.java | 38 ++++++++- .../expansion/service/ExpansionService.java | 2 +- .../avro/AvroGenericCoderRegistrar.java | 18 +++++ .../fn/harness/state/StateBackedIterable.java | 22 ++++++ 17 files changed, 387 insertions(+), 72 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2cf7f983fa35..7c253e8cdeef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -82,6 +82,9 @@ ## Breaking Changes +* Portable Java SDK now encodes SchemaCoders in a portable way ([#34672](https://github.com/apache/beam/issues/34672)). + - Original custom Java coder encoding can still be obtained using [StreamingOptions.setUpdateCompatibilityVersion("2.73")](https://github.com/apache/beam/blob/2cf0930e7ae1aa389c26ce6639b584877a3e31d9/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java#L47) ([#34672](https://github.com/apache/beam/issues/34672)). + - Fixes ([#36496](https://github.com/apache/beam/issues/36496)), ([#30276](https://github.com/apache/beam/issues/30276)), ([#29245](https://github.com/apache/beam/issues/29245)). * (Python) Made Beartype the default fallback type checking tool. This can be disabled with the `--disable_beartype` pipeline option. ([#38275](https://github.com/apache/beam/issues/38275)) * X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). @@ -2435,4 +2438,4 @@ Schema Options, it will be removed in version `2.23.0`. ([BEAM-9704](https://iss ## Highlights -- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). \ No newline at end of file +- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index 4016f31a5475..1609cf6ea238 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -221,7 +221,7 @@ public Boolean visit(OrFinallyTrigger trigger) { private static byte[] serializeWindowingStrategy( WindowingStrategy windowingStrategy, PipelineOptions options) { try { - SdkComponents sdkComponents = SdkComponents.create(); + SdkComponents sdkComponents = SdkComponents.create(options); String workerHarnessContainerImageURL = DataflowRunner.getContainerImageForJob(options.as(DataflowPipelineOptions.class)); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 299e7fa21ed1..c19673a3117e 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1333,19 +1333,20 @@ public DataflowPipelineJob run(Pipeline pipeline) { // with the SDK harness image (which implements Fn API). // // The same Environment is used in different and contradictory ways, depending on whether - // it is a v1 or v2 job submission. + // it is a portable or non-portable job submission. RunnerApi.Environment defaultEnvironmentForDataflow = Environments.createDockerEnvironment(workerHarnessContainerImageURL); - // The SdkComponents for portable an non-portable job submission must be kept distinct. Both + // The SdkComponents for portable and non-portable job submission must be kept distinct. Both // need the default environment. - SdkComponents portableComponents = SdkComponents.create(); - portableComponents.registerEnvironment( - defaultEnvironmentForDataflow - .toBuilder() - .addAllDependencies(getDefaultArtifacts()) - .addAllCapabilities(Environments.getJavaCapabilities()) - .build()); + SdkComponents portableComponents = + SdkComponents.create( + options, + defaultEnvironmentForDataflow + .toBuilder() + .addAllDependencies(getDefaultArtifacts()) + .addAllCapabilities(Environments.getJavaCapabilities()) + .build()); RunnerApi.Pipeline portablePipelineProto = PipelineTranslation.toProto(pipeline, portableComponents, false); @@ -1374,28 +1375,30 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.as(SdkHarnessOptions.class).setPipelineProtoHash(pipelineProtoHash); if (useUnifiedWorker(options)) { - LOG.info("Skipping v1 transform replacements since job will run on v2."); + LOG.info( + "Skipping non-portable transform replacements since job will run on portable worker."); } else { - // Now rewrite things to be as needed for v1 (mutates the pipeline) - // This way the job submitted is valid for v1 and v2, simultaneously + // Now rewrite things to be as needed for non-portable (mutates the pipeline). + // This way the job submitted is valid for portable and non-portable, simultaneously. replaceV1Transforms(pipeline); } - // Capture the SdkComponents for look up during step translations - SdkComponents dataflowV1Components = SdkComponents.create(); - dataflowV1Components.registerEnvironment( - defaultEnvironmentForDataflow - .toBuilder() - .addAllDependencies(getDefaultArtifacts()) - .addAllCapabilities(Environments.getJavaCapabilities()) - .build()); - // No need to perform transform upgrading for the Runner v1 proto. - RunnerApi.Pipeline dataflowV1PipelineProto = - PipelineTranslation.toProto(pipeline, dataflowV1Components, true, false); + // Capture the SdkComponents for look up during step translations. + SdkComponents dataflowNonPortableComponents = + SdkComponents.create( + options, + defaultEnvironmentForDataflow + .toBuilder() + .addAllDependencies(getDefaultArtifacts()) + .addAllCapabilities(Environments.getJavaCapabilities()) + .build()); + // No need to perform transform upgrading for the non-portable runner proto. + RunnerApi.Pipeline dataflowNonPortablePipelineProto = + PipelineTranslation.toProto(pipeline, dataflowNonPortableComponents, true, false); if (LOG.isDebugEnabled()) { LOG.debug( - "Dataflow v1 pipeline proto:\n{}", - TextFormat.printer().printToString(dataflowV1PipelineProto)); + "Dataflow non-portable worker pipeline proto:\n{}", + TextFormat.printer().printToString(dataflowNonPortablePipelineProto)); } // Set a unique client_request_id in the CreateJob request. @@ -1415,7 +1418,11 @@ public DataflowPipelineJob run(Pipeline pipeline) { JobSpecification jobSpecification = translator.translate( - pipeline, dataflowV1PipelineProto, dataflowV1Components, this, packages); + pipeline, + dataflowNonPortablePipelineProto, + dataflowNonPortableComponents, + this, + packages); if (!isNullOrEmpty(dataflowOptions.getDataflowWorkerJar()) && !useUnifiedWorker(options)) { List experiments = diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java index f8818931a68f..953f7a638ede 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java @@ -47,6 +47,7 @@ import com.google.api.services.dataflow.model.Job; import com.google.api.services.dataflow.model.Step; import com.google.api.services.dataflow.model.WorkerPool; +import com.google.auto.value.AutoValue; import java.io.File; import java.io.IOException; import java.io.Serializable; @@ -92,6 +93,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.options.ValueProvider; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; import org.apache.beam.sdk.state.StateSpec; import org.apache.beam.sdk.state.StateSpecs; import org.apache.beam.sdk.state.ValueState; @@ -166,15 +169,11 @@ public class DataflowPipelineTranslatorTest implements Serializable { @Rule public transient ExpectedException thrown = ExpectedException.none(); private SdkComponents createSdkComponents(PipelineOptions options) { - SdkComponents sdkComponents = SdkComponents.create(); - String containerImageURL = DataflowRunner.getContainerImageForJob(options.as(DataflowPipelineOptions.class)); RunnerApi.Environment defaultEnvironmentForDataflow = Environments.createDockerEnvironment(containerImageURL); - - sdkComponents.registerEnvironment(defaultEnvironmentForDataflow); - return sdkComponents; + return SdkComponents.create(options, defaultEnvironmentForDataflow); } // A Custom Mockito matcher for an initial Job that checks that all @@ -1294,15 +1293,16 @@ public String apply(byte[] input) { file1.deleteOnExit(); File file2 = File.createTempFile("file2-", ".txt"); file2.deleteOnExit(); - SdkComponents sdkComponents = SdkComponents.create(); - sdkComponents.registerEnvironment( - Environments.createDockerEnvironment(DataflowRunner.getContainerImageForJob(options)) - .toBuilder() - .addAllDependencies( - Environments.getArtifacts( - ImmutableList.of("file1.txt=" + file1, "file2.txt=" + file2))) - .addAllCapabilities(Environments.getJavaCapabilities()) - .build()); + SdkComponents sdkComponents = + SdkComponents.create( + options, + Environments.createDockerEnvironment(DataflowRunner.getContainerImageForJob(options)) + .toBuilder() + .addAllDependencies( + Environments.getArtifacts( + ImmutableList.of("file1.txt=" + file1, "file2.txt=" + file2))) + .addAllCapabilities(Environments.getJavaCapabilities()) + .build()); RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline, sdkComponents, true); @@ -1870,4 +1870,53 @@ public OffsetRange getInitialRange(@SuppressWarnings("unused") @Element String e return null; } } + + @AutoValue + @DefaultSchema(AutoValueSchema.class) + public abstract static class SimpleAutoValue { + public abstract String getString(); + + public abstract int getInt32(); + + public abstract long getInt64(); + + public static DataflowPipelineTranslatorTest.SimpleAutoValue of( + String string, int int32, long int64) { + return new AutoValue_DataflowPipelineTranslatorTest_SimpleAutoValue(string, int32, int64); + } + } + + @Test + public void testSchemaCoderTranslation() throws Exception { + DataflowPipelineOptions options = buildPipelineOptions(); + Pipeline pipeline = Pipeline.create(options); + pipeline + .apply(Impulse.create()) + .apply( + MapElements.via( + new SimpleFunction() { + @Override + public SimpleAutoValue apply(byte[] input) { + return SimpleAutoValue.of("foo", 5, 10L); + } + })) + .apply(Window.into(FixedWindows.of(Duration.standardMinutes(1)))); + { + SdkComponents sdkComponents = createSdkComponents(options); + RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline, sdkComponents, true); + Map coders = pipelineProto.getComponents().getCodersMap(); + assertTrue(coders.containsKey("SchemaCoder")); + assertEquals("beam:coder:schema:v1", coders.get("SchemaCoder").getSpec().getUrn()); + } + + // Prior to version 2.74, SchemaCoders are translated as custom java coders. + { + options.as(StreamingOptions.class).setUpdateCompatibilityVersion("2.73"); + SdkComponents sdkComponents = createSdkComponents(options); + RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline, sdkComponents, true); + Map coders = pipelineProto.getComponents().getCodersMap(); + assertTrue(coders.containsKey("SchemaCoder")); + assertEquals("beam:coders:javasdk:0.1", coders.get("SchemaCoder").getSpec().getUrn()); + } + } } diff --git a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java index 21d7550c38b9..9ea7404053d6 100644 --- a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java +++ b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java @@ -78,7 +78,8 @@ public void testLengthPrefixingOfKeyCoderInStatefulExecutableStage() throws Exce // Add another stateful stage with a non-standard key coder Pipeline p = Pipeline.create(); Coder keycoder = VoidCoder.of(); - assertThat(ModelCoderRegistrar.isKnownCoder(keycoder), is(false)); + ModelCoderRegistrar coderRegistrar = new ModelCoderRegistrar(); + assertThat(coderRegistrar.isKnownCoder(keycoder, p.getOptions()), is(false)); p.apply("impulse", Impulse.create()) .apply( "create", @@ -165,7 +166,8 @@ public void onTimer() {} public void testLengthPrefixingOfInputCoderExecutableStage() throws Exception { Pipeline p = Pipeline.create(); Coder voidCoder = VoidCoder.of(); - assertThat(ModelCoderRegistrar.isKnownCoder(voidCoder), is(false)); + ModelCoderRegistrar coderRegistrar = new ModelCoderRegistrar(); + assertThat(coderRegistrar.isKnownCoder(voidCoder, p.getOptions()), is(false)); p.apply("impulse", Impulse.create()) .apply( ParDo.of( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java index 22859dc68b93..2cc4bf0c6a03 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java @@ -25,11 +25,13 @@ import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.model.pipeline.v1.RunnerApi.FunctionSpec; import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.BiMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableBiMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.dataflow.qual.Deterministic; @@ -62,6 +64,8 @@ private static class DefaultTranslationContext implements TranslationContext {} private static @MonotonicNonNull BiMap, String> knownCoderUrns; + private static @MonotonicNonNull List coderTranslatorRegistrars; + private static @MonotonicNonNull Map, CoderTranslator> knownTranslators; @@ -80,6 +84,53 @@ static BiMap, String> getKnownCoderUrns() { return knownCoderUrns; } + private static void initializeCoderTranslatorRegistrars() { + ImmutableList.Builder registrars = ImmutableList.builder(); + for (CoderTranslatorRegistrar coderTranslatorRegistrar : + ServiceLoader.load(CoderTranslatorRegistrar.class)) { + registrars.add(coderTranslatorRegistrar); + } + coderTranslatorRegistrars = registrars.build(); + } + + static boolean isKnownCoder(Coder coder, PipelineOptions options) { + if (coderTranslatorRegistrars == null) { + initializeCoderTranslatorRegistrars(); + } + for (CoderTranslatorRegistrar registrar : coderTranslatorRegistrars) { + if (registrar.isKnownCoder(coder, options)) { + return true; + } + } + return false; + } + + static CoderTranslator getCoderTranslator(Class coderClass) { + if (coderTranslatorRegistrars == null) { + initializeCoderTranslatorRegistrars(); + } + for (CoderTranslatorRegistrar registrar : coderTranslatorRegistrars) { + CoderTranslator translator = registrar.getCoderTranslator(coderClass); + if (translator != null) { + return translator; + } + } + return null; + } + + static Class getCoderForUrn(String coderUrn) { + if (coderTranslatorRegistrars == null) { + initializeCoderTranslatorRegistrars(); + } + for (CoderTranslatorRegistrar registrar : coderTranslatorRegistrars) { + Class coder = registrar.getCoderForUrn(coderUrn); + if (coder != null) { + return coder; + } + } + return null; + } + @VisibleForTesting @Deterministic static Map, CoderTranslator> getKnownTranslators() { @@ -107,7 +158,7 @@ public static RunnerApi.MessageWithComponents toProto(Coder coder) throws IOE public static RunnerApi.Coder toProto(Coder coder, SdkComponents components) throws IOException { - if (getKnownCoderUrns().containsKey(coder.getClass())) { + if (isKnownCoder(coder, components.getPipelineOptions())) { return toKnownCoder(coder, components); } @@ -129,7 +180,10 @@ private static RunnerApi.Coder toUnknownCoderWrapper(UnknownCoderWrapper coder) private static RunnerApi.Coder toKnownCoder(Coder coder, SdkComponents components) throws IOException { - CoderTranslator translator = getKnownTranslators().get(coder.getClass()); + CoderTranslator translator = getCoderTranslator(coder.getClass()); + if (translator == null) { + throw new IOException("Unable to find CoderTranslator for known Coder"); + } List componentIds = registerComponents(coder, translator, components); return RunnerApi.Coder.newBuilder() .addAllComponentCoderIds(componentIds) @@ -186,8 +240,8 @@ private static Coder fromKnownCoder( components.getComponents().getCodersOrThrow(componentId), components, context); coderComponents.add(innerCoder); } - Class coderType = getKnownCoderUrns().inverse().get(coderUrn); - CoderTranslator translator = getKnownTranslators().get(coderType); + Class coderType = getCoderForUrn(coderUrn); + CoderTranslator translator = getCoderTranslator(coderType); if (translator != null) { return translator.fromComponents( coderComponents, coder.getSpec().getPayload().toByteArray(), context); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java index 3d89c4c7ff4a..78f5b61c0f0e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java @@ -28,6 +28,7 @@ * additional payload, which is not currently supported. This exists as a temporary measure. */ public interface CoderTranslator> { + /** Extract all component {@link Coder coders} within a coder. */ List> getComponents(T from); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java index b69d0290de52..44e8c2956aee 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java @@ -19,6 +19,8 @@ import java.util.Map; import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.checkerframework.checker.nullness.qual.Nullable; /** A registrar of {@link Coder} URNs to the associated {@link CoderTranslator}. */ @SuppressWarnings({ @@ -34,4 +36,18 @@ public interface CoderTranslatorRegistrar { /** Returns a mapping of URN to {@link CoderTranslator}. */ Map, CoderTranslator> getCoderTranslators(); + + /** + * Returns whether the given Coder is known to this CoderTranslatorRegistrar. If the Coder is + * known, then getCoderTranslator() will return a non-null CoderTranslator. + */ + boolean isKnownCoder(Coder coder, PipelineOptions options); + + /** Returns the CoderTranslator to use for this Coder, or null if the Coder is not known. */ + @Nullable + CoderTranslator getCoderTranslator(Class coderClass); + + /** Returns the Coder to use for the given Urn, or null if the Urn is for an unknown Coder. */ + @Nullable + Class getCoderForUrn(String coderUrn); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java index 84a90721a983..a847bf780dff 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java @@ -19,6 +19,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; +import java.io.IOException; import java.util.Collections; import java.util.List; import org.apache.beam.model.pipeline.v1.SchemaApi; @@ -30,12 +31,19 @@ import org.apache.beam.sdk.coders.RowCoder; import org.apache.beam.sdk.coders.TimestampPrefixingWindowCoder; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaCoder; import org.apache.beam.sdk.schemas.SchemaTranslation; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.InstanceBuilder; +import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.util.ShardedKey; +import org.apache.beam.sdk.util.construction.CoderTranslation.TranslationContext; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; +import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.InvalidProtocolBufferException; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -177,6 +185,77 @@ public RowCoder fromComponents( }; } + static CoderTranslator> schema() { + return new CoderTranslator>() { + private static final String TO_ROW_FUNCTION_URN = "beam:torowfn:javasdk:v1"; + private static final String FROM_ROW_FUNCTION_URN = "beam:fromrowfn:javasdk:v1"; + private static final String TYPE_DESCRIPTOR_URN = "beam:typedescriptor:javasdk:v1"; + + @Override + public ImmutableList> getComponents(SchemaCoder from) { + return ImmutableList.of(); + } + + @Override + public byte[] getPayload(SchemaCoder from) { + SchemaApi.SchemaCoderPayload.Builder payload = SchemaApi.SchemaCoderPayload.newBuilder(); + payload.setSchema(SchemaTranslation.schemaToProto(from.getSchema(), true)); + payload + .getToRowFnBuilder() + .setUrn(TO_ROW_FUNCTION_URN) + .setPayload( + ByteString.copyFrom( + SerializableUtils.serializeToByteArray(from.getToRowFunction()))); + payload + .getFromRowFnBuilder() + .setUrn(FROM_ROW_FUNCTION_URN) + .setPayload( + ByteString.copyFrom( + SerializableUtils.serializeToByteArray(from.getFromRowFunction()))); + payload + .addAdditionalCoderInfosBuilder() + .setUrn(TYPE_DESCRIPTOR_URN) + .setPayload( + ByteString.copyFrom( + SerializableUtils.serializeToByteArray(from.getEncodedTypeDescriptor()))); + return payload.build().toByteArray(); + } + + @Override + public SchemaCoder fromComponents( + List> components, byte[] payload, TranslationContext context) { + checkArgument( + components.isEmpty(), "Expected empty component list, but received: %s", components); + try { + SchemaApi.SchemaCoderPayload schemaCoderPayload = + SchemaApi.SchemaCoderPayload.parseFrom(payload); + if (schemaCoderPayload.getAdditionalCoderInfosCount() == 0) { + throw new IllegalArgumentException("Missing serialized typeDescriptor"); + } + TypeDescriptor typeDescriptor = + (TypeDescriptor) + SerializableUtils.deserializeFromByteArray( + schemaCoderPayload.getAdditionalCoderInfos(0).getPayload().toByteArray(), + "typeDescriptor"); + SerializableFunction toRowFunction = + (SerializableFunction) + SerializableUtils.deserializeFromByteArray( + schemaCoderPayload.getToRowFn().getPayload().toByteArray(), "toRowFunction"); + SerializableFunction fromRowFunction = + (SerializableFunction) + SerializableUtils.deserializeFromByteArray( + schemaCoderPayload.getFromRowFn().getPayload().toByteArray(), + "fromRowFunction"); + + Schema schema = SchemaTranslation.schemaFromProto(schemaCoderPayload.getSchema()); + return SchemaCoder.of(schema, typeDescriptor, toRowFunction, fromRowFunction); + } catch (IOException | IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + }; + } + static CoderTranslator> shardedKey() { return new SimpleStructuredCoderTranslator>() { @Override diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java index 5b0d5aedd619..1f9f1eaafbed 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java @@ -34,6 +34,9 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.coders.TimestampPrefixingWindowCoder; import org.apache.beam.sdk.coders.VarLongCoder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.StreamingOptions; +import org.apache.beam.sdk.schemas.SchemaCoder; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.transforms.windowing.IntervalWindow.IntervalWindowCoder; import org.apache.beam.sdk.util.ShardedKey; @@ -71,6 +74,7 @@ public class ModelCoderRegistrar implements CoderTranslatorRegistrar { ModelCoders.PARAM_WINDOWED_VALUE_CODER_URN) .put(DoubleCoder.class, ModelCoders.DOUBLE_CODER_URN) .put(RowCoder.class, ModelCoders.ROW_CODER_URN) + .put(SchemaCoder.class, ModelCoders.SCHEMA_CODER_URN) .put(ShardedKey.Coder.class, ModelCoders.SHARDED_KEY_CODER_URN) .put(TimestampPrefixingWindowCoder.class, ModelCoders.CUSTOM_WINDOW_CODER_URN) .put(NullableCoder.class, ModelCoders.NULLABLE_CODER_URN) @@ -96,6 +100,7 @@ public class ModelCoderRegistrar implements CoderTranslatorRegistrar { CoderTranslators.paramWindowedValue()) .put(DoubleCoder.class, CoderTranslators.atomic(DoubleCoder.class)) .put(RowCoder.class, CoderTranslators.row()) + .put(SchemaCoder.class, CoderTranslators.schema()) .put(ShardedKey.Coder.class, CoderTranslators.shardedKey()) .put(TimestampPrefixingWindowCoder.class, CoderTranslators.timestampPrefixingWindow()) .put(NullableCoder.class, CoderTranslators.nullable()) @@ -123,10 +128,6 @@ public class ModelCoderRegistrar implements CoderTranslatorRegistrar { Coder.class.getSimpleName()); } - public static boolean isKnownCoder(Coder coder) { - return BEAM_MODEL_CODER_URNS.containsKey(coder.getClass()); - } - @Override public Map, String> getCoderURNs() { return BEAM_MODEL_CODER_URNS; @@ -136,4 +137,23 @@ public Map, String> getCoderURNs() { public Map, CoderTranslator> getCoderTranslators() { return BEAM_MODEL_CODERS; } + + @Override + public boolean isKnownCoder(Coder coder, PipelineOptions options) { + if (coder.getClass() == SchemaCoder.class + && StreamingOptions.updateCompatibilityVersionLessThan(options, "2.74")) { + return false; + } + return BEAM_MODEL_CODER_URNS.containsKey(coder.getClass()); + } + + @Override + public CoderTranslator getCoderTranslator(Class coderClass) { + return BEAM_MODEL_CODERS.getOrDefault(coderClass, null); + } + + @Override + public Class getCoderForUrn(String coderUrn) { + return BEAM_MODEL_CODER_URNS.inverse().getOrDefault(coderUrn, null); + } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java index 7b7546aceb61..5059cc1c6b83 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java @@ -61,6 +61,7 @@ private ModelCoders() {} getUrn(StandardCoders.Enum.PARAM_WINDOWED_VALUE); public static final String ROW_CODER_URN = getUrn(StandardCoders.Enum.ROW); + public static final String SCHEMA_CODER_URN = getUrn(StandardCoders.Enum.SCHEMA); public static final String STATE_BACKED_ITERABLE_CODER_URN = "beam:coder:state_backed_iterable:v1"; @@ -90,6 +91,7 @@ private ModelCoders() {} WINDOWED_VALUE_CODER_URN, DOUBLE_CODER_URN, ROW_CODER_URN, + SCHEMA_CODER_URN, PARAM_WINDOWED_VALUE_CODER_URN, STATE_BACKED_ITERABLE_CODER_URN, SHARDED_KEY_CODER_URN, diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java index f79696214368..64c7898a37b9 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java @@ -189,6 +189,7 @@ public SdkComponents getSdkComponents(Collection requirements) { windowingStrategies.asMap(), coders.asMap(), Collections.emptyMap(), - requirements); + requirements, + pipeline.getOptions()); } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java index 446697f24a81..6288649aba3d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java @@ -63,6 +63,7 @@ public class SdkComponents { private final BiMap environmentIds = HashBiMap.create(); private final BiMap coderProtoToId = HashBiMap.create(); private final Set requirements; + private final PipelineOptions pipelineOptions; private final Set reservedIds = new HashSet<>(); @@ -71,17 +72,7 @@ public class SdkComponents { /** Create a new {@link SdkComponents} with no components. */ public static SdkComponents create() { - return new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, ""); - } - - /** - * Create new {@link SdkComponents} importing all items from provided {@link Components} object. - * - *

    WARNING: This action might cause some of duplicate items created. - */ - public static SdkComponents create( - RunnerApi.Components components, Collection requirements) { - return new SdkComponents(components, requirements, ""); + return new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, "", null); } /*package*/ static SdkComponents create( @@ -91,8 +82,9 @@ public static SdkComponents create( Map> windowingStrategies, Map> coders, Map environments, - Collection requirements) { - SdkComponents sdkComponents = SdkComponents.create(components, requirements); + Collection requirements, + PipelineOptions pipelineOptions) { + SdkComponents sdkComponents = new SdkComponents(components, requirements, "", pipelineOptions); sdkComponents.transformIds.inverse().putAll(transforms); sdkComponents.pCollectionIds.inverse().putAll(pCollections); sdkComponents.windowingStrategyIds.inverse().putAll(windowingStrategies); @@ -103,19 +95,28 @@ public static SdkComponents create( public static SdkComponents create(PipelineOptions options) { SdkComponents sdkComponents = - new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, ""); + new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, "", options); PortablePipelineOptions portablePipelineOptions = options.as(PortablePipelineOptions.class); sdkComponents.registerEnvironment( Environments.createOrGetDefaultEnvironment(portablePipelineOptions)); return sdkComponents; } + public static SdkComponents create(PipelineOptions options, Environment environment) { + SdkComponents sdkComponents = + new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, "", options); + sdkComponents.registerEnvironment(environment); + return sdkComponents; + } + private SdkComponents( @Nullable Components components, @Nullable Collection requirements, - String newIdPrefix) { + String newIdPrefix, + @Nullable PipelineOptions pipelineOptions) { this.newIdPrefix = newIdPrefix; this.requirements = new HashSet<>(); + this.pipelineOptions = pipelineOptions; if (components == null) { if (requirements != null) { @@ -153,7 +154,7 @@ public void mergeFrom( */ public SdkComponents withNewIdPrefix(String newIdPrefix) { SdkComponents sdkComponents = - new SdkComponents(componentsBuilder.build(), requirements, newIdPrefix); + new SdkComponents(componentsBuilder.build(), requirements, newIdPrefix, pipelineOptions); sdkComponents.transformIds.putAll(transformIds); sdkComponents.pCollectionIds.putAll(pCollectionIds); sdkComponents.windowingStrategyIds.putAll(windowingStrategyIds); @@ -174,7 +175,7 @@ public String registerPTransform( throws IOException { String name = getApplicationName(appliedPTransform); // If this transform is present in the components, nothing to do. return the existing name. - // Otherwise the transform must be translated and added to the components. + // Otherwise, the transform must be translated and added to the components. if (componentsBuilder.getTransformsOrDefault(name, null) != null) { return name; } @@ -375,4 +376,8 @@ public RunnerApi.Components toComponents() { public Collection requirements() { return ImmutableSet.copyOf(requirements); } + + public PipelineOptions getPipelineOptions() { + return pipelineOptions; + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java index b8f92ff0053e..1ec0a74f5be1 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; +import com.google.auto.value.AutoValue; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -45,14 +46,20 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.coders.TimestampPrefixingWindowCoder; import org.apache.beam.sdk.coders.VarLongCoder; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.schemas.SchemaCoder; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.transforms.windowing.IntervalWindow.IntervalWindowCoder; import org.apache.beam.sdk.util.ShardedKey; import org.apache.beam.sdk.util.construction.CoderTranslation.TranslationContext; +import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -70,6 +77,34 @@ "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) }) public class CoderTranslationTest { + @AutoValue + @DefaultSchema(AutoValueSchema.class) + public abstract static class SimpleAutoValue { + public abstract String getString(); + + public abstract int getInt32(); + + public abstract long getInt64(); + + public static SimpleAutoValue of(String string, Integer int32, Long int64) { + return new AutoValue_CoderTranslationTest_SimpleAutoValue(string, int32, int64); + } + } + + private static final SchemaRegistry REGISTRY = SchemaRegistry.createDefault(); + + private static SchemaCoder schemaCoderFrom(TypeDescriptor typeDescriptor) { + try { + return SchemaCoder.of( + REGISTRY.getSchema(typeDescriptor), + typeDescriptor, + REGISTRY.getToRowFunction(typeDescriptor), + REGISTRY.getFromRowFunction(typeDescriptor)); + } catch (NoSuchSchemaException e) { + throw new RuntimeException(e); + } + } + private static final Set> KNOWN_CODERS = ImmutableSet.>builder() .add(ByteArrayCoder.of()) @@ -94,6 +129,7 @@ public class CoderTranslationTest { Field.of("array", FieldType.array(FieldType.STRING)), Field.of("map", FieldType.map(FieldType.STRING, FieldType.INT32)), Field.of("bar", FieldType.logicalType(FixedBytes.of(123)))))) + .add(schemaCoderFrom(TypeDescriptor.of(SimpleAutoValue.class))) .add(ShardedKey.Coder.of(StringUtf8Coder.of())) .add(TimestampPrefixingWindowCoder.of(IntervalWindowCoder.of())) .add(NullableCoder.of(ByteArrayCoder.of())) @@ -127,7 +163,7 @@ public void validateKnownCoders() { } @Test - public void validateCoderTranslators() { + public void validateModelCoderTranslators() { assertThat( "Every Model Coder must have a Translator", new ModelCoderRegistrar().getCoderURNs().keySet(), diff --git a/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java b/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java index 6ecb029c5d97..c93de2014798 100644 --- a/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java +++ b/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java @@ -600,7 +600,7 @@ private Map loadRegisteredTransforms() { pipeline.getOptions().as(ExperimentalOptions.class), "use_sdf_read"); } else { LOG.warn( - "Using use_depreacted_read in portable runners is runner-dependent. The " + "Using use_deprecated_read in portable runners is runner-dependent. The " + "ExpansionService will respect that, but if your runner does not have support for " + "native Read transform, your Pipeline will fail during Pipeline submission."); } diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java index 14ab48f66699..8bd18fd8e250 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java @@ -21,9 +21,11 @@ import java.util.Map; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.extensions.avro.coders.AvroGenericCoder; +import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.util.construction.CoderTranslator; import org.apache.beam.sdk.util.construction.CoderTranslatorRegistrar; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.checkerframework.checker.nullness.qual.Nullable; /** Coder registrar for AvroGenericCoder. */ @AutoService(CoderTranslatorRegistrar.class) @@ -42,4 +44,20 @@ public Map, String> getCoderURNs() { public Map, CoderTranslator> getCoderTranslators() { return ImmutableMap.of(AvroGenericCoder.class, new AvroGenericCoderTranslator()); } + + @Override + public boolean isKnownCoder(Coder coder, PipelineOptions options) { + return coder.getClass() == AvroGenericCoder.class; + } + + @Override + public @Nullable CoderTranslator getCoderTranslator( + Class coderClass) { + return coderClass == AvroGenericCoder.class ? new AvroGenericCoderTranslator() : null; + } + + @Override + public @Nullable Class getCoderForUrn(String coderUrn) { + return AVRO_CODER_URN.equals(coderUrn) ? AvroGenericCoder.class : null; + } } diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java index ef8d69bc1ec3..42a6f8d11c2a 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java @@ -38,6 +38,7 @@ import org.apache.beam.sdk.coders.IterableLikeCoder; import org.apache.beam.sdk.fn.stream.PrefetchableIterable; import org.apache.beam.sdk.fn.stream.PrefetchableIterators; +import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.util.BufferedElementCountingOutputStream; import org.apache.beam.sdk.util.VarInt; import org.apache.beam.sdk.util.common.ElementByteSizeObservableIterable; @@ -52,6 +53,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.io.ByteStreams; +import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -300,6 +302,26 @@ public Map, String> getCoderUR getCoderTranslators() { return ImmutableMap.of(StateBackedIterable.Coder.class, new Translator()); } + + @Override + public boolean isKnownCoder( + org.apache.beam.sdk.coders.Coder coder, PipelineOptions options) { + return coder.getClass() == StateBackedIterable.Coder.class; + } + + @Override + public @Nullable CoderTranslator getCoderTranslator( + Class coderClass) { + return coderClass == StateBackedIterable.Coder.class ? new Translator() : null; + } + + @Override + public @Nullable Class getCoderForUrn( + String coderUrn) { + return STATE_BACKED_ITERABLE_CODER_URN.equals(coderUrn) + ? StateBackedIterable.Coder.class + : null; + } } /** From 55ef0cb7a7ddc021eb19a28deac54ce7cb647579 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 07:29:02 -0400 Subject: [PATCH 137/490] Bump github.com/nats-io/nats.go from 1.51.0 to 1.52.0 in /sdks (#38463) Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.51.0 to 1.52.0. - [Release notes](https://github.com/nats-io/nats.go/releases) - [Commits](https://github.com/nats-io/nats.go/compare/v1.51.0...v1.52.0) --- updated-dependencies: - dependency-name: github.com/nats-io/nats.go dependency-version: 1.52.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 49dcfc834ab4..913244fbae2f 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -38,7 +38,7 @@ require ( github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 github.com/aws/smithy-go v1.25.1 - github.com/docker/go-connections v0.6.0 + github.com/docker/go-connections v0.6.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 github.com/google/go-cmp v0.7.0 @@ -47,7 +47,7 @@ require ( github.com/lib/pq v1.12.3 github.com/linkedin/goavro/v2 v2.15.0 github.com/nats-io/nats-server/v2 v2.14.0 - github.com/nats-io/nats.go v1.51.0 + github.com/nats-io/nats.go v1.52.0 github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 github.com/testcontainers/testcontainers-go v0.42.0 diff --git a/sdks/go.sum b/sdks/go.sum index 43dfdd2ed727..be5d85449aa8 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -728,8 +728,8 @@ github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= github.com/nats-io/nats-server/v2 v2.14.0 h1:+8q0HrDFotwLLcGH/legOEOnowunhK+aZ4GYBIWpQlM= github.com/nats-io/nats-server/v2 v2.14.0/go.mod h1:ImVUUDvfClJbb6cuJQRc1VmgDCXKM5ds0OoiG9MVOKo= -github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= -github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= +github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= From 857a299c523aa41d9d778030e23902613f20820b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 07:30:08 -0400 Subject: [PATCH 138/490] Bump actions/upload-artifact from 4 to 7 (#38460) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ...ostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml index b595afe6f42c..da6e175a685b 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml @@ -82,7 +82,7 @@ jobs: -PtestJavaVersion=17 \ -PdisableSpotlessCheck=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results From b5e23e12641d0abedecfba40f1b6aa9578153d16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 07:31:05 -0400 Subject: [PATCH 139/490] Bump github.com/aws/aws-sdk-go-v2/config from 1.32.7 to 1.32.17 in /sdks (#38459) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.7 to 1.32.17. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.32.7...config/v1.32.17) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 25 ++++++++++++------------- sdks/go.sum | 50 ++++++++++++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 913244fbae2f..049c39a1c1fd 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,8 +33,8 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.1 github.com/aws/aws-sdk-go-v2 v1.41.7 - github.com/aws/aws-sdk-go-v2/config v1.32.7 - github.com/aws/aws-sdk-go-v2/credentials v1.19.7 + github.com/aws/aws-sdk-go-v2/config v1.32.17 + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 github.com/aws/smithy-go v1.25.1 @@ -89,7 +89,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -151,18 +151,17 @@ require ( github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index be5d85449aa8..070dcacfce11 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,47 +207,45 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7P github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= -github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW/1MEApMz7uitCF/PQXRJpk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= @@ -258,22 +256,22 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU66 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= From 50d4e33fa08bfaccab90549bea8b953642ffb45a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 07:32:11 -0400 Subject: [PATCH 140/490] Bump golang.org/x/net from 0.53.0 to 0.54.0 in /sdks (#38462) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0. - [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 12 ++++++------ sdks/go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 049c39a1c1fd..f013d8646203 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -55,11 +55,11 @@ require ( github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 - golang.org/x/net v0.53.0 + golang.org/x/net v0.54.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.44.0 - golang.org/x/text v0.36.0 + golang.org/x/text v0.37.0 google.golang.org/api v0.278.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.81.0 @@ -136,7 +136,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect - golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect golang.org/x/time v0.15.0 // indirect ) @@ -204,9 +204,9 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/tools v0.43.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 070dcacfce11..e1cc83a8a667 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -941,8 +941,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -995,8 +995,8 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1052,8 +1052,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1178,8 +1178,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= -golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1188,8 +1188,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1204,8 +1204,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1280,8 +1280,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 9fbe54a1ef9cdae8e40e4588aeb29dc410b923a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 07:32:41 -0400 Subject: [PATCH 141/490] Bump actions/checkout from 4 to 6 (#38464) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ...ostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml index da6e175a685b..d1f214e6a458 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark4 StructuredStreaming ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup repository uses: ./.github/actions/setup-action with: From b73c7b84e91b809abac904975736536b74b45e33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 08:42:33 -0400 Subject: [PATCH 142/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38461) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.21.1 to 1.22.18. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.21.1...feature/s3/manager/v1.22.18) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.18 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 10 +++++----- sdks/go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index f013d8646203..d6f320ac7882 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,8 +35,8 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.17 github.com/aws/aws-sdk-go-v2/credentials v1.19.16 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 github.com/docker/go-connections v0.6.0 // indirect github.com/dustin/go-humanize v1.0.1 @@ -150,15 +150,15 @@ require ( github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index e1cc83a8a667..7570e58c88df 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -203,8 +203,8 @@ github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6t github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8Tc github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW/1MEApMz7uitCF/PQXRJpk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18 h1:9XFUd2lkr7VrbE4Qtrhm7AtNhGgZeGFI5QLZtQIflj8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18/go.mod h1:trImuKdWelQIJALvyGj6sKolJ1W8t628JOoTdDGVL9Q= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= @@ -240,21 +240,21 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZL github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= From ea6c6bf0e71be385f16c9a31cbeb7c6be512a1c6 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 12 May 2026 10:10:32 -0400 Subject: [PATCH 143/490] Make SubprocessServer shared cache purging idempotent (#38455) * Make SubprocessServer shared cache purging idempotent * Reformat --- .../apache_beam/utils/subprocess_server.py | 6 +++- .../utils/subprocess_server_test.py | 29 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index 5752a49dde2e..fed5ee591bcd 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -91,7 +91,11 @@ def purge(self, owner): to_delete = [] with self._lock: if owner not in self._live_owners: - raise ValueError(f"{owner} not in {self._live_owners}") + _LOGGER.warning( + "Subprocess owner %s already purged. If this occurs during atexit " + "shutdown, the subprocess was already cleaned up earlier.", + owner) + return self._live_owners.remove(owner) for key, entry in list(self._cache.items()): if owner in entry.owners: diff --git a/sdks/python/apache_beam/utils/subprocess_server_test.py b/sdks/python/apache_beam/utils/subprocess_server_test.py index 0f25d9904f07..efd357c4c136 100644 --- a/sdks/python/apache_beam/utils/subprocess_server_test.py +++ b/sdks/python/apache_beam/utils/subprocess_server_test.py @@ -421,10 +421,31 @@ def purge_worker(): t1.join() t2.join() - # Exactly one thread should raise the expected ValueError because they are cleanly serialized - self.assertEqual(len(exceptions), 1) - self.assertIsInstance(exceptions[0], ValueError) - self.assertNotIsInstance(exceptions[0], KeyError) + # Both threads should succeed cleanly without raising an exception under idempotent purging. + self.assertEqual(len(exceptions), 0) + + def test_stop_process_after_cache_purged(self): + # Reproduce the ValueError when stop_process() (called by atexit) + # runs after the cache/owner was already purged during test teardown. + cache = subprocess_server._SharedCache( + lambda *args: "dummy_process", lambda obj: None) + + class DummySubprocessServer(subprocess_server.SubprocessServer): + _cache = cache + + def __init__(self): + super().__init__(lambda channel: None, ["dummy_cmd"], port=12345) + + server = DummySubprocessServer() + server.start_process() + owner_id = server._owner_id + + # Simulate pipeline context exit or test teardown purging the cache directly + cache.purge(owner_id) + + # Calling stop_process() (which happens during atexit) should succeed cleanly + # without raising ValueError. + server.stop_process() if __name__ == '__main__': From 8fd2dcb583758ee15a5b1a804573befc4f26fa85 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 12 May 2026 10:36:12 -0400 Subject: [PATCH 144/490] Exercise Spark 4 tests (#38453) --- .github/workflows/README.md | 2 +- ...=> beam_PreCommit_Java_Spark_Versions.yml} | 21 ++- .../beam/gradle/BeamModulePlugin.groovy | 139 +++++++++--------- 3 files changed, 82 insertions(+), 80 deletions(-) rename .github/workflows/{beam_PreCommit_Java_Spark3_Versions.yml => beam_PreCommit_Java_Spark_Versions.yml} (86%) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c3c73c0317a9..9867810a7419 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -267,7 +267,7 @@ PreCommit Jobs run in a schedule and also get triggered in a PR if relevant sour | [ PreCommit Java SingleStore IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml) | N/A |`Run Java_SingleStore_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Snowflake IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml) | N/A |`Run Java_Snowflake_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Solr IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml) | N/A |`Run Java_Solr_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml?query=event%3Aschedule) | -| [ PreCommit Java Spark3 Versions ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark3_Versions.yml) | N/A | `Run Java_Spark3_Versions PreCommit` | [![.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark3_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark3_Versions.yml?query=event%3Aschedule) | +| [ PreCommit Java Spark Versions ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark_Versions.yml) | N/A | `Run Java_Spark_Versions PreCommit` | [![.github/workflows/beam_PreCommit_Java_Spark_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark_Versions.yml?query=event%3Aschedule) | | [ PreCommit Java Splunk IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml) | N/A |`Run Java_Splunk_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Thrift IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml) | N/A |`Run Java_Thrift_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Tika IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml) | N/A |`Run Java_Tika_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml similarity index 86% rename from .github/workflows/beam_PreCommit_Java_Spark3_Versions.yml rename to .github/workflows/beam_PreCommit_Java_Spark_Versions.yml index dd0bb69b32fe..6f8689d00caa 100644 --- a/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -name: PreCommit Java Spark3 Versions +name: PreCommit Java Spark Versions on: push: @@ -23,13 +23,13 @@ on: branches: ['master', 'release-*'] paths: - 'runners/spark/**' - - '.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml' + - '.github/workflows/beam_PreCommit_Java_Spark_Versions.yml' pull_request_target: branches: ['master', 'release-*'] paths: - 'runners/spark/**' - 'release/trigger_all_tests.json' - - '.github/trigger_files/beam_PreCommit_Java_Spark3_Versions.json' + - '.github/trigger_files/beam_PreCommit_Java_Spark_Versions.json' issue_comment: types: [created] schedule: @@ -63,20 +63,20 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PreCommit_Java_Spark3_Versions: + beam_PreCommit_Java_Spark_Versions: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] strategy: matrix: - job_name: [beam_PreCommit_Java_Spark3_Versions] - job_phrase: [Run Java_Spark3_Versions PreCommit] + job_name: [beam_PreCommit_Java_Spark_Versions] + job_phrase: [Run Java_Spark_Versions PreCommit] timeout-minutes: 120 if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event_name == 'workflow_dispatch' || - github.event.comment.body == 'Run Java_Spark3_Versions PreCommit' + github.event.comment.body == 'Run Java_Spark_Versions PreCommit' steps: - uses: actions/checkout@v6 - name: Setup repository @@ -87,12 +87,17 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action + with: + java-version: | + 17 + 11 - name: run sparkVersionsTest script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:spark:3:sparkVersionsTest + gradle-command: :runners:spark:3:sparkVersionsTest :runners:spark:4:build arguments: | -PdisableSpotlessCheck=true \ + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index e32d1c9afb77..2e064b367db3 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -1516,72 +1516,66 @@ class BeamModulePlugin implements Plugin { project.tasks.analyzeDependencies.enabled = false } - // errorprone requires java9+ compiler. It can be used with Java8 but then sets a java9+ errorproneJavac. - // However, the redirect ignores any task that forks and defines either a javaHome or an executable, - // see https://github.com/tbroyer/gradle-errorprone-plugin#jdk-8-support - // which means errorprone cannot run when gradle runs on Java11+ but serve `-testJavaVersion=8 -Pjava8Home` options - if (!(project.findProperty('testJavaVersion') == '8')) { - // Enable errorprone static analysis - project.apply plugin: 'net.ltgt.errorprone' + // Enable errorprone static analysis + project.apply plugin: 'net.ltgt.errorprone' - project.dependencies { - errorprone("com.google.errorprone:error_prone_core:$errorprone_version") - errorprone("jp.skypencil.errorprone.slf4j:errorprone-slf4j:0.1.28") - } - - project.configurations.errorprone { resolutionStrategy.force "com.google.errorprone:error_prone_core:$errorprone_version" } - - project.tasks.withType(JavaCompile) { - options.errorprone.disableWarningsInGeneratedCode = true - options.errorprone.excludedPaths = '(.*/)?(build/generated-src|build/generated.*avro-java|build/generated)/.*' - - // Error Prone requires some packages to be exported/opened on Java versions that support modules, - // i.e. Java 9 and up. The flags became mandatory in Java 17 with JEP-403. - // The -J prefix is not needed if forkOptions.javaHome is unset, - // see http://github.com/gradle/gradle/issues/22747 - if (options.forkOptions.javaHome == null) { - options.fork = true - options.forkOptions.jvmArgs += errorProneAddModuleOpts - } - def disabledChecks = [ - // TODO(https://github.com/apache/beam/issues/20955): Enable errorprone checks - "AutoValueImmutableFields", - "ComparableType", - "DoNotMockAutoValue", - "EmptyBlockTag", - "ExtendsAutoValue", - "InlineMeSuggester", - "InvalidBlockTag", - "JodaConstructors", - "MixedMutabilityReturnType", - "PreferJavaTimeOverload", - "Slf4jSignOnlyFormat", - "UnrecognisedJavadocTag", - // errorprone 3.2.0+ checks - "DirectInvocationOnMock", - "MockNotUsedInProduction", - "NullableWildcard", - "SuperCallToObjectMethod", - // Intended suppressions with justifications - // for encoding efficiency and backward compatibility - "EnumOrdinal", - // widely used in non-public methods - "NotJavadoc", - // return values used for assignments widely, and for backward compatibility. - "NonApiType", - // Used to test self equal - "SelfAssertion", - // Sometimes a static logger is preferred, which is the convention currently used in beam. See docs: - // https://github.com/KengoTODA/findbugs-slf4j#slf4j_logger_should_be_non_static - "Slf4jLoggerShouldBeNonStatic", - // allow implicit Locale.Default - "StringCaseLocaleUsage", - // DoFn methods are executed reflectively at pipeline runtime - "UnusedMethod", - ] - disabledChecks.each { - options.errorprone.errorproneArgs.add("-Xep:${it}:OFF") - } + project.dependencies { + errorprone("com.google.errorprone:error_prone_core:$errorprone_version") + errorprone("jp.skypencil.errorprone.slf4j:errorprone-slf4j:0.1.28") + } + + project.configurations.errorprone { resolutionStrategy.force "com.google.errorprone:error_prone_core:$errorprone_version" } + + project.tasks.withType(JavaCompile) { + options.errorprone.disableWarningsInGeneratedCode = true + options.errorprone.excludedPaths = '(.*/)?(build/generated-src|build/generated.*avro-java|build/generated)/.*' + + // Error Prone requires some packages to be exported/opened on Java versions that support modules, + // i.e. Java 9 and up. The flags became mandatory in Java 17 with JEP-403. + // The -J prefix is not needed if forkOptions.javaHome is unset, + // see http://github.com/gradle/gradle/issues/22747 + if (options.forkOptions.javaHome == null) { + options.fork = true + options.forkOptions.jvmArgs += errorProneAddModuleOpts + } + def disabledChecks = [ + // TODO(https://github.com/apache/beam/issues/20955): Enable errorprone checks + "AutoValueImmutableFields", + "ComparableType", + "DoNotMockAutoValue", + "EmptyBlockTag", + "ExtendsAutoValue", + "InlineMeSuggester", + "InvalidBlockTag", + "JodaConstructors", + "MixedMutabilityReturnType", + "PreferJavaTimeOverload", + "Slf4jSignOnlyFormat", + "UnrecognisedJavadocTag", + // errorprone 3.2.0+ checks + "DirectInvocationOnMock", + "MockNotUsedInProduction", + "NullableWildcard", + "SuperCallToObjectMethod", + // Intended suppressions with justifications + // for encoding efficiency and backward compatibility + "EnumOrdinal", + // widely used in non-public methods + "NotJavadoc", + // return values used for assignments widely, and for backward compatibility. + "NonApiType", + // Used to test self equal + "SelfAssertion", + // Sometimes a static logger is preferred, which is the convention currently used in beam. See docs: + // https://github.com/KengoTODA/findbugs-slf4j#slf4j_logger_should_be_non_static + "Slf4jLoggerShouldBeNonStatic", + // allow implicit Locale.Default + "StringCaseLocaleUsage", + // DoFn methods are executed reflectively at pipeline runtime + "UnusedMethod", + ] + disabledChecks.each { + options.errorprone.errorproneArgs.add("-Xep:${it}:OFF") } } @@ -1624,16 +1618,19 @@ class BeamModulePlugin implements Plugin { preserveFileTimestamps(false) } + String testJavaVersion = project.findProperty('testJavaVersion') + if (!testJavaVersion && forkJavaVersion) { + testJavaVersion = forkJavaVersion + } // if specified test java version, modify the compile and runtime versions accordingly - if (['11', '17', '21', '25'].contains(project.findProperty('testJavaVersion'))) { - String ver = project.getProperty('testJavaVersion') - def testJavaHome = project.getProperty("java${ver}Home") + if (['11', '17', '21', '25'].contains(testJavaVersion)) { + def testJavaHome = project.getProperty("java${testJavaVersion}Home") // redirect java compiler to specified version for compileTestJava only project.tasks.compileTestJava { - setCompileAndRuntimeJavaVersion(options.compilerArgs, ver) - project.ext.setJavaVerOptions(options, ver) - if (ver == '25') { + setCompileAndRuntimeJavaVersion(options.compilerArgs, testJavaVersion) + project.ext.setJavaVerOptions(options, testJavaVersion) + if (testJavaVersion == '25') { // TODO: Upgrade errorprone version to support Java25. Currently compile crashes // java.lang.NoSuchFieldError: Class com.sun.tools.javac.code.TypeTag does not have member field // 'com.sun.tools.javac.code.TypeTag UNKNOWN' From e52766872c09354a219057916ac72546637af4c1 Mon Sep 17 00:00:00 2001 From: scwhittle Date: Tue, 12 May 2026 19:37:32 +0200 Subject: [PATCH 145/490] [Python] Python] Bound the memory used for fnapi outbound data messages and receiving messages. (#38407) * [Python] Bound the memory used for fnapi outbound data messages. Previously an unbounded queue was used for pending data outputs to be sent over the fnapi to the runner. If outputs were being generated faster than the runner was consuming them, this would lead to memory growth and possible OOMs. This PR introduces a byte-limited queue data structure that is used instead to limit the # of bytes in the queue. This was preferred to just using a queue with max number of elements because the size of elements can vary greatly. For batch pipelines they are likely large while for stremaing pipelines there may be more small outputs. * monotonic and not shutdown restriction * change to not subclass queue.Queue and to be fair * fixups * add missing pxd file, fixup test * use 64-bit for size in pxd * address comments * add condition caching --- .../apache_beam/runners/worker/data_plane.py | 70 +++-- .../apache_beam/utils/byte_limited_queue.pxd | 31 ++ .../apache_beam/utils/byte_limited_queue.py | 204 +++++++++++++ .../utils/byte_limited_queue_test.py | 270 ++++++++++++++++++ sdks/python/setup.py | 1 + 5 files changed, 547 insertions(+), 29 deletions(-) create mode 100644 sdks/python/apache_beam/utils/byte_limited_queue.pxd create mode 100644 sdks/python/apache_beam/utils/byte_limited_queue.py create mode 100644 sdks/python/apache_beam/utils/byte_limited_queue_test.py diff --git a/sdks/python/apache_beam/runners/worker/data_plane.py b/sdks/python/apache_beam/runners/worker/data_plane.py index cbd28f8b0a3f..a5589ac33a1b 100644 --- a/sdks/python/apache_beam/runners/worker/data_plane.py +++ b/sdks/python/apache_beam/runners/worker/data_plane.py @@ -49,6 +49,7 @@ from apache_beam.portability.api import beam_fn_api_pb2_grpc from apache_beam.runners.worker.channel_factory import GRPCChannelFactory from apache_beam.runners.worker.worker_id_interceptor import WorkerIdInterceptor +from apache_beam.utils.byte_limited_queue import ByteLimitedQueue if TYPE_CHECKING: import apache_beam.coders.slow_stream @@ -455,11 +456,14 @@ class _GrpcDataChannel(DataChannel): def __init__(self, data_buffer_time_limit_ms=0): # type: (int) -> None + self._data_buffer_time_limit_ms = data_buffer_time_limit_ms - self._to_send = queue.Queue() # type: queue.Queue[DataOrTimers] + self._to_send = ByteLimitedQueue( + maxsize=10000, + maxbytes=100 << 20) # type: ByteLimitedQueue[DataOrTimers] self._received = collections.defaultdict( - lambda: queue.Queue(maxsize=5) - ) # type: DefaultDict[str, queue.Queue[DataOrTimers]] + lambda: ByteLimitedQueue(maxsize=5, maxbytes=100 << 20) + ) # type: DefaultDict[str, ByteLimitedQueue[DataOrTimers]] # Keep a cache of completed instructions. Data for completed instructions # must be discarded. See input_elements() and _clean_receiving_queue(). @@ -474,7 +478,7 @@ def __init__(self, data_buffer_time_limit_ms=0): def close(self): # type: () -> None - self._to_send.put(self._WRITES_FINISHED) + self._to_send.put(self._WRITES_FINISHED, 0) self._closed = True def wait(self, timeout=None): @@ -482,7 +486,7 @@ def wait(self, timeout=None): self._reads_finished.wait(timeout) def _receiving_queue(self, instruction_id): - # type: (str) -> Optional[queue.Queue[DataOrTimers]] + # type: (str) -> Optional[ByteLimitedQueue[DataOrTimers]] """ Gets or creates queue for a instruction_id. Or, returns None if the @@ -585,21 +589,19 @@ def output_stream(self, instruction_id, transform_id): def add_to_send_queue(data): # type: (bytes) -> None if data: - self._to_send.put( - beam_fn_api_pb2.Elements.Data( - instruction_id=instruction_id, - transform_id=transform_id, - data=data)) + elem = beam_fn_api_pb2.Elements.Data( + instruction_id=instruction_id, transform_id=transform_id, data=data) + self._to_send.put(elem, self._get_element_size_bytes(elem)) def close_callback(data): # type: (bytes) -> None add_to_send_queue(data) # End of stream marker. - self._to_send.put( - beam_fn_api_pb2.Elements.Data( - instruction_id=instruction_id, - transform_id=transform_id, - is_last=True)) + elem = beam_fn_api_pb2.Elements.Data( + instruction_id=instruction_id, + transform_id=transform_id, + is_last=True) + self._to_send.put(elem, self._get_element_size_bytes(elem)) return ClosableOutputStream.create( close_callback, add_to_send_queue, self._data_buffer_time_limit_ms) @@ -614,23 +616,23 @@ def output_timer_stream( def add_to_send_queue(timer): # type: (bytes) -> None if timer: - self._to_send.put( - beam_fn_api_pb2.Elements.Timers( - instruction_id=instruction_id, - transform_id=transform_id, - timer_family_id=timer_family_id, - timers=timer, - is_last=False)) + elem = beam_fn_api_pb2.Elements.Timers( + instruction_id=instruction_id, + transform_id=transform_id, + timer_family_id=timer_family_id, + timers=timer, + is_last=False) + self._to_send.put(elem, self._get_element_size_bytes(elem)) def close_callback(timer): # type: (bytes) -> None add_to_send_queue(timer) - self._to_send.put( - beam_fn_api_pb2.Elements.Timers( - instruction_id=instruction_id, - transform_id=transform_id, - timer_family_id=timer_family_id, - is_last=True)) + elem = beam_fn_api_pb2.Elements.Timers( + instruction_id=instruction_id, + transform_id=transform_id, + timer_family_id=timer_family_id, + is_last=True) + self._to_send.put(elem, self._get_element_size_bytes(elem)) return ClosableOutputStream.create( close_callback, add_to_send_queue, self._data_buffer_time_limit_ms) @@ -665,6 +667,15 @@ def _write_outputs(self): raise ValueError('Unexpected output element type %s' % type(stream)) yield beam_fn_api_pb2.Elements(data=data_stream, timers=timer_stream) + def _get_element_size_bytes(self, element): + # type: (Union[beam_fn_api_pb2.Elements.Data, beam_fn_api_pb2.Elements.Timers]) -> int + if isinstance(element, beam_fn_api_pb2.Elements.Data): + return len(element.data) + elif isinstance(element, beam_fn_api_pb2.Elements.Timers): + return len(element.timers) + else: + return 0 + def _read_inputs(self, elements_iterator): # type: (Iterable[beam_fn_api_pb2.Elements]) -> None @@ -691,7 +702,8 @@ def _put_queue(instruction_id, element): next_discard_log_time = current_time + 10 return try: - input_queue.put(element, timeout=1) + input_queue.put( + element, self._get_element_size_bytes(element), timeout=1) return except queue.Full: current_time = time.time() diff --git a/sdks/python/apache_beam/utils/byte_limited_queue.pxd b/sdks/python/apache_beam/utils/byte_limited_queue.pxd new file mode 100644 index 000000000000..396185e8e101 --- /dev/null +++ b/sdks/python/apache_beam/utils/byte_limited_queue.pxd @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# cython: overflowcheck=True + +cdef class ByteLimitedQueue(object): + cdef readonly Py_ssize_t max_elements + cdef readonly Py_ssize_t max_bytes + cdef readonly Py_ssize_t _byte_size + cdef readonly object _mutex + cdef readonly object _not_empty + cdef readonly object _waiting_writers + cdef readonly list _condition_pool + cdef readonly object _queue + cdef readonly Py_ssize_t _blocked_bytes + + cpdef bint _can_fit(self, Py_ssize_t item_bytes) except -1 diff --git a/sdks/python/apache_beam/utils/byte_limited_queue.py b/sdks/python/apache_beam/utils/byte_limited_queue.py new file mode 100644 index 000000000000..a6ff669800c2 --- /dev/null +++ b/sdks/python/apache_beam/utils/byte_limited_queue.py @@ -0,0 +1,204 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A thread-safe queue that limits capacity by total byte size.""" + +import collections +import queue +import threading +import time +import types + + +class ByteLimitedQueue(object): + """A fair queue that limits by both element count and total byte size. + + A single element is allowed to exceed the maxbytes to avoid deadlock. + """ + __class_getitem__ = classmethod(types.GenericAlias) + + def __init__( + self, + maxsize=0, # type: int + maxbytes=0, # type: int + ): + # type: (...) -> None + + """Initializes a ByteLimitedQueue. + + Args: + maxsize: The maximum number of items allowed in the queue. If 0 or + negative, there is no limit on the number of elements. + maxbytes: The maximum accumulated bytes allowed in the queue. If 0 or + negative, there is no limit on the total bytes of the elements. + """ + self.max_elements = maxsize + self.max_bytes = maxbytes + + self._byte_size = 0 + self._blocked_bytes = 0 + self._mutex = threading.Lock() + self._not_empty = threading.Condition(self._mutex) + + self._waiting_writers = collections.deque() + self._condition_pool = [] + self._queue = collections.deque() + + def put(self, item, item_bytes, *, block=True, timeout=None): + """Put an item into the queue. + + If the queue is full, block until a free slot is available, unless `block` + is false or a timeout occurs. + + Args: + item: The item to put into the queue. + item_bytes: The size of the item. + block: If True, block until space is available. If False, raise queue.Full + immediately if the queue is full. + timeout: If block is True, wait for at most `timeout` seconds. If None, + block indefinitely. + + Raises: + ValueError: If timeout or item_bytes is negative. + queue.Full: If the queue is full and block is False or the timeout occurs. + """ + if timeout is not None and timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + if item_bytes < 0: + raise ValueError("'item_bytes' must be a non-negative number") + + with self._mutex: + if not self._waiting_writers and self._can_fit(item_bytes): + self._queue.append((item, item_bytes)) + self._byte_size += item_bytes + self._not_empty.notify() + return + + if not block: + raise queue.Full + + # Reuse or create a condition + my_cond = ( + self._condition_pool.pop() + if self._condition_pool else threading.Condition(self._mutex)) + + endtime = time.monotonic() + timeout if timeout is not None else None + + try: + self._blocked_bytes += item_bytes + self._waiting_writers.append(my_cond) + while True: + if timeout is None: + my_cond.wait() + else: + remaining = endtime - time.monotonic() + if remaining <= 0.0: + raise queue.Full + my_cond.wait(remaining) + + if self._waiting_writers[0] is my_cond and self._can_fit(item_bytes): + break + + self._queue.append((item, item_bytes)) + self._byte_size += item_bytes + self._not_empty.notify() + finally: + self._blocked_bytes -= item_bytes + if self._waiting_writers: + was_first = (self._waiting_writers[0] is my_cond) + if was_first: + self._waiting_writers.popleft() + else: + self._waiting_writers.remove(my_cond) + self._condition_pool.append(my_cond) + if was_first and self._waiting_writers: + self._waiting_writers[0].notify() + + def get(self, *, block=True, timeout=None): + """Remove and return an item from the queue. + + If the queue is empty, block until an item is available, unless `block` + is false or a timeout occurs. + + Args: + block: If True, block until an item is available. If False, raise + queue.Empty immediately if the queue is empty. + timeout: If block is True, wait for at most `timeout` seconds. If None, + block indefinitely. + + Returns: + The item removed from the queue. + + Raises: + ValueError: If timeout is negative. + queue.Empty: If the queue is empty and block is False or the timeout + occurs. + """ + if timeout is not None and timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + + with self._mutex: + if not block: + if not self._queue: + raise queue.Empty + elif timeout is None: + while not self._queue: + self._not_empty.wait() + else: + endtime = time.monotonic() + timeout + while not self._queue: + remaining = endtime - time.monotonic() + if remaining <= 0.0: + raise queue.Empty + self._not_empty.wait(remaining) + + item, item_bytes = self._queue.popleft() + self._byte_size -= item_bytes + + if self._waiting_writers: + self._waiting_writers[0].notify() + + return item + + def get_nowait(self): + """Remove and return an item from the queue without blocking.""" + return self.get(block=False) + + def byte_size(self): + """Return the total byte size of elements in the queue.""" + with self._mutex: + return self._byte_size + + def blocked_byte_size(self): + """Return the total byte size of elements in the queue that are blocked.""" + with self._mutex: + return self._blocked_bytes + + def qsize(self): + """Return the total number of elements in the queue.""" + with self._mutex: + return len(self._queue) + + def _can_fit(self, item_bytes): + # Always let in a single element, regardless of size. + if not self._queue: + return True + if self.max_elements > 0 and len(self._queue) >= self.max_elements: + return False + if self.max_bytes > 0 and self._byte_size + item_bytes > self.max_bytes: + return False + return True diff --git a/sdks/python/apache_beam/utils/byte_limited_queue_test.py b/sdks/python/apache_beam/utils/byte_limited_queue_test.py new file mode 100644 index 000000000000..27ccb2421844 --- /dev/null +++ b/sdks/python/apache_beam/utils/byte_limited_queue_test.py @@ -0,0 +1,270 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Unit tests for byte-limited queue.""" + +import queue +import threading +import time +import unittest + +from apache_beam.utils.byte_limited_queue import ByteLimitedQueue + + +class ByteLimitedQueueTest(unittest.TestCase): + def test_unbounded(self): + bq = ByteLimitedQueue() + for i in range(201): + bq.put(str(i), i) + self.assertEqual(bq.byte_size(), sum(range(201))) + self.assertEqual(bq.qsize(), 201) + + def test_put_and_get(self): + bq = ByteLimitedQueue(maxbytes=200) + bq.put('50', 50) + bq.put('140', 140) + self.assertEqual(bq.byte_size(), 190) + self.assertEqual(bq.qsize(), 2) + # Putting another would exceed 200. + with self.assertRaises(queue.Full): + bq.put('20', 20, block=False) + bq.put('10', 10, block=False) + self.assertEqual(bq.byte_size(), 200) + self.assertEqual(bq.qsize(), 3) + + self.assertEqual(bq.get(), '50') + self.assertEqual(bq.byte_size(), 150) + self.assertEqual(bq.qsize(), 2) + bq.put('20', 20, block=False) + + def test_dual_limit(self): + # Queue limits: at most 3 items, OR at most 100 item bytes. + bq = ByteLimitedQueue(maxsize=3, maxbytes=100) + bq.put('30', 30) + bq.put('40', 40) + bq.put('20', 20) + self.assertEqual(bq.byte_size(), 90) + self.assertEqual(bq.qsize(), 3) + # Full on element count (size=3). + with self.assertRaises(queue.Full): + bq.put('10', 10, block=False) + self.assertEqual(bq.get(), '30') + self.assertEqual(bq.get(), '40') + bq.put('10', 10) + # Full on byte count + with self.assertRaises(queue.Full): + bq.put('90', 90, block=False) + self.assertEqual(bq.get(), '20') + bq.put('90', 90, block=False) + + def test_multithreading(self): + bq = ByteLimitedQueue(maxsize=0, maxbytes=100) + received = [] + + def producer(): + for i in range(101): + bq.put(str(i), i) + + poison_pill = 'POISON' + + def consumer(): + while True: + item = bq.get() + if item == poison_pill: + break + received.append(int(item)) + + t1 = threading.Thread(target=producer) + t2 = threading.Thread(target=producer) + t3 = threading.Thread(target=consumer) + + t1.start() + t2.start() + t3.start() + + t1.join() + t2.join() + bq.put(poison_pill, 0) + + t3.join() + + self.assertEqual(len(received), 202) + self.assertEqual(sum(received), 2 * sum(range(101))) + + def test_put_timeout(self): + bq = ByteLimitedQueue(maxsize=0, maxbytes=10) + bq.put('10', 10) + + # The queue is completely full. A timeout put should raise queue.Full. + with self.assertRaises(queue.Full): + bq.put('5', 5, timeout=0.01) + + def delayed_consumer(): + time.sleep(0.05) + bq.get() + + # Start a thread that will free up space after 50ms. + t = threading.Thread(target=delayed_consumer) + t.start() + + # The put should succeed once the consumer runs, use a high timeout to + # flakiness. + bq.put('item', 5, timeout=60) + t.join() + + def test_get_timeout(self): + bq = ByteLimitedQueue(maxsize=0, maxbytes=100) + with self.assertRaises(queue.Empty): + bq.get(block=False) + with self.assertRaises(queue.Empty): + bq.get(timeout=0.0) + with self.assertRaises(queue.Empty): + bq.get(timeout=.01) + + bq.put('1', 1) + self.assertEqual('1', bq.get(timeout=0)) + + bq.put('2', 2) + self.assertEqual('2', bq.get(timeout=0.1)) + + def delayed_producer(): + time.sleep(0.05) + bq.put('3', 3) + + # Start a thread that will produce soon + t = threading.Thread(target=delayed_producer) + t.start() + + # The get should succeed once the produer runs, use a high timeout to + # flakiness. + self.assertEqual('3', bq.get(timeout=60)) + t.join() + + def test_negative_timeout(self): + bq = ByteLimitedQueue() + # Putting an item with a negative timeout should raise ValueError. + with self.assertRaises(ValueError): + bq.put('5', 5, timeout=-1) + with self.assertRaises(ValueError): + bq.get(timeout=-1) + + def test_single_element_override(self): + bq = ByteLimitedQueue(maxbytes=10) + # An item of size 50 exceeds maxbytes 10, but should be admitted + # immediately without blocking since the queue is currently empty! + bq.put('50', 50, block=False) + self.assertEqual(bq.qsize(), 1) + self.assertEqual(bq.byte_size(), 50) + + def test_fairness(self): + bq = ByteLimitedQueue(maxbytes=10) + # Put an initial item so that the queue is not empty, + # causing the subsequent large item to block. + bq.put('first', 2) + self.assertEqual(bq.blocked_byte_size(), 0) + + def producer(item, size): + bq.put(item, size) + + # Add an item in a background thread that should block due to exceeding + # the limit. + t1 = threading.Thread(target=producer, args=('too_large', 9)) + t1.start() + + # Wait until the background write is queued. + while bq.blocked_byte_size() < 1: + time.sleep(0.005) + self.assertEqual(bq.blocked_byte_size(), 9) + + # Add smaller items afterwards. + t2 = threading.Thread(target=producer, args=('small1', 1)) + t2.start() + + while bq.blocked_byte_size() < 10: + time.sleep(0.005) + self.assertEqual(bq.blocked_byte_size(), 10) + + t3 = threading.Thread(target=producer, args=('small2', 1)) + t3.start() + + while bq.blocked_byte_size() < 11: + time.sleep(0.005) + self.assertEqual(bq.blocked_byte_size(), 11) + + # Verify all items are received in order. + self.assertEqual(bq.get(), 'first') + t1.join() + t2.join() + self.assertEqual(bq.get(), 'too_large') + t3.join() + self.assertEqual(bq.get(), 'small1') + self.assertEqual(bq.get(), 'small2') + + def test_blocked_waiter_timeout_multiple(self): + bq = ByteLimitedQueue(maxbytes=10) + bq.put('initial', 5) + + status = [] + lock = threading.Lock() + + def producer(name, size, timeout_val): + try: + bq.put(name, size, timeout=timeout_val) + with lock: + status.append((name, 'success')) + except queue.Full: + with lock: + status.append((name, 'timeout')) + + threads = [] + threads.append(threading.Thread(target=producer, args=('t1', 8, 0.2))) + threads.append(threading.Thread(target=producer, args=('t2', 8, 60.0))) + threads.append(threading.Thread(target=producer, args=('t3', 3, 0.1))) + threads.append(threading.Thread(target=producer, args=('t4', 3, 60.0))) + threads.append(threading.Thread(target=producer, args=('t5', 3, 0.1))) + for t in threads: + t.start() + + # Wait for the short-timeout threads. + threads[4].join() + threads[2].join() + threads[0].join() + + # Now waiting writers should just be t1 and t3 + self.assertEqual(bq.blocked_byte_size(), 11) + + self.assertEqual(bq.get(), 'initial') + threads[1].join() + self.assertGreater(bq.blocked_byte_size(), 0) + + elem = bq.get() + self.assertTrue(elem == 't2' or elem == 't4') + threads[3].join() + self.assertEqual(bq.blocked_byte_size(), 0) + elem = bq.get() + self.assertTrue(elem == 't2' or elem == 't4') + + with lock: + self.assertIn(('t1', 'timeout'), status) + self.assertIn(('t2', 'success'), status) + self.assertIn(('t3', 'timeout'), status) + self.assertIn(('t4', 'success'), status) + self.assertIn(('t5', 'timeout'), status) + + +if __name__ == '__main__': + unittest.main() diff --git a/sdks/python/setup.py b/sdks/python/setup.py index b3fb98d8b0ef..45781a44c4b1 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -368,6 +368,7 @@ def get_portability_package_data(): 'apache_beam/runners/worker/operations.py', 'apache_beam/transforms/cy_combiners.py', 'apache_beam/transforms/stats.py', + 'apache_beam/utils/byte_limited_queue.py', 'apache_beam/utils/counters.py', 'apache_beam/utils/windowed_value.py', ]) From 9076a0c40a3a804a0cf4e1199db5c60932c90e14 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 12 May 2026 14:15:56 -0400 Subject: [PATCH 146/490] update ruff to pyproject (#38474) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d03641ffe669..5a42dfee100e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,4 +38,4 @@ repos: hooks: - id: ruff-check files: ^sdks/python/apache_beam/ - args: ["--config=sdks/python/ruff.toml"] + args: ["--config=sdks/python/pyproject.toml"] From 593b260a9717cbeb86820ecc6985809a7c44be46 Mon Sep 17 00:00:00 2001 From: claudevdm <33973061+claudevdm@users.noreply.github.com> Date: Tue, 12 May 2026 15:19:18 -0400 Subject: [PATCH 147/490] Expose StorageWriteApiMaxRequestCallbackWaitTimeSec in BQ storage write. (#38470) --- .../beam/sdk/io/gcp/bigquery/BigQueryOptions.java | 7 +++++++ .../beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java index 2f16a64b0d76..face2ef5841a 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java @@ -153,6 +153,13 @@ public interface BigQueryOptions void setStorageWriteMaxInflightBytes(Long value); + @Description( + "Maximum time in seconds a Storage Write API append request is allowed to wait in the " + + "request callback queue before timing out. Overrides Storage Write API default (5 min)") + Integer getStorageWriteApiMaxRequestCallbackWaitTimeSec(); + + void setStorageWriteApiMaxRequestCallbackWaitTimeSec(Integer value); + @Description( "Enables multiplexing mode, where multiple tables can share the same connection. Only available when writing with STORAGE_API_AT_LEAST_ONCE" + " mode. This is recommended if your write operation is creating 20+ connections. When using multiplexing, consider tuning " diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java index aa9a5fd310b0..14765a65ff0b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java @@ -1496,6 +1496,7 @@ public static class WriteStreamServiceImpl implements WriteStreamService { private final BigQueryWriteClient newWriteClient; private final long storageWriteMaxInflightRequests; private final long storageWriteMaxInflightBytes; + private final @Nullable Integer storageWriteApiMaxRequestCallbackWaitTimeSec; private final BigQueryIOMetadata bqIOMetadata; private final PipelineOptions options; @@ -1506,6 +1507,8 @@ public static class WriteStreamServiceImpl implements WriteStreamService { this.options = options; this.storageWriteMaxInflightRequests = bqOptions.getStorageWriteMaxInflightRequests(); this.storageWriteMaxInflightBytes = bqOptions.getStorageWriteMaxInflightBytes(); + this.storageWriteApiMaxRequestCallbackWaitTimeSec = + bqOptions.getStorageWriteApiMaxRequestCallbackWaitTimeSec(); this.bqIOMetadata = BigQueryIOMetadata.create(); } @@ -1514,6 +1517,8 @@ public WriteStreamServiceImpl(BigQueryOptions bqOptions) { this.options = bqOptions; this.storageWriteMaxInflightRequests = bqOptions.getStorageWriteMaxInflightRequests(); this.storageWriteMaxInflightBytes = bqOptions.getStorageWriteMaxInflightBytes(); + this.storageWriteApiMaxRequestCallbackWaitTimeSec = + bqOptions.getStorageWriteApiMaxRequestCallbackWaitTimeSec(); this.bqIOMetadata = BigQueryIOMetadata.create(); } @@ -1578,6 +1583,11 @@ public StreamAppendClient getStreamAppendClient( options.as(BigQueryOptions.class).getMaxConnectionPoolConnections()) .build()); + if (storageWriteApiMaxRequestCallbackWaitTimeSec != null) { + StreamWriter.setMaxRequestCallbackWaitTime( + java.time.Duration.ofSeconds(storageWriteApiMaxRequestCallbackWaitTimeSec)); + } + StreamWriter streamWriter = StreamWriter.newBuilder(streamName, newWriteClient) .setExecutorProvider( From 08ebf1e5ee103b86a545834f3f22ffc84d92ad1c Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 12 May 2026 15:20:59 -0400 Subject: [PATCH 148/490] Upgrade gcsio to 3.1.16 (#38419) --- .../beam/gradle/BeamModulePlugin.groovy | 6 +- .../sdk/extensions/gcp/util/GcsUtilV1.java | 62 +++++-------------- .../sdk/extensions/gcp/util/GcsUtilTest.java | 19 +++--- 3 files changed, 31 insertions(+), 56 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 2e064b367db3..4edcb0b84d6c 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -611,7 +611,7 @@ class BeamModulePlugin implements Plugin { def gax_version = "2.79.0" def google_ads_version = "33.0.0" def google_clients_version = "2.0.0" - def google_cloud_bigdataoss_version = "2.2.26" + def google_cloud_bigdataoss_version = "3.1.16" def google_code_gson_version = "2.10.1" def google_oauth_clients_version = "1.34.1" // [bomupgrader] determined by: io.grpc:grpc-netty, consistent with: google_cloud_platform_libraries_bom @@ -702,9 +702,9 @@ class BeamModulePlugin implements Plugin { aws_java_sdk2_profiles : "software.amazon.awssdk:profiles:$aws_java_sdk2_version", azure_sdk_bom : "com.azure:azure-sdk-bom:1.2.14", bigdataoss_gcsio : "com.google.cloud.bigdataoss:gcsio:$google_cloud_bigdataoss_version", - bigdataoss_gcs_connector : "com.google.cloud.bigdataoss:gcs-connector:hadoop2-$google_cloud_bigdataoss_version", + bigdataoss_gcs_connector : "com.google.cloud.bigdataoss:gcs-connector:$google_cloud_bigdataoss_version", bigdataoss_util : "com.google.cloud.bigdataoss:util:$google_cloud_bigdataoss_version", - bigdataoss_util_hadoop : "com.google.cloud.bigdataoss:util-hadoop:hadoop2-$google_cloud_bigdataoss_version", + bigdataoss_util_hadoop : "com.google.cloud.bigdataoss:util-hadoop:$google_cloud_bigdataoss_version", byte_buddy : "net.bytebuddy:byte-buddy:1.17.7", cassandra_driver_core : "com.datastax.cassandra:cassandra-driver-core:$cassandra_driver_version", cassandra_driver_mapping : "com.datastax.cassandra:cassandra-driver-mapping:$cassandra_driver_version", diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java index 1ade4be6fdb5..97778ac4e1df 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java @@ -30,7 +30,6 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpStatusCodes; -import com.google.api.client.http.HttpTransport; import com.google.api.client.util.BackOff; import com.google.api.client.util.Sleeper; import com.google.api.services.storage.Storage; @@ -53,7 +52,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.reflect.Method; import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; @@ -267,8 +265,12 @@ public boolean shouldRetry(IOException e) { .setReadChannelOptions(gcsReadOptions) .setGrpcEnabled(shouldUseGrpc) .build(); - googleCloudStorage = - createGoogleCloudStorage(googleCloudStorageOptions, storageClient, credentials); + try { + googleCloudStorage = + createGoogleCloudStorage(googleCloudStorageOptions, storageClient, credentials); + } catch (IOException e) { + throw new RuntimeException(e); + } this.batchRequestSupplier = () -> { // Capture reference to this so that the most recent storageClient and initializer @@ -725,48 +727,16 @@ public WritableByteChannel create(GcsPath path, CreateOptions options) throws IO } GoogleCloudStorage createGoogleCloudStorage( - GoogleCloudStorageOptions options, Storage storage, Credentials credentials) { - try { - return new GoogleCloudStorageImpl(options, storage, credentials); - } catch (NoSuchMethodError e) { - // gcs-connector 3.x drops the direct constructor and exclusively uses Builder - // TODO eliminate reflection once Beam drops Java 8 support and upgrades to gcsio 3.x - try { - final Method builderMethod = GoogleCloudStorageImpl.class.getMethod("builder"); - Object builder = builderMethod.invoke(null); - final Class builderClass = - Class.forName( - "com.google.cloud.hadoop.gcsio.AutoBuilder_GoogleCloudStorageImpl_Builder"); - - final Method setOptionsMethod = - builderClass.getMethod("setOptions", GoogleCloudStorageOptions.class); - setOptionsMethod.setAccessible(true); - builder = setOptionsMethod.invoke(builder, options); - - final Method setHttpTransportMethod = - builderClass.getMethod("setHttpTransport", HttpTransport.class); - setHttpTransportMethod.setAccessible(true); - builder = - setHttpTransportMethod.invoke(builder, storage.getRequestFactory().getTransport()); - - final Method setCredentialsMethod = - builderClass.getMethod("setCredentials", Credentials.class); - setCredentialsMethod.setAccessible(true); - builder = setCredentialsMethod.invoke(builder, credentials); - - final Method setHttpRequestInitializerMethod = - builderClass.getMethod("setHttpRequestInitializer", HttpRequestInitializer.class); - setHttpRequestInitializerMethod.setAccessible(true); - builder = setHttpRequestInitializerMethod.invoke(builder, httpRequestInitializer); - - final Method buildMethod = builderClass.getMethod("build"); - buildMethod.setAccessible(true); - return (GoogleCloudStorage) buildMethod.invoke(builder); - } catch (Exception reflectionError) { - throw new RuntimeException( - "Failed to construct GoogleCloudStorageImpl from gcsio 3.x Builder", reflectionError); - } - } + GoogleCloudStorageOptions options, Storage storage, Credentials credentials) + throws IOException { + return GoogleCloudStorageImpl.builder() + .setOptions(options) + .setHttpTransport(storage.getRequestFactory().getTransport()) + .setCredentials(credentials) + // gcsio 3 expects httpRequestInitializer to be either absent or + // com.google.cloud.hadoop.util.RetryHttpInitializer when credentials not provided + .setHttpRequestInitializer(credentials != null ? httpRequestInitializer : null) + .build(); } /** diff --git a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java index a2b0e0af502b..d32ca162e3fd 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java @@ -184,8 +184,8 @@ public void testCreationWithExplicitGoogleCloudStorageReadOptions() throws Excep GoogleCloudStorageReadOptions readOptions = GoogleCloudStorageReadOptions.builder() .setFadvise(GoogleCloudStorageReadOptions.Fadvise.AUTO) - .setSupportGzipEncoding(true) - .setFastFailOnNotFound(false) + .setGzipEncodingSupportEnabled(true) + .setFastFailOnNotFoundEnabled(false) .build(); GcsOptions pipelineOptions = PipelineOptionsFactory.as(GcsOptions.class); @@ -193,7 +193,10 @@ public void testCreationWithExplicitGoogleCloudStorageReadOptions() throws Excep GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); GoogleCloudStorage googleCloudStorageMock = Mockito.spy(GoogleCloudStorage.class); - Mockito.when(googleCloudStorageMock.open(Mockito.any(), Mockito.any())) + Mockito.when( + googleCloudStorageMock.open( + Mockito.any(StorageResourceId.class), + Mockito.any(GoogleCloudStorageReadOptions.class))) .thenReturn(Mockito.mock(SeekableByteChannel.class)); gcsUtil.delegate.setCloudStorageImpl(googleCloudStorageMock); @@ -1006,7 +1009,7 @@ public void testGCSChannelCloseIdempotent() throws IOException { GcsOptions pipelineOptions = gcsOptionsWithTestCredential(); GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); GoogleCloudStorageReadOptions readOptions = - GoogleCloudStorageReadOptions.builder().setFastFailOnNotFound(false).build(); + GoogleCloudStorageReadOptions.builder().setFastFailOnNotFoundEnabled(false).build(); gcsUtil.delegate.setCloudStorageImpl( GoogleCloudStorageOptions.builder() @@ -1026,7 +1029,7 @@ public void testGCSReadMetricsIsSet() { GcsOptions pipelineOptions = gcsOptionsWithTestCredential(); GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); GoogleCloudStorageReadOptions readOptions = - GoogleCloudStorageReadOptions.builder().setFastFailOnNotFound(true).build(); + GoogleCloudStorageReadOptions.builder().setFastFailOnNotFoundEnabled(true).build(); gcsUtil.delegate.setCloudStorageImpl( GoogleCloudStorageOptions.builder() .setAppName("Beam") @@ -1673,8 +1676,10 @@ public static GcsUtilV1Mock createMockWithMockStorage( .thenReturn(Channels.newChannel(new ByteArrayOutputStream())); } else { SeekableByteChannel seekableByteChannel = new SeekableInMemoryByteChannel(readPayload); - Mockito.when(googleCloudStorageMock.open(Mockito.any())).thenReturn(seekableByteChannel); - Mockito.when(googleCloudStorageMock.open(Mockito.any(), Mockito.any())) + Mockito.when(googleCloudStorageMock.open(Mockito.any(StorageResourceId.class))) + .thenReturn(seekableByteChannel); + Mockito.when( + googleCloudStorageMock.open(Mockito.any(StorageResourceId.class), Mockito.any())) .thenReturn(seekableByteChannel); } return gcsUtilMock; From ea7e30d59699b57afa9a97f04899e87be7e0902d Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 12 May 2026 15:36:09 -0400 Subject: [PATCH 149/490] Add retry in connecting manager in MultiProcessShared (#38456) --- .../apache_beam/utils/multi_process_shared.py | 17 +++++++++-- .../utils/multi_process_shared_test.py | 30 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/sdks/python/apache_beam/utils/multi_process_shared.py b/sdks/python/apache_beam/utils/multi_process_shared.py index b05fdd305a60..13a0f13e617b 100644 --- a/sdks/python/apache_beam/utils/multi_process_shared.py +++ b/sdks/python/apache_beam/utils/multi_process_shared.py @@ -38,6 +38,8 @@ import fasteners +from apache_beam.utils import retry + # In some python versions, there is a bug where AutoProxy doesn't handle # the kwarg 'manager_owned'. We implement our own backup here to make sure # we avoid this problem. More info here: @@ -391,10 +393,21 @@ def _get_manager(self): manager = _SingletonRegistrar( address=(host, int(port)), authkey=AUTH_KEY) multiprocessing.current_process().authkey = AUTH_KEY - try: + + retryable_exceptions = (ConnectionError, EOFError) + + @retry.with_exponential_backoff( + num_retries=5, + initial_delay_secs=0.1, + retry_filter=lambda exn: isinstance( + exn, retryable_exceptions)) + def connect_manager(): manager.connect() + + try: + connect_manager() self._manager = manager - except ConnectionError: + except retryable_exceptions: # The server is no longer good, assume it died. os.unlink(address_file) diff --git a/sdks/python/apache_beam/utils/multi_process_shared_test.py b/sdks/python/apache_beam/utils/multi_process_shared_test.py index 3c74903b8d99..18ed49c6fa17 100644 --- a/sdks/python/apache_beam/utils/multi_process_shared_test.py +++ b/sdks/python/apache_beam/utils/multi_process_shared_test.py @@ -23,6 +23,7 @@ import threading import unittest from typing import Any +from unittest.mock import patch from apache_beam.utils import multi_process_shared @@ -293,7 +294,8 @@ def setUp(self): 'mix1', 'mix2', 'test_process_exit', - 'thundering_herd_test']: + 'thundering_herd_test', + 'transient_test']: for ext in ['', '.address', '.address.error']: try: os.remove(os.path.join(tempdir, tag + ext)) @@ -461,6 +463,32 @@ def test_zombie_reaping_on_acquire(self): except Exception: pass + def test_transient_connection_error_recovery(self): + shared1 = multi_process_shared.MultiProcessShared( + Counter, tag='transient_test', always_proxy=True, spawn_process=True) + shared2 = multi_process_shared.MultiProcessShared( + Counter, tag='transient_test', always_proxy=True, spawn_process=True) + + counter1 = shared1.acquire() + + orig_connect = multi_process_shared._SingletonRegistrar.connect + connect_calls = [0] + + def side_effect_connect(self_mgr, *args, **kwargs): + connect_calls[0] += 1 + if connect_calls[0] == 1: + raise ConnectionError("Simulated transient connection failure") + return orig_connect(self_mgr, *args, **kwargs) + + with patch.object(multi_process_shared._SingletonRegistrar, + 'connect', + autospec=True, + side_effect=side_effect_connect): + counter2 = shared2.acquire() + + self.assertEqual(counter1.increment(), 1) + self.assertEqual(counter2.increment(), 2) + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) From b7097b888d232e139e4d8abb8d81684ff8693933 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 12 May 2026 15:45:58 -0400 Subject: [PATCH 150/490] [Prism] Fix gRPC deadline exceeded errors during bundle failure by passing errgroup context (#38472) * Add go test to reproduce the deadline exceed errors when a dofn fails * Add python unit test to reproduce it. * Change the context to egctx so a bundle failure will cancel other bundle execution. * Fix lints. * Remove unused import. * Move test to a test class that use built prism during vr test. * Remove the new python test due to flakiness. --- .../beam/runners/prism/internal/execute.go | 6 ++++- .../runners/prism/internal/execute_test.go | 22 +++++++++++++++++++ .../runners/prism/internal/testdofns_test.go | 5 +++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/sdks/go/pkg/beam/runners/prism/internal/execute.go b/sdks/go/pkg/beam/runners/prism/internal/execute.go index 853b7974479d..f6e148f9f3f6 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/execute.go +++ b/sdks/go/pkg/beam/runners/prism/internal/execute.go @@ -376,7 +376,11 @@ func executePipeline(ctx context.Context, wks map[string]*worker.W, j *jobservic eg.Go(func() error { s := stages[rb.StageID] wk := wks[s.envID] - if err := s.Execute(ctx, j, wk, comps, em, rb); err != nil { + // Pass egctx instead of the parent ctx so that when any bundle fails, + // the errgroup cancels egctx and all other concurrent bundle execution + // goroutines immediately detect cancellation and abort. This prevents + // eg.Wait() from blocking indefinitely and allows prompt error reporting. + if err := s.Execute(egctx, j, wk, comps, em, rb); err != nil { // Ensure we clean up on bundle failure j.Logger.Error("Bundle Failed.", slog.Any("error", err)) em.FailBundle(rb) diff --git a/sdks/go/pkg/beam/runners/prism/internal/execute_test.go b/sdks/go/pkg/beam/runners/prism/internal/execute_test.go index 29fccaeb238e..2bb73f20e200 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/execute_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/execute_test.go @@ -519,6 +519,28 @@ func TestFailure(t *testing.T) { } } +func TestFailureHang(t *testing.T) { + initRunner(t) + + p, s := beam.NewPipelineWithRoot() + imp := beam.Impulse(s) + col1 := beam.ParDo(s, doFnBlock, imp) + col2 := beam.ParDo(s, doFnFail, imp) + beam.ParDo(s, &int64Check{Name: "block", Want: []int{}}, col1) + beam.ParDo(s, &int64Check{Name: "fail", Want: []int{}}, col2) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err := executeWithT(ctx, t, p) + if err == nil { + t.Fatalf("expected pipeline failure, but got a success") + } + if want := "doFnFail: failing as intended"; !strings.Contains(err.Error(), want) { + t.Fatalf("expected pipeline failure with %q, but was %v", want, err) + } +} + func TestRunner_Passert(t *testing.T) { initRunner(t) tests := []struct { diff --git a/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go b/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go index 334d74fcae1d..d21ccd53afd0 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go @@ -59,6 +59,7 @@ func init() { register.Function3x0(dofn1Counter) register.Function2x0(dofnSink) register.Function3x1(doFnFail) + register.Function3x0(doFnBlock) register.Function2x1(combineIntSum) @@ -283,6 +284,10 @@ func doFnFail(ctx context.Context, _ []byte, emit func(int64)) error { return fmt.Errorf("doFnFail: failing as intended") } +func doFnBlock(ctx context.Context, _ []byte, emit func(int64)) { + <-ctx.Done() +} + func combineIntSum(a, b int64) int64 { return a + b } From 15a2e038d6f7ca75d3a914b8abbb1acedb3e86a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Wed, 13 May 2026 09:26:25 +0200 Subject: [PATCH 151/490] [Website] add drain update to docs (#38450) * add drain docs --- CHANGES.md | 1 + .../site/content/en/blog/looping-timers.md | 11 ++- .../en/documentation/programming-guide.md | 83 +++++++++++++++++++ website/www/site/data/capability_matrix.yaml | 2 +- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7c253e8cdeef..058e19ea7435 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,7 @@ ## New Features / Improvements +* Capability introduces an indicator for aggregations and timers firing during a pipeline drain, allowing users and sinks to recognize and appropriately handle potentially incomplete or partial data ([#36884](https://github.com/apache/beam/issues/36884)). * Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Go/Python) ([#38349](https://github.com/apache/beam/issues/38349)). * TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to encode finished bitset. SentinelBitSetCoder and BitSetCoder are state diff --git a/website/www/site/content/en/blog/looping-timers.md b/website/www/site/content/en/blog/looping-timers.md index 2690dc967ecf..ad13aae75444 100644 --- a/website/www/site/content/en/blog/looping-timers.md +++ b/website/www/site/content/en/blog/looping-timers.md @@ -221,11 +221,18 @@ public static class LoopingStatefulTimer extends DoFn, KV key, - @TimerId("loopingTimer") Timer loopingTimer) { + @TimerId("loopingTimer") Timer loopingTimer, + CausedByDrain drain) { LOG.info("Timer @ {} fired", c.timestamp()); c.output(KV.of(key.read(), 0)); + // Check if drain is in progress and avoid resetting the timer + if (drain == CausedByDrain.CAUSED_BY_DRAIN) { + LOG.info("Drain in progress, stopping looping timer."); + return; + } + // If we do not put in a “time to live” value, then the timer would loop forever Instant nextTimer = c.timestamp().plus(Duration.standardMinutes(1)); if (nextTimer.isBefore(stopTimerTime)) { @@ -347,4 +354,4 @@ support for dealing with this use case in production. Runner specific notes: -Google Cloud Dataflow Runners Drain feature does not support looping timers (Link to matrix) +Support for cancelling looping timers on drain is currently limited to Dataflow and is being implemented (see [Issue #36884](https://github.com/apache/beam/issues/36884)). diff --git a/website/www/site/content/en/documentation/programming-guide.md b/website/www/site/content/en/documentation/programming-guide.md index 343fb128b3ef..3f37f45bace6 100644 --- a/website/www/site/content/en/documentation/programming-guide.md +++ b/website/www/site/content/en/documentation/programming-guide.md @@ -7501,6 +7501,89 @@ class BufferDoFn(DoFn): {{< code_sample "sdks/go/examples/snippets/04transforms.go" batching_dofn_example >}} {{< /highlight >}} +#### 11.5.3. Looping timers {#looping-timers} + +Looping timers are a pattern where a timer sets another timer for a future time, creating a loop. This is useful for producing periodic outputs or heartbeats in the absence of data for a specific key. + +When draining a pipeline, it is important to terminate these loops to allow the pipeline to finish. In the Java SDK, you can use the `CausedByDrain` parameter in the `@OnTimer` method to check if the timer firing was induced by a drain operation. **Note:** `CausedByDrain` will be set only in certain runners. Check the [capability matrix](/documentation/runners/capability-matrix/) for more details. + +{{< highlight java >}} +public static class LoopingStatefulTimer extends DoFn, KV> { + @TimerId("loopingTimer") private final TimerSpec loopingTimer = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void process( + @Element KV element, + @TimerId("loopingTimer") Timer timer, + OutputReceiver> output) { + + // Set initial timer + timer.offset(Duration.standardMinutes(1)).setRelative(); + output.output(element); + } + + @OnTimer("loopingTimer") + public void onTimer( + @Key String key, + @TimerId("loopingTimer") Timer timer, + OutputReceiver> output, + CausedByDrain drain) { + + output.output(KV.of(key, 0)); + + // Cancel looping timer if drain is in progress + if (drain == CausedByDrain.CAUSED_BY_DRAIN) { + return; + } + + // Set next timer + timer.offset(Duration.standardMinutes(1)).setRelative(); + } +} +{{< /highlight >}} + +{{< highlight py >}} +# Python does not currently support detecting drain in OnTimer. +# The following example demonstrates a looping timer without drain support, +# using event time. + +class LoopingTimerDoFn(DoFn): + TIMER = TimerSpec('timer', TimeDomain.WATERMARK) + + def process(self, element, ts=DoFn.TimestampParam, timer=DoFn.TimerParam(TIMER)): + timer.set(ts + Duration(seconds=60)) + yield element + + @on_timer(TIMER) + def on_timer(self, key=DoFn.KeyParam, timestamp=DoFn.TimestampParam, timer=DoFn.TimerParam(TIMER)): + yield (key, 0) + # Loops forever, cannot handle drain safely if it never stops. + timer.set(timestamp + Duration(seconds=60)) +{{< /highlight >}} + +{{< highlight go >}} +// Go does not currently support detecting drain in OnTimer. +// The following example demonstrates a looping timer without drain support, +// using event time. + +type LoopingTimerFn struct { + Timer timers.EventTime +} + +func (fn *LoopingTimerFn) ProcessElement(et beam.EventTime, sp state.Provider, tp timers.Provider, key string, value int, emit func(string, int)) { + nextTime := et.ToTime().Add(60 * time.Second) + fn.Timer.Set(tp, nextTime) + emit(key, value) +} + +func (fn *LoopingTimerFn) OnTimer(et beam.EventTime, sp state.Provider, tp timers.Provider, key string, timer timers.Context, emit func(string, int)) { + emit(key, 0) + // Loops forever, cannot handle drain safely if it never stops. + nextTime := et.ToTime().Add(60 * time.Second) + fn.Timer.Set(tp, nextTime) +} +{{< /highlight >}} + ## 12. Splittable `DoFns` {#splittable-dofns} diff --git a/website/www/site/data/capability_matrix.yaml b/website/www/site/data/capability_matrix.yaml index b7c236865ef3..a1afdc6f8abc 100644 --- a/website/www/site/data/capability_matrix.yaml +++ b/website/www/site/data/capability_matrix.yaml @@ -1475,7 +1475,7 @@ capability-matrix: - class: dataflow l1: "Partially" l2: - l3: Dataflow has a native drain operation, but it does not work in the presence of event time timer loops. Final implemention pending model support. + l3: Dataflow has a native drain operation, support for event time timer loops drain is limited to Non-portable runner. - class: prism l1: "No" l2: From 67c899bbb55475da97494c94355cdbf03234921c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Wed, 13 May 2026 09:27:56 +0200 Subject: [PATCH 152/490] Remove legacy processContext usage across and replace it with argument provider (#38366) Starting with examples for java and kotlin. --- .../iceberg/IcebergBatchWriteExample.java | 15 +-- .../beam/examples/SchemaTransformExample.java | 10 +- .../beam/examples/SqlTransformExample.java | 10 +- .../examples/ApproximateQuantilesExample.java | 10 +- .../examples/CoCombineTransformExample.java | 19 ++-- .../beam/examples/CoGroupByKeyExample.java | 10 +- .../apache/beam/examples/CombineExample.java | 10 +- .../apache/beam/examples/CountExample.java | 10 +- .../beam/examples/CountPerKeyExample.java | 10 +- .../apache/beam/examples/CreateExample.java | 10 +- .../beam/examples/DebuggingWordCount.java | 15 ++- .../apache/beam/examples/DistinctExample.java | 10 +- .../beam/examples/FlatMapElementsExample.java | 10 +- .../examples/GroupIntoBatchesExample.java | 10 +- .../examples/KafkaPassengerCountJson.java | 21 ++-- .../apache/beam/examples/KafkaStreaming.java | 33 +++--- .../beam/examples/KafkaWordCountAvro.java | 11 +- .../beam/examples/KafkaWordCountJson.java | 11 +- .../org/apache/beam/examples/KeysExample.java | 10 +- .../apache/beam/examples/KvSwapExample.java | 10 +- .../apache/beam/examples/LatestExample.java | 10 +- .../beam/examples/MapElementsExample.java | 10 +- .../org/apache/beam/examples/MaxExample.java | 10 +- .../beam/examples/MaxPerKeyExample.java | 10 +- .../org/apache/beam/examples/MeanExample.java | 10 +- .../beam/examples/MeanPerKeyExample.java | 10 +- .../org/apache/beam/examples/MinExample.java | 10 +- .../beam/examples/MinPerKeyExample.java | 10 +- .../beam/examples/PartitionExample.java | 10 +- .../examples/RateLimitedPubSubReader.java | 6 +- .../beam/examples/RateLimiterSimple.java | 8 +- .../apache/beam/examples/RegexExample.java | 10 +- .../apache/beam/examples/SampleExample.java | 10 +- .../org/apache/beam/examples/SumExample.java | 10 +- .../beam/examples/SumPerKeyExample.java | 10 +- .../apache/beam/examples/ToStringExample.java | 10 +- .../org/apache/beam/examples/TopExample.java | 10 +- .../apache/beam/examples/ValuesExample.java | 10 +- .../org/apache/beam/examples/ViewExample.java | 19 ++-- .../apache/beam/examples/WindowExample.java | 10 +- .../beam/examples/complete/AutoComplete.java | 60 ++++++---- .../complete/StreamingWordExtract.java | 16 +-- .../apache/beam/examples/complete/TfIdf.java | 77 ++++++++----- .../complete/TopWikipediaSessions.java | 32 ++++-- .../examples/complete/TrafficMaxLaneFlow.java | 32 ++++-- .../beam/examples/complete/TrafficRoutes.java | 48 +++++--- .../transforms/DataProtectors.java | 21 ++-- .../transforms/io/TokenizationBigQueryIO.java | 7 +- .../transforms/io/TokenizationBigTableIO.java | 6 +- .../datatokenization/utils/CsvConverters.java | 107 ++++++++++++------ .../utils/ErrorConverters.java | 30 +++-- .../examples/complete/game/GameStats.java | 45 ++++---- .../complete/game/HourlyTeamScore.java | 6 +- .../examples/complete/game/LeaderBoard.java | 18 ++- .../complete/game/StatefulTeamScore.java | 18 +-- .../examples/complete/game/UserScore.java | 14 ++- .../complete/game/utils/WriteToBigQuery.java | 22 +++- .../complete/game/utils/WriteToText.java | 21 +++- .../game/utils/WriteWindowedToBigQuery.java | 15 ++- .../cookbook/BigQueryStreamingTornadoes.java | 21 ++-- .../examples/cookbook/BigQueryTornadoes.java | 16 +-- .../cookbook/CombinePerKeyExamples.java | 14 ++- .../examples/cookbook/FilterExamples.java | 31 ++--- .../beam/examples/cookbook/JoinExamples.java | 32 +++--- .../examples/cookbook/MaxPerKeyExamples.java | 17 +-- .../cookbook/MinimalBigQueryTornadoes.java | 20 ++-- .../examples/cookbook/TriggerExample.java | 47 +++++--- .../beam/examples/snippets/Snippets.java | 40 ++++--- .../subprocess/ExampleEchoPipeline.java | 10 +- .../examples/cookbook/TriggerExampleTest.java | 8 +- .../subprocess/ExampleEchoPipelineTest.java | 10 +- .../examples/kotlin/DebuggingWordCount.kt | 12 +- .../kotlin/cookbook/BigQueryTornadoes.kt | 15 +-- .../kotlin/cookbook/CombinePerKeyExamples.kt | 13 ++- .../kotlin/cookbook/FilterExamples.kt | 30 ++--- .../examples/kotlin/cookbook/JoinExamples.kt | 31 ++--- .../kotlin/cookbook/MaxPerKeyExamples.kt | 15 +-- .../kotlin/cookbook/TriggerExample.kt | 42 ++++--- .../beam/examples/kotlin/snippets/Snippets.kt | 13 ++- 79 files changed, 898 insertions(+), 592 deletions(-) diff --git a/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java b/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java index 2a5f85e524ed..167d3017c3d9 100644 --- a/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java +++ b/examples/java/iceberg/src/main/java/org/apache/beam/examples/iceberg/IcebergBatchWriteExample.java @@ -28,6 +28,8 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -74,9 +76,8 @@ private static Row flattenAnalyticsRow(Row row) { static class ExtractBrowserTransactionsFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext c) { - Row row = c.element(); - c.output( + public void processElement(@Element Row row, OutputReceiver> receiver) { + receiver.output( KV.of( Preconditions.checkStateNotNull(row.getString("browser")), Preconditions.checkStateNotNull(row.getInt64("transactions")))); @@ -85,13 +86,13 @@ public void processElement(ProcessContext c) { static class FormatCountsFn extends DoFn, Row> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement(@Element KV element, OutputReceiver receiver) { Row row = Row.withSchema(AGGREGATED_SCHEMA) - .withFieldValue("browser", c.element().getKey()) - .withFieldValue("transaction_count", c.element().getValue()) + .withFieldValue("browser", element.getKey()) + .withFieldValue("transaction_count", element.getValue()) .build(); - c.output(row); + receiver.output(row); } } diff --git a/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java b/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java index 5b8e4a34cf2c..505252e76a6d 100644 --- a/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java +++ b/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java @@ -22,7 +22,7 @@ // description: Demonstration of Schema transform usage. // multifile: false // default_example: false -// context_line: 60 +// context_line: 65 // categories: // - Schemas // - Combiners @@ -42,6 +42,8 @@ import org.apache.beam.sdk.schemas.transforms.Select; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; @@ -101,9 +103,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java b/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java index 9c2302c3de2f..f2a3a21195f8 100644 --- a/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java +++ b/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java @@ -22,7 +22,7 @@ // description: Demonstration of SQL transform usage. // multifile: false // default_example: false -// context_line: 60 +// context_line: 62 // categories: // - Beam SQL // - Combiners @@ -41,6 +41,8 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; @@ -95,9 +97,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java b/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java index 9e2a96b1eca9..daf943484ff2 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java @@ -24,6 +24,8 @@ import org.apache.beam.sdk.transforms.ApproximateQuantiles; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.slf4j.Logger; @@ -34,7 +36,7 @@ // description: Demonstration of ApproximateQuantiles transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -70,9 +72,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java b/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java index 0ee7d7bcac89..a46ac539aff6 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java @@ -22,7 +22,7 @@ // description: Demonstration of Composed Combine transform usage. // multifile: false // default_example: false -// context_line: 143 +// context_line: 145 // categories: // - Schemas // - Combiners @@ -46,6 +46,8 @@ import org.apache.beam.sdk.transforms.CombineFns; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; @@ -185,13 +187,16 @@ public Long apply(Long input) { new DoFn< KV, KV>>>() { @ProcessElement - public void processElement(ProcessContext c) throws Exception { - CombineFns.CoCombineResult e = c.element().getValue(); + public void processElement( + @Element KV element, + OutputReceiver>>> receiver) + throws Exception { + CombineFns.CoCombineResult e = element.getValue(); ArrayList> o = new ArrayList>(); o.add(KV.of(minTag.getId(), e.get(minTag))); o.add(KV.of(maxTag.getId(), e.get(maxTag))); o.add(KV.of(sumTag.getId(), e.get(sumTag))); - c.output(KV.of(c.element().getKey(), o)); + receiver.output(KV.of(element.getKey(), o)); } })); @@ -210,9 +215,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java index c77708b5de20..a04c9256a1e0 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.join.CoGbkResult; import org.apache.beam.sdk.transforms.join.CoGroupByKey; @@ -37,7 +39,7 @@ // description: Demonstration of CoGroupByKey transform usage. // multifile: false // default_example: false -// context_line: 54 +// context_line: 56 // categories: // - Core Transforms // - Joins @@ -84,9 +86,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java b/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java index 24bed27c2360..c4952bfb8d97 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java @@ -23,6 +23,8 @@ import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.values.PCollection; @@ -34,7 +36,7 @@ // description: Demonstration of Combine transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // - Combiners @@ -68,9 +70,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CountExample.java b/examples/java/src/main/java/org/apache/beam/examples/CountExample.java index cb0bd0ecf943..80e01b5d6b7f 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CountExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CountExample.java @@ -23,6 +23,8 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.slf4j.Logger; @@ -33,7 +35,7 @@ // description: Demonstration of Count transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC @@ -63,9 +65,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java index 9bf9bf1ef00f..017a736e56f6 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java @@ -23,6 +23,8 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -34,7 +36,7 @@ // description: Demonstration of Count.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -67,9 +69,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java b/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java index 5943ffa489d4..4052a9031470 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java @@ -27,6 +27,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -38,7 +40,7 @@ // description: Demonstration of Create transform usage. // multifile: false // default_example: false -// context_line: 51 +// context_line: 53 // categories: // - Core Transforms // complexity: BASIC @@ -79,9 +81,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java b/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java index 7c54e238da33..693aadc7850e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java +++ b/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java @@ -23,7 +23,7 @@ // filter("Flourish|stomach"). // multifile: false // pipeline_options: --output output.txt -// context_line: 180 +// context_line: 182 // categories: // - Debugging // - Filtering @@ -46,6 +46,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -115,18 +117,19 @@ public FilterTextFn(String pattern) { private final Counter unmatchedWords = Metrics.counter(FilterTextFn.class, "unmatchedWords"); @ProcessElement - public void processElement(ProcessContext c) { - if (filter.matcher(c.element().getKey()).matches()) { + public void processElement( + @Element KV element, OutputReceiver> receiver) { + if (filter.matcher(element.getKey()).matches()) { // Log at the "DEBUG" level each element that we match. When executing this pipeline // these log lines will appear only if the log level is set to "DEBUG" or lower. - LOG.debug("Matched: {}", c.element().getKey()); + LOG.debug("Matched: {}", element.getKey()); matchedWords.inc(); - c.output(c.element()); + receiver.output(element); } else { // Log at the "TRACE" level each element that is not matched. Different log levels // can be used to control the verbosity of logging providing an effective mechanism // to filter less important information. - LOG.trace("Did not match: {}", c.element().getKey()); + LOG.trace("Did not match: {}", element.getKey()); unmatchedWords.inc(); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java b/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java index d3ff9d663f44..3dc5c776f2ba 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java @@ -23,6 +23,8 @@ import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.Distinct; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.slf4j.Logger; @@ -33,7 +35,7 @@ // description: Demonstration of Distinct transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -68,9 +70,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java index 71d05ac7ade3..d944ab75ebf0 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java @@ -24,6 +24,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.FlatMapElements; import org.apache.beam.sdk.transforms.InferableFunction; import org.apache.beam.sdk.transforms.ParDo; @@ -36,7 +38,7 @@ // description: Demonstration of FlatMapElements transform usage. // multifile: false // default_example: false -// context_line: 50 +// context_line: 52 // categories: // - Core Transforms // complexity: BASIC @@ -78,9 +80,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java b/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java index 78e898b9c173..012b951fe8a9 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.GroupIntoBatches; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of GroupIntoBatches transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -74,9 +76,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java index 20f70232ae94..144093b704a7 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java @@ -22,7 +22,7 @@ // description: Read NYC Taxi dataset from Kafka server to count passengers for each vendor // multifile: false // default_example: false -// context_line: 64 +// context_line: 81 // categories: // - Emulated Data Source // - IO @@ -55,6 +55,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.transforms.Values; @@ -109,10 +111,13 @@ public static void main(String[] args) { ParDo.of( new DoFn>() { @ProcessElement - public void processElement(ProcessContext c) throws JsonProcessingException { + public void processElement( + @Element String element, OutputReceiver> receiver) + throws JsonProcessingException { final VendorToPassengerDTO result = - om.readValue(c.element(), new TypeReference() {}); - c.output(KV.of(result.getVendorIdField(), result.getPassengerCountField())); + om.readValue(element, new TypeReference() {}); + receiver.output( + KV.of(result.getVendorIdField(), result.getPassengerCountField())); } })) .apply( @@ -124,11 +129,11 @@ public void processElement(ProcessContext c) throws JsonProcessingException { new DoFn, KV>() { @ProcessElement public void processElement( - ProcessContext c, OutputReceiver> out) { + OutputReceiver> out, + @Element KV element) { System.out.printf( - "Vendor: %s, Passengers: %s%n", - c.element().getKey(), c.element().getValue()); - out.output(c.element()); + "Vendor: %s, Passengers: %s%n", element.getKey(), element.getValue()); + out.output(element); } })); p.run().waitUntilFinish(); diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java index f0b09226865a..83c36d18925a 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java @@ -21,7 +21,7 @@ // name: KafkaStreaming // description: Example of streaming data processing using Kafka // multifile: false -// context_line: 186 +// context_line: 113 // never_run: true // always_run: true // categories: @@ -49,6 +49,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; @@ -175,8 +177,9 @@ static class RandomUserScoreGeneratorFn extends DoFn private static final Random RANDOM = new Random(); @ProcessElement - public void processElement(ProcessContext c) { - c.output(generate()); + public void processElement( + @Element Object element, OutputReceiver> receiver) { + receiver.output(generate()); } public KV generate() { @@ -290,17 +293,17 @@ public Map extractOutput(Map accumulator) { static class LogResults extends DoFn, Map> { @ProcessElement - public void processElement(ProcessContext c, IntervalWindow w) throws Exception { - Map map = c.element(); - if (map == null) { - c.output(c.element()); - return; - } + public void processElement( + PaneInfo pane, + IntervalWindow w, + @Element Map scores, + OutputReceiver> receiver) + throws Exception { String startTime = w.start().toString(dateTimeFormatter); String endTime = w.end().toString(dateTimeFormatter); - PaneInfo.Timing timing = c.pane().getTiming(); + PaneInfo.Timing timing = pane.getTiming(); switch (timing) { case EARLY: @@ -316,7 +319,7 @@ public void processElement(ProcessContext c, IntervalWindow w) throws Exception throw new RuntimeException("Unknown timing value"); } - for (Map.Entry entry : map.entrySet()) { + for (Map.Entry entry : scores.entrySet()) { System.out.printf("%10s: %-10s%n", entry.getKey(), entry.getValue()); } @@ -326,7 +329,7 @@ public void processElement(ProcessContext c, IntervalWindow w) throws Exception System.out.println(); } - c.output(c.element()); + receiver.output(scores); } } @@ -340,9 +343,9 @@ public PCollection expand(PCollection input) { static class LogErrorFn extends DoFn { @ProcessElement - public void processElement(@Element BadRecord record, OutputReceiver receiver) { - System.out.println(record); - receiver.output(record); + public void processElement(@Element BadRecord badRecord, OutputReceiver receiver) { + System.out.println(badRecord); + receiver.output(badRecord); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java index 9e2da248017d..50a5e746d0c4 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java @@ -21,7 +21,7 @@ // name: KafkaWordCountAvro // description: Read CountWords dataset (CountWords.avro) from Kafka to count words // multifile: false -// context_line: 64 +// context_line: 66 // categories: // - Emulated Data Source // - IO @@ -51,6 +51,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -103,10 +105,11 @@ public static void main(String[] args) { ParDo.of( new DoFn() { @ProcessElement - public void processElement(ProcessContext c) { - for (String word : c.element().split(TOKENIZER_PATTERN, 0)) { + public void processElement( + @Element String element, OutputReceiver receiver) { + for (String word : element.split(TOKENIZER_PATTERN, 0)) { if (!word.isEmpty()) { - c.output(word); + receiver.output(word); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java index 355614b43869..e0d0976a1053 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java @@ -21,7 +21,7 @@ // name: KafkaWordCountJson // description: Read CountWords dataset (CountWords.json) from Kafka to count words // multifile: false -// context_line: 65 +// context_line: 67 // categories: // - Emulated Data Source // - IO @@ -52,6 +52,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -104,10 +106,11 @@ public static void main(String[] args) { ParDo.of( new DoFn() { @ProcessElement - public void processElement(ProcessContext c) { - for (String word : c.element().split(TOKENIZER_PATTERN, 0)) { + public void processElement( + @Element String element, OutputReceiver receiver) { + for (String word : element.split(TOKENIZER_PATTERN, 0)) { if (!word.isEmpty()) { - c.output(word); + receiver.output(word); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java b/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java index 155834bc0a43..725fb3586fe5 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Keys; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Keys transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -70,9 +72,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java b/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java index 090779de7c36..f91623f24d3c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.KvSwap; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of KvSwap transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -69,9 +71,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java b/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java index 5e9662a2f1ab..691727382cef 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Latest; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.WithTimestamps; @@ -36,7 +38,7 @@ // description: Demonstration of Latest transform usage. // multifile: false // default_example: false -// context_line: 49 +// context_line: 51 // categories: // - Core Transforms // complexity: BASIC @@ -84,9 +86,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java index 93d885750ae8..08a11ccbbb19 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -34,7 +36,7 @@ // description: Demonstration of MapElements transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -76,9 +78,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java b/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java index 9173d11754a3..ba164aa6c435 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; @@ -33,7 +35,7 @@ // description: Demonstration of Max transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC @@ -63,9 +65,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java index f5eda8179929..ae0a60664ed4 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Max.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -67,9 +69,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java b/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java index a31907977dfb..506d4a6196ff 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; @@ -33,7 +35,7 @@ // description: Demonstration of Mean transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC @@ -63,9 +65,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java index aecccee067e4..a1e3a1f6ff58 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Mean.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -67,9 +69,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MinExample.java b/examples/java/src/main/java/org/apache/beam/examples/MinExample.java index a76bcdc5ee3f..17a4e0d6554f 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MinExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MinExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; @@ -33,7 +35,7 @@ // description: Demonstration of Min transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC @@ -63,9 +65,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java index d3c0312feaf3..6fb8a0468ae0 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Min; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Min.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -67,9 +69,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("LogOutput: {} {}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("LogOutput: {} {}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java b/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java index b34f2bdd16bf..780c795b0874 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java @@ -32,6 +32,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Partition; import org.apache.beam.sdk.values.PCollection; @@ -44,7 +46,7 @@ // description: Demonstration of Partition transform usage. // multifile: false // default_example: false -// context_line: 58 +// context_line: 60 // categories: // - Core Transforms // - Coders @@ -192,9 +194,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java b/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java index b1156dabee3e..060a9195e6a3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java @@ -104,8 +104,8 @@ public void teardown() { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - String element = c.element(); + public void processElement(@Element String element, OutputReceiver receiver) + throws Exception { try { Preconditions.checkNotNull(rateLimiter).allow(1); } catch (Exception e) { @@ -115,7 +115,7 @@ public void processElement(ProcessContext c) throws Exception { // Simulate external API call or simply log the read entry Thread.sleep(100); LOG.info("Received and rate-limited record: {}", element); - c.output(element); + receiver.output(element); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java b/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java index 3ec8fcec0bd8..57b7a0848499 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RateLimiterSimple.java @@ -31,6 +31,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.checkerframework.checker.nullness.qual.Nullable; @@ -98,8 +100,8 @@ public void teardown() { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - String element = c.element(); + public void processElement(@Element String element, OutputReceiver receiver) + throws Exception { try { Preconditions.checkNotNull(rateLimiter).allow(1); } catch (Exception e) { @@ -109,7 +111,7 @@ public void processElement(ProcessContext c) throws Exception { // Simulate external API call LOG.info("Processing: {}", element); Thread.sleep(100); - c.output("Processed: " + element); + receiver.output("Processed: " + element); } } // [END RateLimiterSimpleJava] diff --git a/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java b/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java index a0d467718bed..c71ce8bb9b4e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Regex; import org.apache.beam.sdk.values.PCollection; @@ -33,7 +35,7 @@ // description: Demonstration of Regex transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -75,9 +77,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java b/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java index ed1d90606d3b..d5db80403d9c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sample; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Sample transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -76,9 +78,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/SumExample.java b/examples/java/src/main/java/org/apache/beam/examples/SumExample.java index 00fcc8697926..58c7cc9d2d2c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SumExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SumExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.values.PCollection; @@ -33,7 +35,7 @@ // description: Demonstration of Sum transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC @@ -63,9 +65,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java index 45d4a9ffd852..8cc4baa2b4a3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Sum.integersPerKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC @@ -67,9 +69,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java b/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java index 23e3db6cfd96..b2020ee65abb 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.ToString; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of ToString transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -74,9 +76,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/TopExample.java b/examples/java/src/main/java/org/apache/beam/examples/TopExample.java index 520af0f66550..fa76f1308e3c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/TopExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/TopExample.java @@ -23,6 +23,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Top; import org.apache.beam.sdk.values.PCollection; @@ -34,7 +36,7 @@ // description: Demonstration of Top transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -64,9 +66,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java b/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java index 3fc9e84fcb39..9502bd6824d2 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java @@ -22,6 +22,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Values; import org.apache.beam.sdk.values.KV; @@ -34,7 +36,7 @@ // description: Demonstration of Values transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC @@ -70,9 +72,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java b/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java index 01c1b1c3f31d..39e15affcfae 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java @@ -23,6 +23,9 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.View; import org.apache.beam.sdk.values.KV; @@ -36,7 +39,7 @@ // description: Demonstration of View transform usage. // multifile: false // default_example: false -// context_line: 49 +// context_line: 52 // categories: // - Core Transforms // complexity: MEDIUM @@ -83,10 +86,8 @@ public static void main(String[] args) { @ProcessElement public void processElement( @Element KV person, - OutputReceiver> out, - ProcessContext context) { - Map citiesToCountries = - context.sideInput(citiesToCountriesView); + @SideInput("citiesToCountries") Map citiesToCountries, + OutputReceiver> out) { String city = person.getValue(); String country = citiesToCountries.get(city); if (country == null) { @@ -95,7 +96,7 @@ public void processElement( out.output(KV.of(person.getKey(), country)); } }) - .withSideInputs(citiesToCountriesView)); + .withSideInput("citiesToCountries", citiesToCountriesView)); // [END main_section] output.apply("Log", ParDo.of(new LogOutput<>("Output: "))); @@ -112,9 +113,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java b/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java index 244a42bc9613..a669a0eca771 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java @@ -25,6 +25,8 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.FixedWindows; import org.apache.beam.sdk.transforms.windowing.Window; @@ -39,7 +41,7 @@ // description: Demonstration of Window transform usage. // multifile: false // default_example: false -// context_line: 54 +// context_line: 56 // categories: // - Core Transforms // - Windowing @@ -87,9 +89,9 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java b/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java index 11114043e830..24a3c1b16dcf 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/AutoComplete.java @@ -52,6 +52,8 @@ import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Filter; import org.apache.beam.sdk.transforms.Flatten; import org.apache.beam.sdk.transforms.PTransform; @@ -131,10 +133,11 @@ public PCollection>> expand(PCollection, CompletionCandidate>() { @ProcessElement - public void processElement(ProcessContext c) { - c.output( - new CompletionCandidate( - c.element().getKey(), c.element().getValue())); + public void processElement( + @Element KV element, + OutputReceiver receiver) { + receiver.output( + new CompletionCandidate(element.getKey(), element.getValue())); } })); @@ -210,9 +213,11 @@ public int partitionFor(KV> elem, int numParti private static class FlattenTops extends DoFn>, CompletionCandidate> { @ProcessElement - public void processElement(ProcessContext c) { - for (CompletionCandidate cc : c.element().getValue()) { - c.output(cc); + public void processElement( + @Element KV> element, + OutputReceiver receiver) { + for (CompletionCandidate cc : element.getValue()) { + receiver.output(cc); } } } @@ -267,10 +272,12 @@ public AllPrefixes(int minPrefix, int maxPrefix) { } @ProcessElement - public void processElement(ProcessContext c) { - String word = c.element().value; + public void processElement( + @Element CompletionCandidate element, + OutputReceiver> receiver) { + String word = element.value; for (int i = minPrefix; i <= Math.min(word.length(), maxPrefix); i++) { - c.output(KV.of(word.substring(0, i), c.element())); + receiver.output(KV.of(word.substring(0, i), element)); } } } @@ -332,23 +339,24 @@ public String toString() { /** Takes as input a set of strings, and emits each #hashtag found therein. */ static class ExtractHashtags extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - Matcher m = Pattern.compile("#\\S+").matcher(c.element()); + public void processElement(@Element String element, OutputReceiver receiver) { + Matcher m = Pattern.compile("#\\S+").matcher(element); while (m.find()) { - c.output(m.group().substring(1)); + receiver.output(m.group().substring(1)); } } } static class FormatForBigquery extends DoFn>, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV> element, OutputReceiver receiver) { List completions = new ArrayList<>(); - for (CompletionCandidate cc : c.element().getValue()) { + for (CompletionCandidate cc : element.getValue()) { completions.add(new TableRow().set("count", cc.getCount()).set("tag", cc.getValue())); } - TableRow row = new TableRow().set("prefix", c.element().getKey()).set("tags", completions); - c.output(row); + TableRow row = new TableRow().set("prefix", element.getKey()).set("tags", completions); + receiver.output(row); } /** Defines the BigQuery schema used for the output. */ @@ -386,15 +394,16 @@ public FormatForDatastore(String kind, String ancestorKey) { } @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV> element, OutputReceiver receiver) { Entity.Builder entityBuilder = Entity.newBuilder(); com.google.datastore.v1.Key key = - makeKey(makeKey(kind, ancestorKey).build(), kind, c.element().getKey()).build(); + makeKey(makeKey(kind, ancestorKey).build(), kind, element.getKey()).build(); entityBuilder.setKey(key); List candidates = new ArrayList<>(); Map properties = new HashMap<>(); - for (CompletionCandidate tag : c.element().getValue()) { + for (CompletionCandidate tag : element.getValue()) { Entity.Builder tagEntity = Entity.newBuilder(); properties.put("tag", makeValue(tag.value).build()); properties.put("count", makeValue(tag.count).build()); @@ -402,7 +411,7 @@ public void processElement(ProcessContext c) { } properties.put("candidates", makeValue(candidates).build()); entityBuilder.putAllProperties(properties); - c.output(entityBuilder.build()); + receiver.output(entityBuilder.build()); } } @@ -527,11 +536,12 @@ public static void runAutocompletePipeline(Options options) throws IOException { ParDo.of( new DoFn>, Long>() { @ProcessElement - public void process(ProcessContext c) { - KV> elm = c.element(); + public void process( + @Element KV> elm, + OutputReceiver receiver) { Long listHash = - c.element().getValue().stream().mapToLong(cc -> cc.hashCode()).sum(); - c.output(Long.valueOf(elm.getKey().hashCode()) + listHash); + elm.getValue().stream().mapToLong(cc -> cc.hashCode()).sum(); + receiver.output(Long.valueOf(elm.getKey().hashCode()) + listHash); } })) .apply(Sum.longsGlobally()); diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java b/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java index e4ce5e3eb17e..ea5f2e5399e7 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/StreamingWordExtract.java @@ -34,6 +34,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; /** @@ -55,12 +57,12 @@ public class StreamingWordExtract { /** A {@link DoFn} that tokenizes lines of text into individual words. */ static class ExtractWords extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - String[] words = c.element().split(ExampleUtils.TOKENIZER_PATTERN, -1); + public void processElement(@Element String element, OutputReceiver receiver) { + String[] words = element.split(ExampleUtils.TOKENIZER_PATTERN, -1); for (String word : words) { if (!word.isEmpty()) { - c.output(word); + receiver.output(word); } } } @@ -69,8 +71,8 @@ public void processElement(ProcessContext c) { /** A {@link DoFn} that uppercases a word. */ static class Uppercase extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - c.output(c.element().toUpperCase()); + public void processElement(@Element String element, OutputReceiver receiver) { + receiver.output(element.toUpperCase()); } } @@ -78,8 +80,8 @@ public void processElement(ProcessContext c) { static class StringToRowConverter extends DoFn { /** In this example, put the whole string into single BigQuery field. */ @ProcessElement - public void processElement(ProcessContext c) { - c.output(new TableRow().set("string_field", c.element())); + public void processElement(@Element String element, OutputReceiver receiver) { + receiver.output(new TableRow().set("string_field", element)); } static TableSchema getSchema() { diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java index b3e5fd04fa7e..2b5c8b42ee1e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java @@ -23,7 +23,7 @@ // GCS prefix. // multifile: true // pipeline_options: --output output.txt -// context_line: 447 +// context_line: 462 // categories: // - Combiners // - Options @@ -56,6 +56,9 @@ import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Distinct; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.Flatten; import org.apache.beam.sdk.transforms.Keys; import org.apache.beam.sdk.transforms.PTransform; @@ -236,9 +239,11 @@ public PCollection>> expand( ParDo.of( new DoFn, KV>() { @ProcessElement - public void processElement(ProcessContext c) { - URI uri = c.element().getKey(); - String line = c.element().getValue(); + public void processElement( + @Element KV element, + OutputReceiver> receiver) { + URI uri = element.getKey(); + String line = element.getValue(); for (String word : line.split("\\W+", -1)) { // Log INFO messages when the word “love” is found. if ("love".equalsIgnoreCase(word)) { @@ -246,7 +251,7 @@ public void processElement(ProcessContext c) { } if (!word.isEmpty()) { - c.output(KV.of(uri, word.toLowerCase())); + receiver.output(KV.of(uri, word.toLowerCase())); } } } @@ -281,11 +286,13 @@ public void processElement(ProcessContext c) { ParDo.of( new DoFn, Long>, KV>>() { @ProcessElement - public void processElement(ProcessContext c) { - URI uri = c.element().getKey().getKey(); - String word = c.element().getKey().getValue(); - Long occurrences = c.element().getValue(); - c.output(KV.of(uri, KV.of(word, occurrences))); + public void processElement( + @Element KV, Long> element, + OutputReceiver>> receiver) { + URI uri = element.getKey().getKey(); + String word = element.getKey().getValue(); + Long occurrences = element.getValue(); + receiver.output(KV.of(uri, KV.of(word, occurrences))); } })); @@ -322,16 +329,18 @@ public void processElement(ProcessContext c) { ParDo.of( new DoFn, KV>>() { @ProcessElement - public void processElement(ProcessContext c) { - URI uri = c.element().getKey(); - Long wordTotal = c.element().getValue().getOnly(wordTotalsTag); + public void processElement( + @Element KV element, + OutputReceiver>> receiver) { + URI uri = element.getKey(); + Long wordTotal = element.getValue().getOnly(wordTotalsTag); for (KV wordAndCount : - c.element().getValue().getAll(wordCountsTag)) { + element.getValue().getAll(wordCountsTag)) { String word = wordAndCount.getKey(); Long wordCount = wordAndCount.getValue(); Double termFrequency = wordCount.doubleValue() / wordTotal.doubleValue(); - c.output(KV.of(word, KV.of(uri, termFrequency))); + receiver.output(KV.of(word, KV.of(uri, termFrequency))); } } })); @@ -348,17 +357,19 @@ public void processElement(ProcessContext c) { ParDo.of( new DoFn, KV>() { @ProcessElement - public void processElement(ProcessContext c) { - String word = c.element().getKey(); - Long documentCount = c.element().getValue(); - Long documentTotal = c.sideInput(totalDocuments); + public void processElement( + @SideInput("totalDocuments") Long documentTotal, + @Element KV element, + OutputReceiver> receiver) { + String word = element.getKey(); + Long documentCount = element.getValue(); Double documentFrequency = documentCount.doubleValue() / documentTotal.doubleValue(); - c.output(KV.of(word, documentFrequency)); + receiver.output(KV.of(word, documentFrequency)); } }) - .withSideInputs(totalDocuments)); + .withSideInput("totalDocuments", totalDocuments)); // Join the term frequency and document frequency // collections, each keyed on the word. @@ -380,15 +391,17 @@ public void processElement(ProcessContext c) { ParDo.of( new DoFn, KV>>() { @ProcessElement - public void processElement(ProcessContext c) { - String word = c.element().getKey(); - Double df = c.element().getValue().getOnly(dfTag); + public void processElement( + @Element KV element, + OutputReceiver>> receiver) { + String word = element.getKey(); + Double df = element.getValue().getOnly(dfTag); - for (KV uriAndTf : c.element().getValue().getAll(tfTag)) { + for (KV uriAndTf : element.getValue().getAll(tfTag)) { URI uri = uriAndTf.getKey(); Double tf = uriAndTf.getValue(); Double tfIdf = tf * Math.log(1 / df); - c.output(KV.of(word, KV.of(uri, tfIdf))); + receiver.output(KV.of(word, KV.of(uri, tfIdf))); } } })); @@ -419,13 +432,15 @@ public PDone expand(PCollection>> wordToUriAndTfIdf) ParDo.of( new DoFn>, String>() { @ProcessElement - public void processElement(ProcessContext c) { - c.output( + public void processElement( + @Element KV> element, + OutputReceiver receiver) { + receiver.output( String.format( "%s,\t%s,\t%f", - c.element().getKey(), - c.element().getValue().getKey(), - c.element().getValue().getValue())); + element.getKey(), + element.getValue().getKey(), + element.getValue().getValue())); } })) .apply(TextIO.write().to(output).withSuffix(".csv")); diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java index b06cd8da9d43..e79f09c9d084 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java @@ -24,7 +24,7 @@ // each month. // multifile: true // pipeline_options: --output output.txt -// context_line: 236 +// context_line: 244 // categories: // - Combiners // - Options @@ -48,6 +48,8 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -93,8 +95,7 @@ public class TopWikipediaSessions { /** Extracts user and timestamp from a TableRow representing a Wikipedia edit. */ static class ExtractUserAndTimestamp extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { int timestamp; // TODO(BEAM-5390): Avoid this workaround. try { @@ -105,7 +106,7 @@ public void processElement(ProcessContext c) { String userName = (String) row.get("contributor_username"); if (userName != null) { // Sets the implicit timestamp field to be used in windowing. - c.outputWithTimestamp(userName, new Instant(timestamp * 1000L)); + receiver.outputWithTimestamp(userName, new Instant(timestamp * 1000L)); } } } @@ -143,18 +144,24 @@ public PCollection>> expand(PCollection> static class SessionsToStringsDoFn extends DoFn, KV> { @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) { - c.output(KV.of(c.element().getKey() + " : " + window, c.element().getValue())); + public void processElement( + BoundedWindow window, + @Element KV element, + OutputReceiver> receiver) { + receiver.output(KV.of(element.getKey() + " : " + window, element.getValue())); } } static class FormatOutputDoFn extends DoFn>, String> { @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) { - for (KV item : c.element()) { + public void processElement( + BoundedWindow window, + @Element List> element, + OutputReceiver receiver) { + for (KV item : element) { String session = item.getKey(); long count = item.getValue(); - c.output(session + " : " + count + " : " + ((IntervalWindow) window).start()); + receiver.output(session + " : " + count + " : " + ((IntervalWindow) window).start()); } } } @@ -187,10 +194,11 @@ public PCollection expand(PCollection input) { ParDo.of( new DoFn() { @ProcessElement - public void processElement(ProcessContext c) { - if (Math.abs((long) c.element().hashCode()) + public void processElement( + @Element String element, OutputReceiver receiver) { + if (Math.abs((long) element.hashCode()) <= Integer.MAX_VALUE * samplingThreshold) { - c.output(c.element()); + receiver.output(element); } } })) diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java index 6f75e2e03d99..ab80a220f571 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java @@ -23,7 +23,7 @@ // window, it finds the lane that had the highest flow recorded, for each sensor station. // It writes those max values along with auxiliary info to a BigQuery table. // multifile: true -// context_line: 402 +// context_line: 410 // categories: // - Combiners // - Streaming @@ -58,6 +58,9 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SerializableFunction; @@ -182,13 +185,14 @@ static class ExtractTimestamps extends DoFn { DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss"); @ProcessElement - public void processElement(DoFn.ProcessContext c) throws Exception { - String[] items = c.element().split(",", -1); + public void processElement(@Element String element, OutputReceiver receiver) + throws Exception { + String[] items = element.split(",", -1); if (items.length > 0) { try { String timestamp = items[0]; - c.outputWithTimestamp(c.element(), new Instant(dateTimeFormat.parseMillis(timestamp))); + receiver.outputWithTimestamp(element, new Instant(dateTimeFormat.parseMillis(timestamp))); } catch (IllegalArgumentException e) { // Skip the invalid input. } @@ -206,8 +210,9 @@ public void processElement(DoFn.ProcessContext c) throws Excepti static class ExtractFlowInfoFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext c) { - String[] items = c.element().split(",", -1); + public void processElement( + @Element String element, OutputReceiver> receiver) { + String[] items = element.split(",", -1); if (items.length < 48) { // Skip the invalid input. return; @@ -236,7 +241,7 @@ public void processElement(ProcessContext c) { laneAvgOccupancy, laneAvgSpeed, totalFlow); - c.output(KV.of(stationId, laneInfo)); + receiver.output(KV.of(stationId, laneInfo)); } } } @@ -270,12 +275,15 @@ public LaneInfo apply(Iterable input) { */ static class FormatMaxesFn extends DoFn, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV element, + @Timestamp Instant timestamp, + OutputReceiver receiver) { - LaneInfo laneInfo = c.element().getValue(); + LaneInfo laneInfo = element.getValue(); TableRow row = new TableRow() - .set("station_id", c.element().getKey()) + .set("station_id", element.getKey()) .set("direction", laneInfo.getDirection()) .set("freeway", laneInfo.getFreeway()) .set("lane_max_flow", laneInfo.getLaneFlow()) @@ -284,8 +292,8 @@ public void processElement(ProcessContext c) { .set("avg_speed", laneInfo.getLaneAS()) .set("total_flow", laneInfo.getTotalFlow()) .set("recorded_timestamp", laneInfo.getRecordedTimestamp()) - .set("window_timestamp", c.timestamp().toString()); - c.output(row); + .set("window_timestamp", timestamp.toString()); + receiver.output(row); } /** Defines the BigQuery schema used for the output. */ diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java index 958415626863..3918f2d72a64 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java @@ -24,7 +24,7 @@ // of predefined 'routes', and looks for 'slowdowns' in those routes. It writes its // results to a BigQuery table. // multifile: true -// context_line: 399 +// context_line: 411 // categories: // - Combiners // - Streaming @@ -63,6 +63,9 @@ import org.apache.beam.sdk.options.Description; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.GroupByKey; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -187,12 +190,13 @@ static class ExtractTimestamps extends DoFn { DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss"); @ProcessElement - public void processElement(DoFn.ProcessContext c) throws Exception { - String[] items = c.element().split(","); + public void processElement(@Element String element, OutputReceiver receiver) + throws Exception { + String[] items = element.split(","); String timestamp = tryParseTimestamp(items); if (timestamp != null) { try { - c.outputWithTimestamp(c.element(), new Instant(dateTimeFormat.parseMillis(timestamp))); + receiver.outputWithTimestamp(element, new Instant(dateTimeFormat.parseMillis(timestamp))); } catch (IllegalArgumentException e) { // Skip the invalid input. } @@ -207,8 +211,11 @@ public void processElement(DoFn.ProcessContext c) throws Excepti static class ExtractStationSpeedFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext c) { - String[] items = c.element().split(","); + public void processElement( + @Timestamp Instant timestamp, + @Element String element, + OutputReceiver> receiver) { + String[] items = element.split(","); String stationType = tryParseStationType(items); // For this analysis, use only 'main line' station types if ("ML".equals(stationType)) { @@ -216,11 +223,10 @@ public void processElement(ProcessContext c) { String stationId = tryParseStationId(items); // For this simple example, filter out everything but some hardwired routes. if (avgSpeed != null && stationId != null && sdStations.containsKey(stationId)) { - StationSpeed stationSpeed = - new StationSpeed(stationId, avgSpeed, c.timestamp().getMillis()); + StationSpeed stationSpeed = new StationSpeed(stationId, avgSpeed, timestamp.getMillis()); // The tuple key is the 'route' name stored in the 'sdStations' hash. KV outputValue = KV.of(sdStations.get(stationId), stationSpeed); - c.output(outputValue); + receiver.output(outputValue); } } } @@ -234,13 +240,16 @@ public void processElement(ProcessContext c) { */ static class GatherStats extends DoFn>, KV> { @ProcessElement - public void processElement(ProcessContext c) throws IOException { - String route = c.element().getKey(); + public void processElement( + @Element KV> element, + OutputReceiver> receiver) + throws IOException { + String route = element.getKey(); double speedSum = 0.0; int speedCount = 0; int speedups = 0; int slowdowns = 0; - List infoList = Lists.newArrayList(c.element().getValue()); + List infoList = Lists.newArrayList(element.getValue()); // StationSpeeds sort by embedded timestamp. Collections.sort(infoList); Map prevSpeeds = new HashMap<>(); @@ -268,22 +277,25 @@ public void processElement(ProcessContext c) throws IOException { double speedAvg = speedSum / speedCount; boolean slowdownEvent = slowdowns >= 2 * speedups; RouteInfo routeInfo = new RouteInfo(route, speedAvg, slowdownEvent); - c.output(KV.of(route, routeInfo)); + receiver.output(KV.of(route, routeInfo)); } } /** Format the results of the slowdown calculations to a TableRow, to save to BigQuery. */ static class FormatStatsFn extends DoFn, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { - RouteInfo routeInfo = c.element().getValue(); + public void processElement( + @Element KV element, + @Timestamp Instant timestamp, + OutputReceiver receiver) { + RouteInfo routeInfo = element.getValue(); TableRow row = new TableRow() .set("avg_speed", routeInfo.getAvgSpeed()) .set("slowdown_event", routeInfo.getSlowdownEvent()) - .set("route", c.element().getKey()) - .set("window_timestamp", c.timestamp().toString()); - c.output(row); + .set("route", element.getKey()) + .set("window_timestamp", timestamp.toString()); + receiver.output(row); } /** Defines the BigQuery schema used for the output. */ diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java index cf097c8ea979..6e5321dc54ab 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/DataProtectors.java @@ -37,6 +37,9 @@ import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.MultiOutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.GroupIntoBatches; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -199,20 +202,24 @@ public void close() { @ProcessElement @SuppressWarnings("argument") - public void process(@Element KV> element, ProcessContext context) { + public void process( + @Element KV> element, + OutputReceiver mainReceiver, + MultiOutputReceiver multiReceiver) { Iterable rows = element.getValue(); try { for (Row outputRow : getTokenizedRow(rows)) { - context.output(outputRow); + mainReceiver.output(outputRow); } } catch (Exception e) { for (Row outputRow : rows) { - context.output( - failureTag, - FailsafeElement.of(outputRow, outputRow) - .setErrorMessage(e.getMessage()) - .setStacktrace(Throwables.getStackTraceAsString(e))); + multiReceiver + .get(failureTag) + .output( + FailsafeElement.of(outputRow, outputRow) + .setErrorMessage(e.getMessage()) + .setStacktrace(Throwables.getStackTraceAsString(e))); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java index fe8f4c1afad8..667e26fa9c2d 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigQueryIO.java @@ -26,6 +26,8 @@ import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy; import org.apache.beam.sdk.io.gcp.bigquery.WriteResult; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; @@ -90,9 +92,8 @@ public static FailsafeElement wrapBigQueryInsertError( public static class RowToTableRowFn extends DoFn { @ProcessElement - public void processElement(ProcessContext context) { - Row row = context.element(); - context.output(BigQueryUtils.toTableRow(row)); + public void processElement(@Element Row row, OutputReceiver receiver) { + receiver.output(BigQueryUtils.toTableRow(row)); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java index d7d1c3e97232..435620a4b011 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/transforms/io/TokenizationBigTableIO.java @@ -75,8 +75,10 @@ static class TransformToBigTableFormat extends DoFn>> out, ProcessContext c) { - DataTokenizationOptions options = c.getPipelineOptions().as(DataTokenizationOptions.class); + @Element Row in, + OutputReceiver>> out, + PipelineOptions pipelineOptions) { + DataTokenizationOptions options = pipelineOptions.as(DataTokenizationOptions.class); // Mapping every field in provided Row to Mutation.SetCell, which will create/update // cell content with provided data Set mutations = diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java index d827e4b30cb3..9fead4e1a7fd 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/CsvConverters.java @@ -264,8 +264,6 @@ public static Builder newBuilder() { @Override public PCollectionTuple expand(PCollectionTuple lines) { - PCollectionView headersView = null; - // Convert csv lines into Failsafe elements so that we can recover over multiple transforms. PCollection> lineFailsafeElements = lines @@ -285,16 +283,14 @@ public PCollectionTuple expand(PCollectionTuple lines) { return lineFailsafeElements.apply( "LineToDocumentUsingSchema", - ParDo.of( - new FailsafeElementToJsonFn( - headersView, schema, delimiter(), udfDeadletterTag())) + ParDo.of(new FailsafeElementToJsonFn(schema, delimiter(), udfDeadletterTag())) .withOutputTags(udfOutputTag(), TupleTagList.of(udfDeadletterTag()))); } // Run if using headers - headersView = lines.get(headerTag()).apply(Sample.any(1)).apply(View.asSingleton()); + PCollectionView headersView = + lines.get(headerTag()).apply(Sample.any(1)).apply(View.asSingleton()); - PCollectionView finalHeadersView = headersView; lines .get(headerTag()) .apply( @@ -302,23 +298,24 @@ headersView, schema, delimiter(), udfDeadletterTag())) ParDo.of( new DoFn() { @ProcessElement - public void processElement(ProcessContext c) { - String headers = c.sideInput(finalHeadersView); - if (!c.element().equals(headers)) { + public void processElement( + @SideInput("finalHeadersView") String headers, + @Element String element) { + if (!element.equals(headers)) { LOG.error("Headers do not match, consistency cannot be guaranteed"); throw new RuntimeException( "Headers do not match, consistency cannot be guaranteed"); } } }) - .withSideInputs(finalHeadersView)); + .withSideInput("finalHeadersView", headersView)); return lineFailsafeElements.apply( "LineToDocumentWithHeaders", ParDo.of( - new FailsafeElementToJsonFn( - headersView, jsonSchemaPath(), delimiter(), udfDeadletterTag())) - .withSideInputs(headersView) + new FailsafeElementToJsonWithHeadersFn( + jsonSchemaPath(), delimiter(), udfDeadletterTag())) + .withSideInput("finalHeadersView", headersView) .withOutputTags(udfOutputTag(), TupleTagList.of(udfDeadletterTag()))); } @@ -357,45 +354,90 @@ public static class FailsafeElementToJsonFn @Nullable public final String jsonSchema; public final String delimiter; public final TupleTag> udfDeadletterTag; - @Nullable private final PCollectionView headersView; private Counter successCounter = Metrics.counter(FailsafeElementToJsonFn.class, SUCCESSFUL_TO_JSON_COUNTER); private Counter failedCounter = Metrics.counter(FailsafeElementToJsonFn.class, FAILED_TO_JSON_COUNTER); FailsafeElementToJsonFn( - PCollectionView headersView, String jsonSchema, String delimiter, TupleTag> udfDeadletterTag) { - this.headersView = headersView; this.jsonSchema = jsonSchema; this.delimiter = delimiter; this.udfDeadletterTag = udfDeadletterTag; } @ProcessElement - public void processElement(ProcessContext context) { - FailsafeElement element = context.element(); + public void processElement( + @Element FailsafeElement element, + OutputReceiver> receiver, + MultiOutputReceiver multiReceiver) { + List header = null; + List record = Arrays.asList(element.getOriginalPayload().split(this.delimiter)); + + try { + String json = buildJsonString(header, record, this.jsonSchema); + receiver.output(FailsafeElement.of(element.getOriginalPayload(), json)); + successCounter.inc(); + } catch (Exception e) { + failedCounter.inc(); + multiReceiver + .get(this.udfDeadletterTag) + .output( + FailsafeElement.of(element) + .setErrorMessage(e.getMessage()) + .setStacktrace(Throwables.getStackTraceAsString(e))); + } + } + } + + public static class FailsafeElementToJsonWithHeadersFn + extends DoFn, FailsafeElement> { + + @Nullable public final String jsonSchema; + public final String delimiter; + public final TupleTag> udfDeadletterTag; + private Counter successCounter = + Metrics.counter(FailsafeElementToJsonWithHeadersFn.class, SUCCESSFUL_TO_JSON_COUNTER); + private Counter failedCounter = + Metrics.counter(FailsafeElementToJsonWithHeadersFn.class, FAILED_TO_JSON_COUNTER); + + FailsafeElementToJsonWithHeadersFn( + String jsonSchema, + String delimiter, + TupleTag> udfDeadletterTag) { + this.jsonSchema = jsonSchema; + this.delimiter = delimiter; + this.udfDeadletterTag = udfDeadletterTag; + } + + @ProcessElement + public void processElement( + @Element FailsafeElement element, + OutputReceiver> receiver, + MultiOutputReceiver multiReceiver, + @SideInput("finalHeadersView") String headersStr) { List header = null; - if (this.headersView != null) { - header = Arrays.asList(context.sideInput(this.headersView).split(this.delimiter)); + if (headersStr != null) { + header = Arrays.asList(headersStr.split(this.delimiter)); } List record = Arrays.asList(element.getOriginalPayload().split(this.delimiter)); try { String json = buildJsonString(header, record, this.jsonSchema); - context.output(FailsafeElement.of(element.getOriginalPayload(), json)); + receiver.output(FailsafeElement.of(element.getOriginalPayload(), json)); successCounter.inc(); } catch (Exception e) { failedCounter.inc(); - context.output( - this.udfDeadletterTag, - FailsafeElement.of(element) - .setErrorMessage(e.getMessage()) - .setStacktrace(Throwables.getStackTraceAsString(e))); + multiReceiver + .get(this.udfDeadletterTag) + .output( + FailsafeElement.of(element) + .setErrorMessage(e.getMessage()) + .setStacktrace(Throwables.getStackTraceAsString(e))); } } } @@ -407,9 +449,9 @@ public void processElement(ProcessContext context) { static class LineToFailsafeElementFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext context) { - String message = context.element(); - context.output(FailsafeElement.of(message, message)); + public void processElement( + @Element String message, OutputReceiver> receiver) { + receiver.output(FailsafeElement.of(message, message)); } } @@ -510,13 +552,12 @@ static class GetCsvHeadersFn extends DoFn { } @ProcessElement - public void processElement(ProcessContext context, MultiOutputReceiver outputReceiver) { - ReadableFile f = context.element(); + public void processElement(@Element ReadableFile file, MultiOutputReceiver outputReceiver) { String headers; List records = null; String delimiter = String.valueOf(this.csvFormat.getDelimiter()); try { - String csvFileString = f.readFullyAsUTF8String(); + String csvFileString = file.readFullyAsUTF8String(); StringReader reader = new StringReader(csvFileString); CSVParser parser = CSVParser.parse(reader, this.csvFormat.withFirstRecordAsHeader()); records = diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java index 01377add0858..99981cb17b84 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/datatokenization/utils/ErrorConverters.java @@ -29,6 +29,9 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.bigquery.WriteResult; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -43,6 +46,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.DateTimeZone; import org.joda.time.Duration; +import org.joda.time.Instant; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -123,16 +127,17 @@ public FailedStringToCsvRowFn() { } @ProcessElement - public void processElement(ProcessContext context) { - FailsafeElement failsafeElement = context.element(); + public void processElement( + @Element FailsafeElement failsafeElement, + @Timestamp Instant timestamp, + OutputReceiver receiver) { ArrayList outputRow = new ArrayList<>(); final String message = failsafeElement.getOriginalPayload(); // Format the timestamp for insertion - String timestamp = - TIMESTAMP_FORMATTER.print(context.timestamp().toDateTime(DateTimeZone.UTC)); + String timestampStr = TIMESTAMP_FORMATTER.print(timestamp.toDateTime(DateTimeZone.UTC)); - outputRow.add(timestamp); + outputRow.add(timestampStr); outputRow.add(MoreObjects.firstNonNull(failsafeElement.getErrorMessage(), "")); // Only set the payload if it's populated on the message. @@ -140,7 +145,7 @@ public void processElement(ProcessContext context) { outputRow.add(message); } - context.output(String.join(csvDelimiter, outputRow)); + receiver.output(String.join(csvDelimiter, outputRow)); } } @@ -198,19 +203,20 @@ public static class FailedStringToTableRowFn DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"); @ProcessElement - public void processElement(ProcessContext context) { - FailsafeElement failsafeElement = context.element(); + public void processElement( + @Timestamp Instant timestamp, + @Element FailsafeElement failsafeElement, + OutputReceiver receiver) { final String message = failsafeElement.getOriginalPayload(); // Format the timestamp for insertion - String timestamp = - TIMESTAMP_FORMATTER.print(context.timestamp().toDateTime(DateTimeZone.UTC)); + String timestampStr = TIMESTAMP_FORMATTER.print(timestamp.toDateTime(DateTimeZone.UTC)); // Build the table row @SuppressWarnings("nullness") // TableRow.set not annotated but does accept nulls final TableRow failedRow = new TableRow() - .set("timestamp", timestamp) + .set("timestamp", timestampStr) .set("errorMessage", failsafeElement.getErrorMessage()) .set("stacktrace", failsafeElement.getStacktrace()); @@ -221,7 +227,7 @@ public void processElement(ProcessContext context) { .set("payloadBytes", message.getBytes(StandardCharsets.UTF_8)); } - context.output(failedRow); + receiver.output(failedRow); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java index a3ed04bb1c48..0018201f3686 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/GameStats.java @@ -34,6 +34,9 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.PTransform; @@ -126,21 +129,23 @@ public PCollection> expand(PCollection> Metrics.counter("main", "SpammerUsers"); @ProcessElement - public void processElement(ProcessContext c) { - Integer score = c.element().getValue(); - Double gmc = c.sideInput(globalMeanScore); + public void processElement( + @SideInput("globalMeanScore") Double gmc, + @Element KV element, + OutputReceiver> receiver) { + Integer score = element.getValue(); if (score > (gmc * SCORE_WEIGHT)) { LOG.info( "user {} spammer score {} with mean {}", - c.element().getKey(), + element.getKey(), score, gmc); numSpammerUsers.inc(); - c.output(c.element()); + receiver.output(element); } } }) - .withSideInputs(globalMeanScore)); + .withSideInput("globalMeanScore", globalMeanScore)); return filtered; } } @@ -149,10 +154,10 @@ public void processElement(ProcessContext c) { /** Calculate and output an element's session duration. */ private static class UserSessionInfoFn extends DoFn, Integer> { @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) { + public void processElement(BoundedWindow window, OutputReceiver receiver) { IntervalWindow w = (IntervalWindow) window; int duration = new Duration(w.start(), w.end()).toPeriod().toStandardMinutes().getMinutes(); - c.output(duration); + receiver.output(duration); } } @@ -192,22 +197,21 @@ public interface Options extends LeaderBoard.Options { configureWindowedWrite() { Map>> tableConfigure = new HashMap<>(); tableConfigure.put( - "team", new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.element().getKey())); + "team", new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); tableConfigure.put( - "total_score", - new WriteToBigQuery.FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); + "total_score", new WriteToBigQuery.FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); tableConfigure.put( "window_start", new WriteToBigQuery.FieldInfo<>( "STRING", - (c, w) -> { + (e, w, t, p) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); })); tableConfigure.put( "processing_time", new WriteToBigQuery.FieldInfo<>( - "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); return tableConfigure; } @@ -222,12 +226,12 @@ protected static Map> configureSession "window_start", new WriteToBigQuery.FieldInfo<>( "STRING", - (c, w) -> { + (e, w, t, p) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); })); tableConfigure.put( - "mean_duration", new WriteToBigQuery.FieldInfo<>("FLOAT", (c, w) -> c.element())); + "mean_duration", new WriteToBigQuery.FieldInfo<>("FLOAT", (e, w, t, p) -> e)); return tableConfigure; } @@ -288,14 +292,17 @@ public static void main(String[] args) throws Exception { ParDo.of( new DoFn() { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @SideInput("spammersView") Map spammers, + @Element GameActionInfo element, + OutputReceiver receiver) { // If the user is not in the spammers Map, output the data element. - if (c.sideInput(spammersView).get(c.element().getUser().trim()) == null) { - c.output(c.element()); + if (spammers.get(element.getUser().trim()) == null) { + receiver.output(element); } } }) - .withSideInputs(spammersView)) + .withSideInput("spammersView", spammersView)) // Extract and sum teamname/score pairs from the event data. .apply("ExtractTeamScore", new ExtractAndSumScore("team")) // [END DocInclude_FilterAndCalc] diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java index c57d9ba6b8c8..a94f699282a8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/HourlyTeamScore.java @@ -112,11 +112,11 @@ public interface Options extends UserScore.Options { */ protected static Map>> configureOutput() { Map>> config = new HashMap<>(); - config.put("team", (c, w) -> c.element().getKey()); - config.put("total_score", (c, w) -> c.element().getValue()); + config.put("team", (e, w, t, p) -> e.getKey()); + config.put("total_score", (e, w, t, p) -> e.getValue()); config.put( "window_start", - (c, w) -> { + (e, w, t, p) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); }); diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java index 832c0ad79e76..3750b33aa791 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java @@ -135,25 +135,24 @@ public interface Options extends ExampleOptions, StreamingOptions { Map>> tableConfigure = new HashMap<>(); tableConfigure.put( - "team", new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.element().getKey())); + "team", new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); tableConfigure.put( - "total_score", - new WriteToBigQuery.FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); + "total_score", new WriteToBigQuery.FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); tableConfigure.put( "window_start", new WriteToBigQuery.FieldInfo<>( "STRING", - (c, w) -> { + (e, w, t, p) -> { IntervalWindow window = (IntervalWindow) w; return GameConstants.DATE_TIME_FORMATTER.print(window.start()); })); tableConfigure.put( "processing_time", new WriteToBigQuery.FieldInfo<>( - "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); tableConfigure.put( "timing", - new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.pane().getTiming().toString())); + new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> p.getTiming().toString())); return tableConfigure; } @@ -165,10 +164,9 @@ public interface Options extends ExampleOptions, StreamingOptions { configureBigQueryWrite() { Map>> tableConfigure = new HashMap<>(); tableConfigure.put( - "user", new WriteToBigQuery.FieldInfo<>("STRING", (c, w) -> c.element().getKey())); + "user", new WriteToBigQuery.FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); tableConfigure.put( - "total_score", - new WriteToBigQuery.FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); + "total_score", new WriteToBigQuery.FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); return tableConfigure; } @@ -184,7 +182,7 @@ public interface Options extends ExampleOptions, StreamingOptions { tableConfigure.put( "processing_time", new WriteToBigQuery.FieldInfo<>( - "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); return tableConfigure; } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java index b28db261ab0e..3caa1e619526 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/StatefulTeamScore.java @@ -37,6 +37,8 @@ import org.apache.beam.sdk.state.StateSpecs; import org.apache.beam.sdk.state.ValueState; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -101,12 +103,12 @@ public interface Options extends LeaderBoard.Options { private static Map>> configureCompleteWindowedTableWrite() { Map>> tableConfigure = new HashMap<>(); - tableConfigure.put("team", new FieldInfo<>("STRING", (c, w) -> c.element().getKey())); - tableConfigure.put("total_score", new FieldInfo<>("INTEGER", (c, w) -> c.element().getValue())); + tableConfigure.put("team", new FieldInfo<>("STRING", (e, w, t, p) -> e.getKey())); + tableConfigure.put("total_score", new FieldInfo<>("INTEGER", (e, w, t, p) -> e.getValue())); tableConfigure.put( "processing_time", new FieldInfo<>( - "STRING", (c, w) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); + "STRING", (e, w, t, p) -> GameConstants.DATE_TIME_FORMATTER.print(Instant.now()))); return tableConfigure; } @@ -203,9 +205,11 @@ public UpdateTeamScoreFn(int thresholdScore) { */ @ProcessElement public void processElement( - ProcessContext c, @StateId(TOTAL_SCORE) ValueState totalScore) { - String teamName = c.element().getKey(); - GameActionInfo gInfo = c.element().getValue(); + @Element KV element, + @StateId(TOTAL_SCORE) ValueState totalScore, + OutputReceiver> receiver) { + String teamName = element.getKey(); + GameActionInfo gInfo = element.getValue(); // ValueState cells do not contain a default value. If the state is possibly not written, make // sure to check for null on read. @@ -218,7 +222,7 @@ public void processElement( // the new total is 2002, and the threshold is 1000, 1999 / 1000 = 1, 2002 / 1000 = 2. // Therefore, this team passed the threshold. if (oldTotalScore / this.thresholdScore < totalScore.read() / this.thresholdScore) { - c.output(KV.of(teamName, totalScore.read())); + receiver.output(KV.of(teamName, totalScore.read())); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java index b30b4665d265..054ce7a52935 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/UserScore.java @@ -34,6 +34,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -161,18 +163,18 @@ static class ParseEventFn extends DoFn { private final Counter numParseErrors = Metrics.counter("main", "ParseErrors"); @ProcessElement - public void processElement(ProcessContext c) { - String[] components = c.element().split(",", -1); + public void processElement(@Element String element, OutputReceiver receiver) { + String[] components = element.split(",", -1); try { String user = components[0].trim(); String team = components[1].trim(); Integer score = Integer.parseInt(components[2].trim()); Long timestamp = Long.parseLong(components[3].trim()); GameActionInfo gInfo = new GameActionInfo(user, team, score, timestamp); - c.output(gInfo); + receiver.output(gInfo); } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) { numParseErrors.inc(); - LOG.info("Parse error on {}", c.element(), e); + LOG.info("Parse error on {}", element, e); } } } @@ -232,8 +234,8 @@ public interface Options extends PipelineOptions { */ protected static Map>> configureOutput() { Map>> config = new HashMap<>(); - config.put("user", (c, w) -> c.element().getKey()); - config.put("total_score", (c, w) -> c.element().getValue()); + config.put("user", (e, w, t, p) -> e.getKey()); + config.put("total_score", (e, w, t, p) -> e.getValue()); return config; } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java index eef4bc932682..5486025083c9 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToBigQuery.java @@ -30,11 +30,16 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PDone; +import org.joda.time.Instant; /** * Generate, format, and write BigQuery table row information. Use provided information about the @@ -64,11 +69,11 @@ public WriteToBigQuery( } /** - * A {@link Serializable} function from a {@link DoFn.ProcessContext} and {@link BoundedWindow} to - * the value for that field. + * A {@link Serializable} function from an element and {@link BoundedWindow} to the value for that + * field. */ public interface FieldFn extends Serializable { - Object apply(DoFn.ProcessContext context, BoundedWindow window); + Object apply(InputT element, BoundedWindow window, Instant timestamp, PaneInfo pane); } /** Define a class to hold information about output table field definitions. */ @@ -96,16 +101,21 @@ FieldFn getFieldFn() { protected class BuildRowFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) { + public void processElement( + @Element InputT element, + @Timestamp Instant timestamp, + PaneInfo pane, + BoundedWindow window, + OutputReceiver receiver) { TableRow row = new TableRow(); for (Map.Entry> entry : fieldInfo.entrySet()) { String key = entry.getKey(); FieldInfo fcnInfo = entry.getValue(); FieldFn fcn = fcnInfo.getFieldFn(); - row.set(key, fcn.apply(c, window)); + row.set(key, fcn.apply(element, window, timestamp, pane)); } - c.output(row); + receiver.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java index 330769e0c79e..35471163c036 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteToText.java @@ -32,6 +32,9 @@ import org.apache.beam.sdk.io.fs.ResolveOptions.StandardResolveOptions; import org.apache.beam.sdk.io.fs.ResourceId; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; @@ -70,26 +73,32 @@ public WriteToText( } /** - * A {@link Serializable} function from a {@link DoFn.ProcessContext} and {@link BoundedWindow} to - * the value for that field. + * A {@link Serializable} function from an element and {@link BoundedWindow} to the value for that + * field. */ public interface FieldFn extends Serializable { - Object apply(DoFn.ProcessContext context, BoundedWindow window); + Object apply( + InputT element, BoundedWindow window, org.joda.time.Instant timestamp, PaneInfo pane); } /** Convert each key/score pair into a row as specified by fieldFn. */ protected class BuildRowFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) { + public void processElement( + @Element InputT element, + @Timestamp org.joda.time.Instant timestamp, + PaneInfo pane, + BoundedWindow window, + OutputReceiver receiver) { List fields = new ArrayList<>(); for (Map.Entry> entry : fieldFn.entrySet()) { String key = entry.getKey(); FieldFn fcn = entry.getValue(); - fields.add(key + ": " + fcn.apply(c, window)); + fields.add(key + ": " + fcn.apply(element, window, timestamp, pane)); } String result = fields.stream().collect(Collectors.joining(", ")); - c.output(result); + receiver.output(result); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java index 36fa18a34e0d..77a59f2c3b6e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/game/utils/WriteWindowedToBigQuery.java @@ -24,8 +24,12 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.bigquery.InsertRetryPolicy; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PDone; @@ -44,15 +48,20 @@ public WriteWindowedToBigQuery( /** Convert each key/score pair into a BigQuery TableRow. */ protected class BuildRowFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) { + public void processElement( + @Element T element, + @Timestamp org.joda.time.Instant timestamp, + PaneInfo pane, + BoundedWindow window, + OutputReceiver receiver) { TableRow row = new TableRow(); for (Map.Entry> entry : fieldInfo.entrySet()) { String key = entry.getKey(); FieldInfo fcnInfo = entry.getValue(); - row.set(key, fcnInfo.getFieldFn().apply(c, window)); + row.set(key, fcnInfo.getFieldFn().apply(element, window, timestamp, pane)); } - c.output(row); + receiver.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java index 3b2653b7601e..d662cde1f000 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryStreamingTornadoes.java @@ -32,6 +32,9 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -85,10 +88,9 @@ public class BigQueryStreamingTornadoes { */ static class ExtractTornadoesFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { if (Boolean.TRUE.equals(row.get("tornado"))) { - c.output(Integer.parseInt((String) row.get("month"))); + receiver.output(Integer.parseInt((String) row.get("month"))); } } } @@ -99,13 +101,16 @@ public void processElement(ProcessContext c) { */ static class FormatCountsFn extends DoFn, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV element, + @Timestamp Instant timestamp, + OutputReceiver receiver) { TableRow row = new TableRow() - .set("ts", c.timestamp().toString()) - .set("month", c.element().getKey()) - .set("tornado_count", c.element().getValue()); - c.output(row); + .set("ts", timestamp.toString()) + .set("month", element.getKey()) + .set("tornado_count", element.getValue()); + receiver.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java index 43d720e35268..67e0661a1a42 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/BigQueryTornadoes.java @@ -32,6 +32,8 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; @@ -79,10 +81,9 @@ public class BigQueryTornadoes { */ static class ExtractTornadoesFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { if ((Boolean) row.get("tornado")) { - c.output(Integer.parseInt((String) row.get("month"))); + receiver.output(Integer.parseInt((String) row.get("month"))); } } } @@ -93,12 +94,11 @@ public void processElement(ProcessContext c) { */ static class FormatCountsFn extends DoFn, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV element, OutputReceiver receiver) { TableRow row = - new TableRow() - .set("month", c.element().getKey()) - .set("tornado_count", c.element().getValue()); - c.output(row); + new TableRow().set("month", element.getKey()).set("tornado_count", element.getValue()); + receiver.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java index 2a581d769dc7..5f5b5d06b1f8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/CombinePerKeyExamples.java @@ -33,6 +33,8 @@ import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SerializableFunction; @@ -78,12 +80,11 @@ static class ExtractLargeWordsFn extends DoFn> { private final Counter smallerWords = Metrics.counter(ExtractLargeWordsFn.class, "smallerWords"); @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver> receiver) { String playName = (String) row.get("corpus"); String word = (String) row.get("word"); if (word.length() >= MIN_WORD_LENGTH) { - c.output(KV.of(word, playName)); + receiver.output(KV.of(word, playName)); } else { // Track how many smaller words we're not including. This information will be // visible in the Monitoring UI. @@ -98,10 +99,11 @@ public void processElement(ProcessContext c) { */ static class FormatShakespeareOutputFn extends DoFn, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV element, OutputReceiver receiver) { TableRow row = - new TableRow().set("word", c.element().getKey()).set("all_plays", c.element().getValue()); - c.output(row); + new TableRow().set("word", element.getKey()).set("all_plays", element.getValue()); + receiver.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java index 9187bb83d7da..26a6659a86b8 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/FilterExamples.java @@ -31,6 +31,9 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.SideInput; import org.apache.beam.sdk.transforms.Mean; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -87,8 +90,7 @@ public class FilterExamples { */ static class ProjectionFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { // Grab year, month, day, mean_temp from the row Integer year = Integer.parseInt((String) row.get("year")); Integer month = Integer.parseInt((String) row.get("month")); @@ -101,7 +103,7 @@ public void processElement(ProcessContext c) { .set("month", month) .set("day", day) .set("mean_temp", meanTemp); - c.output(outRow); + receiver.output(outRow); } } @@ -119,12 +121,11 @@ public FilterSingleMonthDataFn(Integer monthFilter) { } @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { Integer month; month = (Integer) row.get("month"); if (month.equals(this.monthFilter)) { - c.output(row); + receiver.output(row); } } } @@ -135,10 +136,9 @@ public void processElement(ProcessContext c) { */ static class ExtractTempFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { Double meanTemp = Double.parseDouble(row.get("mean_temp").toString()); - c.output(meanTemp); + receiver.output(meanTemp); } } @@ -178,16 +178,17 @@ public PCollection expand(PCollection rows) { ParDo.of( new DoFn() { @ProcessElement - public void processElement(ProcessContext c) { - Double meanTemp = - Double.parseDouble(c.element().get("mean_temp").toString()); - Double gTemp = c.sideInput(globalMeanTemp); + public void processElement( + @SideInput("globalMeanTemp") Double gTemp, + @Element TableRow element, + OutputReceiver receiver) { + Double meanTemp = Double.parseDouble(element.get("mean_temp").toString()); if (meanTemp < gTemp) { - c.output(c.element()); + receiver.output(element); } } }) - .withSideInputs(globalMeanTemp)); + .withSideInput("globalMeanTemp", globalMeanTemp)); return filteredRows; } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java index f78df0c09461..e6f8573705a2 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/JoinExamples.java @@ -26,6 +26,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.join.CoGbkResult; import org.apache.beam.sdk.transforms.join.CoGroupByKey; @@ -90,13 +92,14 @@ static PCollection joinEvents( ParDo.of( new DoFn, KV>() { @ProcessElement - public void processElement(ProcessContext c) { - KV e = c.element(); - String countryCode = e.getKey(); - String countryName = e.getValue().getOnly(countryInfoTag); - for (String eventInfo : c.element().getValue().getAll(eventInfoTag)) { + public void processElement( + @Element KV element, + OutputReceiver> receiver) { + String countryCode = element.getKey(); + String countryName = element.getValue().getOnly(countryInfoTag); + for (String eventInfo : element.getValue().getAll(eventInfoTag)) { // Generate a string that combines information from both collection values - c.output( + receiver.output( KV.of( countryCode, "Country name: " + countryName + ", Event info: " + eventInfo)); @@ -111,10 +114,11 @@ public void processElement(ProcessContext c) { ParDo.of( new DoFn, String>() { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV element, OutputReceiver receiver) { String outputstring = - "Country code: " + c.element().getKey() + ", " + c.element().getValue(); - c.output(outputstring); + "Country code: " + element.getKey() + ", " + element.getValue(); + receiver.output(outputstring); } })); return formattedResults; @@ -126,14 +130,13 @@ public void processElement(ProcessContext c) { */ static class ExtractEventDataFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver> receiver) { String countryCode = (String) row.get("ActionGeo_CountryCode"); String sqlDate = (String) row.get("SQLDATE"); String actor1Name = (String) row.get("Actor1Name"); String sourceUrl = (String) row.get("SOURCEURL"); String eventInfo = "Date: " + sqlDate + ", Actor1: " + actor1Name + ", url: " + sourceUrl; - c.output(KV.of(countryCode, eventInfo)); + receiver.output(KV.of(countryCode, eventInfo)); } } @@ -143,11 +146,10 @@ public void processElement(ProcessContext c) { */ static class ExtractCountryInfoFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver> receiver) { String countryCode = (String) row.get("FIPSCC"); String countryName = (String) row.get("HumanName"); - c.output(KV.of(countryCode, countryName)); + receiver.output(KV.of(countryCode, countryName)); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java index 8760d562d040..85df56a58258 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MaxPerKeyExamples.java @@ -30,6 +30,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.Validation; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.Max; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -73,23 +75,22 @@ public class MaxPerKeyExamples { */ static class ExtractTempFn extends DoFn> { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement( + @Element TableRow row, OutputReceiver> receiver) { Integer month = Integer.parseInt((String) row.get("month")); Double meanTemp = Double.parseDouble(row.get("mean_temp").toString()); - c.output(KV.of(month, meanTemp)); + receiver.output(KV.of(month, meanTemp)); } } /** Format the results to a TableRow, to save to BigQuery. */ static class FormatMaxesFn extends DoFn, TableRow> { @ProcessElement - public void processElement(ProcessContext c) { + public void processElement( + @Element KV element, OutputReceiver receiver) { TableRow row = - new TableRow() - .set("month", c.element().getKey()) - .set("max_mean_temp", c.element().getValue()); - c.output(row); + new TableRow().set("month", element.getKey()).set("max_mean_temp", element.getValue()); + receiver.output(row); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java index 713af1d50953..62f7b492c72b 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java @@ -26,6 +26,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -41,7 +43,7 @@ // always_run: true // default_example: false // pipeline_options: --project apache-beam-testing -// context_line: 102 +// context_line: 125 // categories: // - Filtering // - IO @@ -73,10 +75,9 @@ public class MinimalBigQueryTornadoes { */ static class ExtractTornadoesFn extends DoFn { @ProcessElement - public void processElement(ProcessContext c) { - TableRow row = c.element(); + public void processElement(@Element TableRow row, OutputReceiver receiver) { if ((Boolean) row.get("tornado")) { - c.output(Integer.parseInt((String) row.get("month"))); + receiver.output(Integer.parseInt((String) row.get("month"))); } } } @@ -87,8 +88,9 @@ public void processElement(ProcessContext c) { */ static class FormatCountsFn extends DoFn, String> { @ProcessElement - public void processElement(ProcessContext c) { - c.output(c.element().getKey() + ": " + c.element().getValue()); + public void processElement( + @Element KV element, OutputReceiver receiver) { + receiver.output(element.getKey() + ": " + element.getValue()); } } @@ -134,9 +136,9 @@ static class LogOutput extends DoFn { } @ProcessElement - public void processElement(ProcessContext c) { - LOG.info("{}{}", prefix, c.element()); - c.output(c.element()); + public void processElement(@Element T element, OutputReceiver receiver) { + LOG.info("{}{}", prefix, element); + receiver.output(element); } } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java index cd3c6dd84157..5270077ea037 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/TriggerExample.java @@ -38,6 +38,9 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.GroupByKey; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -46,6 +49,7 @@ import org.apache.beam.sdk.transforms.windowing.AfterWatermark; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.Repeatedly; import org.apache.beam.sdk.transforms.windowing.Window; import org.apache.beam.sdk.values.KV; @@ -364,15 +368,18 @@ public PCollection expand(PCollection> flowInfo) { new DoFn>, KV>() { @ProcessElement - public void processElement(ProcessContext c) throws Exception { - Iterable flows = c.element().getValue(); + public void processElement( + @Element KV> element, + OutputReceiver> receiver) + throws Exception { + Iterable flows = element.getValue(); Integer sum = 0; Long numberOfRecords = 0L; for (Integer value : flows) { sum += value; numberOfRecords++; } - c.output(KV.of(c.element().getKey(), sum + "," + numberOfRecords)); + receiver.output(KV.of(element.getKey(), sum + "," + numberOfRecords)); } })); PCollection output = results.apply(ParDo.of(new FormatTotalFlow(triggerType))); @@ -392,21 +399,27 @@ public FormatTotalFlow(String triggerType) { } @ProcessElement - public void processElement(ProcessContext c, BoundedWindow window) throws Exception { - String[] values = c.element().getValue().split(",", -1); + public void processElement( + PaneInfo pane, + @Timestamp Instant timestamp, + BoundedWindow window, + @Element KV element, + OutputReceiver receiver) + throws Exception { + String[] values = element.getValue().split(",", -1); TableRow row = new TableRow() .set("trigger_type", triggerType) - .set("freeway", c.element().getKey()) + .set("freeway", element.getKey()) .set("total_flow", Integer.parseInt(values[0])) .set("number_of_records", Long.parseLong(values[1])) .set("window", window.toString()) - .set("isFirst", c.pane().isFirst()) - .set("isLast", c.pane().isLast()) - .set("timing", c.pane().getTiming().toString()) - .set("event_time", c.timestamp().toString()) + .set("isFirst", pane.isFirst()) + .set("isLast", pane.isLast()) + .set("timing", pane.getTiming().toString()) + .set("event_time", timestamp.toString()) .set("processing_time", Instant.now().toString()); - c.output(row); + receiver.output(row); } } @@ -418,8 +431,9 @@ static class ExtractFlowInfo extends DoFn> { private static final int VALID_NUM_FIELDS = 50; @ProcessElement - public void processElement(ProcessContext c) throws Exception { - String[] laneInfo = c.element().split(",", -1); + public void processElement( + @Element String element, OutputReceiver> receiver) throws Exception { + String[] laneInfo = element.split(",", -1); if ("timestamp".equals(laneInfo[0])) { // Header row return; @@ -435,7 +449,7 @@ public void processElement(ProcessContext c) throws Exception { if (totalFlow == null || totalFlow <= 0) { return; } - c.output(KV.of(freeway, totalFlow)); + receiver.output(KV.of(freeway, totalFlow)); } } @@ -509,7 +523,8 @@ public void setup() { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { + public void processElement(@Element String element, OutputReceiver receiver) + throws Exception { Instant timestamp = Instant.now(); if (random.nextDouble() < THRESHOLD) { int range = MAX_DELAY - MIN_DELAY; @@ -517,7 +532,7 @@ public void processElement(ProcessContext c) throws Exception { long delayInMillis = TimeUnit.MINUTES.toMillis(delayInMinutes); timestamp = new Instant(timestamp.getMillis() - delayInMillis); } - c.outputWithTimestamp(c.element(), timestamp); + receiver.outputWithTimestamp(element, timestamp); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java b/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java index 4f24c69f74b7..99aada20669b 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java +++ b/examples/java/src/main/java/org/apache/beam/examples/snippets/Snippets.java @@ -76,6 +76,10 @@ import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.DoFn.BoundedPerElement; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; +import org.apache.beam.sdk.transforms.DoFn.SideInput; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; import org.apache.beam.sdk.transforms.Latest; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; @@ -540,14 +544,14 @@ public static PCollection coGroupByKeyTuple( ParDo.of( new DoFn, String>() { @ProcessElement - public void processElement(ProcessContext c) { - KV e = c.element(); + public void processElement( + @Element KV e, OutputReceiver receiver) { String name = e.getKey(); Iterable emailsIter = e.getValue().getAll(emailsTag); Iterable phonesIter = e.getValue().getAll(phonesTag); String formattedResult = Snippets.formatCoGbkResults(name, emailsIter, phonesIter); - c.output(formattedResult); + receiver.output(formattedResult); } })); // [END CoGroupByKeyTuple] @@ -642,20 +646,23 @@ public void process( new DoFn>() { @ProcessElement - public void process(ProcessContext c, @Timestamp Instant timestamp) { - Iterable> si = c.sideInput(mapIterable); + public void process( + @Timestamp Instant timestamp, + @Element Long element, + @SideInput("mapIterable") Iterable> si, + OutputReceiver> receiver) { // Take an element from the side input iterable (likely length 1) Map keyMap = si.iterator().next(); - c.outputWithTimestamp(KV.of(1L, c.element()), Instant.now()); + receiver.outputWithTimestamp(KV.of(1L, element), Instant.now()); LOG.info( "Value is {} with timestamp {}, using key A from side input with time {}.", - c.element(), + element, timestamp.toString(DateTimeFormat.forPattern("HH:mm:ss")), keyMap.get("Key_A")); } }) - .withSideInputs(mapIterable)); + .withSideInput("mapIterable", mapIterable)); p.run(); } @@ -701,9 +708,9 @@ public static void accessingValueProviderInfoAfterRunSnip1(String[] args) { // Define the DoFn that logs the ValueProvider value. @ProcessElement - public void process(ProcessContext c) { + public void process(PipelineOptions options) { - MyOptions ops = c.getPipelineOptions().as(MyOptions.class); + MyOptions ops = options.as(MyOptions.class); // This example logs the ValueProvider value, but you could store it by // pushing it to an external database. @@ -943,11 +950,13 @@ public void process(@Element String src, OutputReceiver o) { ParDo.of( new DoFn() { @ProcessElement - public void process(ProcessContext c) { - c.output((long) c.sideInput(sideInput).size()); + public void process( + @SideInput("sideInput") List sideInputValue, + OutputReceiver receiver) { + receiver.output((long) sideInputValue.size()); } }) - .withSideInputs(sideInput)); + .withSideInput("sideInput", sideInput)); // [END PeriodicallyUpdatingSideInputs] return result; } @@ -1188,7 +1197,10 @@ private static class BundleFinalization { private static class BundleFinalizationDoFn extends DoFn { // [START BundleFinalize] @ProcessElement - public void processElement(ProcessContext c, BundleFinalizer bundleFinalizer) { + public void processElement( + @Element String element, + OutputReceiver receiver, + BundleFinalizer bundleFinalizer) { // ... produce output ... bundleFinalizer.afterBundleCommit( diff --git a/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java b/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java index 4aa20fc10dfb..b7fb7b82db31 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java +++ b/examples/java/src/main/java/org/apache/beam/examples/subprocess/ExampleEchoPipeline.java @@ -28,6 +28,8 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.slf4j.Logger; @@ -84,11 +86,13 @@ public void setUp() throws Exception { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { + public void processElement( + @Element KV element, OutputReceiver> receiver) + throws Exception { try { // Our Library takes a single command in position 0 which it will echo back in the result SubProcessCommandLineArgs commands = new SubProcessCommandLineArgs(); - Command command = new Command(0, String.valueOf(c.element().getValue())); + Command command = new Command(0, String.valueOf(element.getValue())); commands.putCommand(command); // The ProcessingKernel deals with the execution of the process @@ -97,7 +101,7 @@ public void processElement(ProcessContext c) throws Exception { // Run the command and work through the results List results = kernel.exec(commands); for (String s : results) { - c.output(KV.of(c.element().getKey(), s)); + receiver.output(KV.of(element.getKey(), s)); } } catch (Exception ex) { LOG.error("Error processing element ", ex); diff --git a/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java b/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java index 19c83c6eb73c..0d9e257fc6c0 100644 --- a/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java +++ b/examples/java/src/test/java/org/apache/beam/examples/cookbook/TriggerExampleTest.java @@ -29,6 +29,8 @@ import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.windowing.FixedWindows; import org.apache.beam.sdk.transforms.windowing.Window; @@ -146,8 +148,8 @@ static String canonicalFormat(TableRow row) { static class FormatResults extends DoFn { @ProcessElement - public void processElement(ProcessContext c) throws Exception { - TableRow element = c.element(); + public void processElement(@Element TableRow element, OutputReceiver receiver) + throws Exception { TableRow row = new TableRow() .set("trigger_type", element.get("trigger_type")) @@ -158,7 +160,7 @@ public void processElement(ProcessContext c) throws Exception { .set("isLast", element.get("isLast")) .set("timing", element.get("timing")) .set("window", element.get("window")); - c.output(canonicalFormat(row)); + receiver.output(canonicalFormat(row)); } } } diff --git a/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java b/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java index 01055d9658a9..9981b4040e3b 100644 --- a/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java +++ b/examples/java/src/test/java/org/apache/beam/examples/subprocess/ExampleEchoPipelineTest.java @@ -41,6 +41,8 @@ import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.Element; +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -144,11 +146,13 @@ public void setUp() throws Exception { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { + public void processElement( + @Element KV element, OutputReceiver> receiver) + throws Exception { try { // Our Library takes a single command in position 0 which it will echo back in the result SubProcessCommandLineArgs commands = new SubProcessCommandLineArgs(); - Command command = new Command(0, String.valueOf(c.element().getValue())); + Command command = new Command(0, String.valueOf(element.getValue())); commands.putCommand(command); // The ProcessingKernel deals with the execution of the process @@ -157,7 +161,7 @@ public void processElement(ProcessContext c) throws Exception { // Run the command and work through the results List results = kernel.exec(commands); for (String s : results) { - c.output(KV.of(c.element().getKey(), s)); + receiver.output(KV.of(element.getKey(), s)); } } catch (Exception ex) { LOG.error("Error processing element ", ex); diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt index 05cb404ac6d2..7be4df2c8a91 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/DebuggingWordCount.kt @@ -25,6 +25,8 @@ import org.apache.beam.sdk.options.Description import org.apache.beam.sdk.options.PipelineOptionsFactory import org.apache.beam.sdk.testing.PAssert import org.apache.beam.sdk.transforms.DoFn +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.ParDo import org.apache.beam.sdk.values.KV import org.slf4j.LoggerFactory @@ -84,18 +86,18 @@ public object DebuggingWordCount { private val unmatchedWords = Metrics.counter(FilterTextFn::class.java, "unmatchedWords") @ProcessElement - fun processElement(c: ProcessContext) { - if (filter.matcher(c.element().key).matches()) { + fun processElement(@Element element: KV, receiver: OutputReceiver>) { + if (filter.matcher(element.key).matches()) { // Log at the "DEBUG" level each element that we match. When executing this pipeline // these log lines will appear only if the log level is set to "DEBUG" or lower. - LOG.debug("Matched: ${c.element().key}") + LOG.debug("Matched: ${element.key}") matchedWords.inc() - c.output(c.element()) + receiver.output(element) } else { // Log at the "TRACE" level each element that is not matched. Different log levels // can be used to control the verbosity of logging providing an effective mechanism // to filter less important information. - LOG.trace("Did not match: ${c.element().key}") + LOG.trace("Did not match: ${element.key}") unmatchedWords.inc() } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt index ec56bc659970..8edc98872350 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/BigQueryTornadoes.kt @@ -27,6 +27,8 @@ import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.Count import org.apache.beam.sdk.transforms.DoFn +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.PTransform import org.apache.beam.sdk.transforms.ParDo import org.apache.beam.sdk.values.KV @@ -73,10 +75,9 @@ object BigQueryTornadoes { */ internal class ExtractTornadoesFn : DoFn() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver) { if (row["tornado"] as Boolean) { - c.output(Integer.parseInt(row["month"] as String)) + receiver.output(Integer.parseInt(row["month"] as String)) } } } @@ -87,11 +88,11 @@ object BigQueryTornadoes { */ internal class FormatCountsFn : DoFn, TableRow>() { @ProcessElement - fun processElement(c: ProcessContext) { + fun processElement(@Element element: KV, receiver: OutputReceiver) { val row = TableRow() - .set("month", c.element().key) - .set("tornado_count", c.element().value) - c.output(row) + .set("month", element.key) + .set("tornado_count", element.value) + receiver.output(row) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt index 9e388031002d..4d89b692dd82 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/CombinePerKeyExamples.kt @@ -26,6 +26,8 @@ import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.metrics.Metrics import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.* +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.values.KV import org.apache.beam.sdk.values.PCollection @@ -70,12 +72,11 @@ object CombinePerKeyExamples { private val smallerWords = Metrics.counter(ExtractLargeWordsFn::class.java, "smallerWords") @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { val playName = row["corpus"] as String val word = row["word"] as String if (word.length >= MIN_WORD_LENGTH) { - c.output(KV.of(word, playName)) + receiver.output(KV.of(word, playName)) } else { // Track how many smaller words we're not including. This information will be // visible in the Monitoring UI. @@ -90,9 +91,9 @@ object CombinePerKeyExamples { */ internal class FormatShakespeareOutputFn : DoFn, TableRow>() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = TableRow().set("word", c.element().key).set("all_plays", c.element().value) - c.output(row) + fun processElement(@Element element: KV, receiver: OutputReceiver) { + val row = TableRow().set("word", element.key).set("all_plays", element.value) + receiver.output(row) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt index 2625f5bfec10..218188448965 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/FilterExamples.kt @@ -25,6 +25,9 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.* +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver +import org.apache.beam.sdk.transforms.DoFn.SideInput import org.apache.beam.sdk.values.PCollection import java.util.logging.Logger @@ -80,8 +83,7 @@ object FilterExamples { */ internal class ProjectionFn : DoFn() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver) { // Grab year, month, day, mean_temp from the row val year = Integer.parseInt(row["year"] as String) val month = Integer.parseInt(row["month"] as String) @@ -94,7 +96,7 @@ object FilterExamples { .set("month", month) .set("day", day) .set("mean_temp", meanTemp) - c.output(outRow) + receiver.output(outRow) } } @@ -108,11 +110,10 @@ object FilterExamples { internal class FilterSingleMonthDataFn(private var monthFilter: Int?) : DoFn() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver) { val month = row["month"] if (month == this.monthFilter) { - c.output(row) + receiver.output(row) } } } @@ -123,10 +124,9 @@ object FilterExamples { */ internal class ExtractTempFn : DoFn() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver) { val meanTemp = java.lang.Double.parseDouble(row["mean_temp"].toString()) - c.output(meanTemp) + receiver.output(meanTemp) } } @@ -158,15 +158,17 @@ object FilterExamples { ParDo.of( object : DoFn() { @ProcessElement - fun processElement(c: ProcessContext) { - val meanTemp = java.lang.Double.parseDouble(c.element()["mean_temp"].toString()) - val gTemp = c.sideInput(globalMeanTemp) + fun processElement( + @SideInput("globalMeanTemp") gTemp: Double, + @Element element: TableRow, + receiver: OutputReceiver) { + val meanTemp = java.lang.Double.parseDouble(element["mean_temp"].toString()) if (meanTemp < gTemp) { - c.output(c.element()) + receiver.output(element) } } }) - .withSideInputs(globalMeanTemp)) + .withSideInput("globalMeanTemp", globalMeanTemp)) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt index 2f2215e1d96a..7f81629ae463 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/JoinExamples.kt @@ -26,6 +26,8 @@ import org.apache.beam.sdk.options.PipelineOptions import org.apache.beam.sdk.options.PipelineOptionsFactory import org.apache.beam.sdk.options.Validation import org.apache.beam.sdk.transforms.DoFn +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.ParDo import org.apache.beam.sdk.transforms.join.CoGbkResult import org.apache.beam.sdk.transforms.join.CoGroupByKey @@ -89,13 +91,14 @@ object JoinExamples { ParDo.of( object : DoFn, KV>() { @ProcessElement - fun processElement(c: ProcessContext) { - val e = c.element() - val countryCode = e.key - val countryName = e.value.getOnly(countryInfoTag) - for (ei in c.element().value.getAll(eventInfoTag)) { + fun processElement( + @Element element: KV, + receiver: OutputReceiver>) { + val countryCode = element.key + val countryName = element.value.getOnly(countryInfoTag) + for (ei in element.value.getAll(eventInfoTag)) { // Generate a string that combines information from both collection values - c.output( + receiver.output( KV.of( countryCode, "Country name: $countryName, Event info: $ei")) @@ -109,9 +112,9 @@ object JoinExamples { ParDo.of( object : DoFn, String>() { @ProcessElement - fun processElement(c: ProcessContext) { - val outputString = "Country code: ${c.element().key}, ${c.element().value}" - c.output(outputString) + fun processElement(@Element element: KV, receiver: OutputReceiver) { + val outputString = "Country code: ${element.key}, ${element.value}" + receiver.output(outputString) } })) } @@ -122,14 +125,13 @@ object JoinExamples { */ internal class ExtractEventDataFn : DoFn>() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { val countryCode = row["ActionGeo_CountryCode"] as String val sqlDate = row["SQLDATE"] as String val actor1Name = row["Actor1Name"] as String val sourceUrl = row["SOURCEURL"] as String val eventInfo = "Date: $sqlDate, Actor1: $actor1Name, url: $sourceUrl" - c.output(KV.of(countryCode, eventInfo)) + receiver.output(KV.of(countryCode, eventInfo)) } } @@ -139,11 +141,10 @@ object JoinExamples { */ internal class ExtractCountryInfoFn : DoFn>() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { val countryCode = row["FIPSCC"] as String val countryName = row["HumanName"] as String - c.output(KV.of(countryCode, countryName)) + receiver.output(KV.of(countryCode, countryName)) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt index 11418d3933cf..ad16ecde9657 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/MaxPerKeyExamples.kt @@ -26,6 +26,8 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.options.* import org.apache.beam.sdk.transforms.DoFn +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.Max import org.apache.beam.sdk.transforms.PTransform import org.apache.beam.sdk.transforms.ParDo @@ -73,22 +75,21 @@ object MaxPerKeyExamples { */ internal class ExtractTempFn : DoFn>() { @ProcessElement - fun processElement(c: ProcessContext) { - val row = c.element() + fun processElement(@Element row: TableRow, receiver: OutputReceiver>) { val month = Integer.parseInt(row["month"] as String) val meanTemp = java.lang.Double.parseDouble(row["mean_temp"].toString()) - c.output(KV.of(month, meanTemp)) + receiver.output(KV.of(month, meanTemp)) } } /** Format the results to a TableRow, to save to BigQuery. */ internal class FormatMaxesFn : DoFn, TableRow>() { @ProcessElement - fun processElement(c: ProcessContext) { + fun processElement(@Element element: KV, receiver: OutputReceiver) { val row = TableRow() - .set("month", c.element().key) - .set("max_mean_temp", c.element().value) - c.output(row) + .set("month", element.key) + .set("max_mean_temp", element.value) + receiver.output(row) } } diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt index 4afa7d0dfc70..bb8c0900e319 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/cookbook/TriggerExample.kt @@ -34,6 +34,9 @@ import org.apache.beam.sdk.options.Description import org.apache.beam.sdk.options.PipelineOptionsFactory import org.apache.beam.sdk.options.StreamingOptions import org.apache.beam.sdk.transforms.DoFn +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver +import org.apache.beam.sdk.transforms.DoFn.Timestamp import org.apache.beam.sdk.transforms.GroupByKey import org.apache.beam.sdk.transforms.PTransform import org.apache.beam.sdk.transforms.ParDo @@ -364,15 +367,17 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement(c: ProcessContext) { - val flows = c.element().value + fun processElement( + @Element element: KV>, + receiver: OutputReceiver>) { + val flows = element.value var sum = 0 var numberOfRecords = 0L for (value in flows) { sum += value numberOfRecords++ } - c.output(KV.of(c.element().key, "$sum,$numberOfRecords")) + receiver.output(KV.of(element.key, "$sum,$numberOfRecords")) } })) return results.apply(ParDo.of(FormatTotalFlow(triggerType))) @@ -387,20 +392,25 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement(c: ProcessContext, window: BoundedWindow) { - val values = c.element().value.split(",".toRegex()).toTypedArray() + fun processElement( + @Element element: KV, + window: BoundedWindow, + pane: PaneInfo, + @Timestamp timestamp: Instant, + receiver: OutputReceiver) { + val values = element.value.split(",".toRegex()).toTypedArray() val row = TableRow() .set("trigger_type", triggerType) - .set("freeway", c.element().key) + .set("freeway", element.key) .set("total_flow", Integer.parseInt(values[0])) .set("number_of_records", java.lang.Long.parseLong(values[1])) .set("window", window.toString()) - .set("isFirst", c.pane().isFirst) - .set("isLast", c.pane().isLast) - .set("timing", c.pane().timing.toString()) - .set("event_time", c.timestamp().toString()) + .set("isFirst", pane.isFirst) + .set("isLast", pane.isLast) + .set("timing", pane.timing.toString()) + .set("event_time", timestamp.toString()) .set("processing_time", Instant.now().toString()) - c.output(row) + receiver.output(row) } } @@ -412,8 +422,8 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement(c: ProcessContext) { - val laneInfo = c.element().split(",".toRegex()).toTypedArray() + fun processElement(@Element element: String, receiver: OutputReceiver>) { + val laneInfo = element.split(",".toRegex()).toTypedArray() if ("timestamp" == laneInfo[0]) { // Header row return @@ -429,7 +439,7 @@ object TriggerExample { if (totalFlow == null || totalFlow <= 0) { return } - c.output(KV.of(freeway, totalFlow)) + receiver.output(KV.of(freeway, totalFlow)) } companion object { @@ -486,7 +496,7 @@ object TriggerExample { @ProcessElement @Throws(Exception::class) - fun processElement(c: ProcessContext) { + fun processElement(@Element element: String, receiver: OutputReceiver) { var timestamp = Instant.now() if (random.nextDouble() < THRESHOLD) { val range = MAX_DELAY - MIN_DELAY @@ -494,7 +504,7 @@ object TriggerExample { val delayInMillis = TimeUnit.MINUTES.toMillis(delayInMinutes.toLong()) timestamp = Instant(timestamp.millis - delayInMillis) } - c.outputWithTimestamp(c.element(), timestamp) + receiver.outputWithTimestamp(element, timestamp) } companion object { diff --git a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt index d2f58c215a56..eb2099372482 100644 --- a/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt +++ b/examples/kotlin/src/main/java/org/apache/beam/examples/kotlin/snippets/Snippets.kt @@ -33,6 +33,8 @@ import org.apache.beam.sdk.io.gcp.bigquery.DynamicDestinations import org.apache.beam.sdk.io.gcp.bigquery.TableDestination import org.apache.beam.sdk.io.gcp.bigquery.WriteResult import org.apache.beam.sdk.transforms.* +import org.apache.beam.sdk.transforms.DoFn.Element +import org.apache.beam.sdk.transforms.DoFn.OutputReceiver import org.apache.beam.sdk.transforms.join.CoGbkResult import org.apache.beam.sdk.transforms.join.CoGroupByKey import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple @@ -360,13 +362,12 @@ object Snippets { ParDo.of( object : DoFn, String>() { @ProcessElement - fun processElement(c: ProcessContext) { - val e = c.element() - val name = e.key - val emailsIter = e.value.getAll(emailsTag) - val phonesIter = e.value.getAll(phonesTag) + fun processElement(@Element element: KV, receiver: OutputReceiver) { + val name = element.key + val emailsIter = element.value.getAll(emailsTag) + val phonesIter = element.value.getAll(phonesTag) val formattedResult = formatCoGbkResults(name, emailsIter, phonesIter) - c.output(formattedResult) + receiver.output(formattedResult) } })) } From bb4507d45b6b9a4dee7dabacc2f2f5eaab6ed872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 07:56:02 -0400 Subject: [PATCH 153/490] Bump google.golang.org/api from 0.278.0 to 0.279.0 in /sdks (#38480) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.278.0 to 0.279.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.278.0...v0.279.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.279.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index d6f320ac7882..028923c21dd8 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.44.0 golang.org/x/text v0.37.0 - google.golang.org/api v0.278.0 + google.golang.org/api v0.279.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.81.0 google.golang.org/protobuf v1.36.11 diff --git a/sdks/go.sum b/sdks/go.sum index 7570e58c88df..fbe69acf027a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1342,8 +1342,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= -google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74= +google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From c6e0853bf09c2b0f7fd39ba363eb4ec6fc07f0ee Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 13 May 2026 11:01:51 -0400 Subject: [PATCH 154/490] Setup Validates runner tests for Spark4 runner (#38478) --- .github/workflows/README.md | 4 +- .../beam_PostCommit_Java_PVR_Spark4_Batch.yml | 104 ++++++++++++++++++ ...m_PostCommit_Java_PVR_Spark4_Streaming.yml | 96 ++++++++++++++++ ...ostCommit_Java_ValidatesRunner_Spark4.yml} | 20 ++-- runners/spark/4/build.gradle | 12 +- 5 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml create mode 100644 .github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml rename .github/workflows/{beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml => beam_PostCommit_Java_ValidatesRunner_Spark4.yml} (82%) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 9867810a7419..c6a95b29b4c0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -357,6 +357,8 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java SingleStoreIO IT ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml) | N/A |`beam_PostCommit_Java_SingleStoreIO_IT.json`| [![.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Spark3 Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Spark3_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Spark Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Spark_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml?query=event%3Aschedule) | +| [ PostCommit Java PVR Spark4 Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Spark4_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml?query=event%3Aschedule) | +| [ PostCommit Java PVR Spark4 Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Spark4_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml?query=event%3Aschedule) | | [ PostCommit Java Tpcds Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml) | N/A |`beam_PostCommit_Java_Tpcds_Dataflow.json`| [![.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Java Tpcds Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Flink.yml) | N/A |`beam_PostCommit_Java_Tpcds_Flink.json`| [![.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Flink.yml?query=event%3Aschedule) | | [ PostCommit Java Tpcds Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Spark.yml) | N/A |`beam_PostCommit_Java_Tpcds_Spark.json`| [![.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Spark.yml?query=event%3Aschedule) | @@ -370,7 +372,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner SparkStructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Spark4StructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml?query=event%3Aschedule) | +| [ PostCommit Java ValidatesRunner Spark4 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark4.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Twister2 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Twister2.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner ULR ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_ULR.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml?query=event%3Aschedule) | | [ PostCommit Java ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml) | N/A |`beam_PostCommit_Java.json`| [![.github/workflows/beam_PostCommit_Java.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml new file mode 100644 index 000000000000..eb2d65afc2d8 --- /dev/null +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml @@ -0,0 +1,104 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: PostCommit Java PVR Spark4 Batch + +on: + schedule: + - cron: '45 5/6 * * *' + pull_request_target: + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_PVR_Spark4_Batch.json'] + workflow_dispatch: + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +permissions: + actions: write + pull-requests: write + checks: write + contents: read + deployments: read + id-token: none + issues: write + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + +jobs: + beam_PostCommit_Java_PVR_Spark4_Batch: + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 240 + strategy: + matrix: + job_name: [beam_PostCommit_Java_PVR_Spark4_Batch] + job_phrase: [Run Java Spark4 PortableValidatesRunner Batch] + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') + steps: + - uses: actions/checkout@v6 + - name: Setup repository + uses: ./.github/actions/setup-action + with: + comment_phrase: ${{ matrix.job_phrase }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + java-version: 17 + - name: run PostCommit Java PortableValidatesRunner Spark4 Batch script + env: + CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: | + :runners:spark:4:job-server:validatesPortableRunnerBatch \ + :runners:spark:4:job-server:validatesPortableRunnerDocker + - name: Archive JUnit Test Results + uses: actions/upload-artifact@v7 + if: ${{ !success() }} + with: + name: JUnit Test Results + path: "**/build/reports/tests/" + - name: Publish JUnit Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + large_files: true + commit: '${{ env.prsha || env.GITHUB_SHA }}' + comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} + files: '**/build/test-results/**/*.xml' + - name: Archive SpotBugs Results + uses: actions/upload-artifact@v7 + with: + name: SpotBugs Results + path: "**/build/reports/spotbugs/*.html" diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml new file mode 100644 index 000000000000..38866bedeb1b --- /dev/null +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: PostCommit Java PVR Spark4 Streaming + +on: + schedule: + - cron: '45 5/6 * * *' + pull_request_target: + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_PVR_Spark4_Streaming.json'] + workflow_dispatch: + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +permissions: + actions: write + pull-requests: write + checks: write + contents: read + deployments: read + id-token: none + issues: write + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + +jobs: + beam_PostCommit_Java_PVR_Spark4_Streaming: + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 180 + strategy: + matrix: + job_name: [beam_PostCommit_Java_PVR_Spark4_Streaming] + job_phrase: [Run Java Spark v4 PortableValidatesRunner Streaming] + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event.comment.body == 'Run Java Spark v4 PortableValidatesRunner Streaming' + steps: + - uses: actions/checkout@v6 + - name: Setup repository + uses: ./.github/actions/setup-action + with: + comment_phrase: ${{ matrix.job_phrase }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + java-version: 17 + - name: run PostCommit Java PortableValidatesRunner Spark4 Streaming script + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :runners:spark:4:job-server:validatesPortableRunnerStreaming + - name: Archive JUnit Test Results + uses: actions/upload-artifact@v7 + if: ${{ !success() }} + with: + name: JUnit Test Results + path: "**/build/reports/tests/" + - name: Publish JUnit Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + commit: '${{ env.prsha || env.GITHUB_SHA }}' + comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} + files: '**/build/test-results/**/*.xml' + large_files: true \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml similarity index 82% rename from .github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml rename to .github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml index d1f214e6a458..c6e742204894 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml @@ -13,13 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: PostCommit Java ValidatesRunner Spark4 StructuredStreaming +name: PostCommit Java ValidatesRunner Spark4 on: schedule: - cron: '45 4/6 * * *' pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json'] + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4.json'] workflow_dispatch: #Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event @@ -49,19 +49,19 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming: + beam_PostCommit_Java_ValidatesRunner_Spark4: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 120 strategy: matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming] - job_phrase: [Run Spark4 StructuredStreaming ValidatesRunner] + job_name: [beam_PostCommit_Java_ValidatesRunner_Spark4] + job_phrase: [Run Spark4 ValidatesRunner] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run Spark4 StructuredStreaming ValidatesRunner' + github.event.comment.body == 'Run Spark4 ValidatesRunner' steps: - uses: actions/checkout@v6 - name: Setup repository @@ -74,13 +74,11 @@ jobs: uses: ./.github/actions/setup-environment-action with: java-version: '17' - - name: run validatesStructuredStreamingRunnerBatch script + - name: run validatesRunner script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:spark:4:validatesStructuredStreamingRunnerBatch - arguments: | - -PtestJavaVersion=17 \ - -PdisableSpotlessCheck=true \ + gradle-command: :runners:spark:4:validatesRunner + arguments: -PdisableSpotlessCheck=true - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} diff --git a/runners/spark/4/build.gradle b/runners/spark/4/build.gradle index 01fb3680b078..ec1af8df38a4 100644 --- a/runners/spark/4/build.gradle +++ b/runners/spark/4/build.gradle @@ -36,13 +36,23 @@ apply from: "$basePath/spark_runner.gradle" // Spark 4 always requires Java 17, so unconditionally add the --add-opens flags // required by Kryo and other libraries that use reflection on JDK internals. -test { +tasks.withType(Test).configureEach { jvmArgs "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens=java.base/java.nio=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED" } +tasks.validatesStructuredStreamingRunnerBatch { + filter { + // TODO(https://github.com/apache/beam/issues/36841): currently failing with INTERNAL_ERROR] + // Couldn't find method fromByteArray with arguments + excludeTestsMatching 'org.apache.beam.sdk.transforms.CombineTest$BasicTests.testHotKeyCombining' + } +} + +tasks.validatesRunner.dependsOn(validatesStructuredStreamingRunnerBatch) + // Exclude DStream-based streaming tests from the shared-base copy: the Spark 4 module // supports only structured streaming (batch) and does not include legacy DStream support. // Streaming test utilities also depend on kafka.server.KafkaServerStartable which was From bb591363ad74b65aa6a15390529b28d06f7b44eb Mon Sep 17 00:00:00 2001 From: Amar3tto Date: Wed, 13 May 2026 17:41:19 +0000 Subject: [PATCH 155/490] Moving to 2.75.0-SNAPSHOT on master branch. --- .asf.yaml | 1 + gradle.properties | 4 ++-- scripts/beam-sql.sh | 2 +- sdks/go/pkg/beam/core/core.go | 2 +- sdks/python/apache_beam/version.py | 2 +- sdks/typescript/package.json | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.asf.yaml b/.asf.yaml index e22c1d6c1d66..e69dcbc6ff96 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -51,6 +51,7 @@ github: protected_branches: master: {} + release-2.74: {} release-2.73.0-postrelease: {} release-2.73: {} release-2.72.0-postrelease: {} diff --git a/gradle.properties b/gradle.properties index fc2813f11fec..1d40c7ed4669 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,8 +30,8 @@ signing.gnupg.useLegacyGpg=true # buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy. # To build a custom Beam version make sure you change it in both places, see # https://github.com/apache/beam/issues/21302. -version=2.74.0-SNAPSHOT -sdk_version=2.74.0.dev +version=2.75.0-SNAPSHOT +sdk_version=2.75.0.dev javaVersion=11 diff --git a/scripts/beam-sql.sh b/scripts/beam-sql.sh index b83e8bc4ecae..c9ec365b99bc 100755 --- a/scripts/beam-sql.sh +++ b/scripts/beam-sql.sh @@ -22,7 +22,7 @@ set -e # Exit immediately if a command exits with a non-zero status. # --- Configuration --- -DEFAULT_BEAM_VERSION="2.74.0" +DEFAULT_BEAM_VERSION="2.75.0" MAIN_CLASS="org.apache.beam.sdk.extensions.sql.jdbc.BeamSqlLine" # Directory to store cached executable JAR files CACHE_DIR="${HOME}/.beam/cache" diff --git a/sdks/go/pkg/beam/core/core.go b/sdks/go/pkg/beam/core/core.go index a277d1efdd96..e3a8900b1e77 100644 --- a/sdks/go/pkg/beam/core/core.go +++ b/sdks/go/pkg/beam/core/core.go @@ -27,7 +27,7 @@ const ( // SdkName is the human readable name of the SDK for UserAgents. SdkName = "Apache Beam SDK for Go" // SdkVersion is the current version of the SDK. - SdkVersion = "2.74.0.dev" + SdkVersion = "2.75.0.dev" // DefaultDockerImage represents the associated image for this release. DefaultDockerImage = "apache/beam_go_sdk:" + SdkVersion diff --git a/sdks/python/apache_beam/version.py b/sdks/python/apache_beam/version.py index 633523425762..7d1cd44ad012 100644 --- a/sdks/python/apache_beam/version.py +++ b/sdks/python/apache_beam/version.py @@ -17,4 +17,4 @@ """Apache Beam SDK version information and utilities.""" -__version__ = '2.74.0.dev' +__version__ = '2.75.0.dev' diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index e6042aa40e63..d052111248ed 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "apache-beam", - "version": "2.74.0-SNAPSHOT", + "version": "2.75.0-SNAPSHOT", "devDependencies": { "@google-cloud/bigquery": "^5.12.0", "@types/mocha": "^9.0.0", From 3c9e27b3480d336707a3518354900c564cda9c9c Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 13 May 2026 15:01:13 -0400 Subject: [PATCH 156/490] update port for jdbc test (#38485) * increase timeout switch to minute :) * check post commit * try adding back nat for one test case * try getting port instead * revert time change --- .github/trigger_files/beam_PostCommit_XVR_Direct.json | 2 +- sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_XVR_Direct.json b/.github/trigger_files/beam_PostCommit_XVR_Direct.json index 73867c483554..702328d16d4b 100644 --- a/.github/trigger_files/beam_PostCommit_XVR_Direct.json +++ b/.github/trigger_files/beam_PostCommit_XVR_Direct.json @@ -1,3 +1,3 @@ { - "modification": 5 + "modification": 1 } diff --git a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go index 4cd3c38a3bf2..2ea84d427987 100644 --- a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go +++ b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go @@ -32,6 +32,7 @@ import ( "github.com/apache/beam/sdks/v2/go/test/integration/internal/containers" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" + "strings" "github.com/testcontainers/testcontainers-go/wait" ) @@ -60,7 +61,8 @@ func setupTestContainer(ctx context.Context, t *testing.T, dbname, username, pas hostname := "localhost" dbURL := func(host string, port string) string { - return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, port, dbname) + cleanPort := strings.Split(port, "/")[0] + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, cleanPort, dbname) } waitStrategy := wait.ForSQL(postgresPort, "postgres", dbURL).WithStartupTimeout(time.Second * 5) From 00023fe237097579e02fdac7607ba65bf6937487 Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Wed, 13 May 2026 22:00:23 +0200 Subject: [PATCH 157/490] [runners-spark] Spark 4 follow-up: Announce in CHANGES.md and address remaining review comments from #38255 (#38489) * [runners-spark] Spark 4 follow-up: address remaining review comments from #38255 Addresses two trailing review comments left on PR #38255 at the approved/merged head SHA that were never folded in before merge: - Restore the `feature Y added to Java SDK` placeholder line in CHANGES.md Highlights (lost to a rebase). Comment: https://github.com/apache/beam/pull/38255#discussion_r3204237226 - Delete two orphaned `.github/trigger_files/*.json`: - `beam_PreCommit_Java_Spark4_Versions.json`: its workflow YAML was deleted in #38255. - `beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json`: flagged as "not effective yet"; since then, #38478 renamed the associated workflow YAML to `beam_PostCommit_Java_ValidatesRunner_Spark4.yml`, so this trigger JSON is doubly orphaned. Comment: https://github.com/apache/beam/pull/38255#discussion_r3204241059 The renamed workflow YAML stays in place; per the trigger-files convention these JSONs are only added when an author wants to manually trigger a specific PostCommit. * Update CHANGES.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Fix CHANGES.md URL: /pull/N -> /issues/N for validateChanges * Update CHANGES.md Co-authored-by: Yi Hu --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Yi Hu --- ...tCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json | 3 --- .github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json | 3 --- CHANGES.md | 2 ++ 3 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 .github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json delete mode 100644 .github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json deleted file mode 100644 index c4edaa85a89d..000000000000 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "comment": "Modify this file in a trivial way to cause this test suite to run" -} diff --git a/.github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json b/.github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json deleted file mode 100644 index c4edaa85a89d..000000000000 --- a/.github/trigger_files/beam_PreCommit_Java_Spark4_Versions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "comment": "Modify this file in a trivial way to cause this test suite to run" -} diff --git a/CHANGES.md b/CHANGES.md index 058e19ea7435..ca911e52a7ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -60,6 +60,8 @@ ## Highlights * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). +* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). +* Spark 4 runner support for Java SDK ([#38255](https://github.com/apache/beam/issues/38255)). ## I/Os From 278db88e00e759e350102e6909b316adcdff9a1c Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 13 May 2026 22:23:25 -0400 Subject: [PATCH 158/490] Sickbay two failed tests due to new schema coder urn. (#38497) --- .../beam_PostCommit_Java_ValidatesRunner_ULR.json | 3 ++- runners/portability/java/build.gradle | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json index 6e2f429dd24e..fbd81891f93b 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json @@ -2,5 +2,6 @@ "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", "comment": "Modify this file in a trivial way to cause this test suite to run", "https://github.com/apache/beam/pull/31156": "noting that PR #31156 should run this test", - "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" + "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface", + "https://github.com/apache/beam/pull/38497": "sickbay two failed tests" } diff --git a/runners/portability/java/build.gradle b/runners/portability/java/build.gradle index 6e3b431e802b..aa147e8426c4 100644 --- a/runners/portability/java/build.gradle +++ b/runners/portability/java/build.gradle @@ -214,6 +214,11 @@ def createUlrValidatesRunnerTask = { name, environmentType, dockerImageTask = "" // TODO(https://github.com/apache/beam/issues/31231) excludeTestsMatching 'org.apache.beam.sdk.transforms.RedistributeTest.testRedistributePreservesMetadata' + // TODO(https://github.com/apache/beam/issues/33859): Failed with "KeyError: 'beam:coder:schema:v1'". + // New schema coder urn is not yet supported in runners other than dataflow + excludeTestsMatching 'org.apache.beam.sdk.transforms.PerKeyOrderingTest.testMultipleStatefulOrderingWithShuffle' + excludeTestsMatching 'org.apache.beam.sdk.transforms.PerKeyOrderingTest.testMultipleStatefulOrderingWithoutShuffle' + for (String test : sickbayTests) { excludeTestsMatching test } From fd20c967053ea89c6ffa8020f34a51ac16e14365 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 14 May 2026 00:06:56 -0400 Subject: [PATCH 159/490] Bump Java dev image for Dataflow (#38496) --- runners/google-cloud-dataflow-java/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index e4fce90f2e9a..93fe9cb227bb 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -52,8 +52,8 @@ evaluationDependsOn(":sdks:java:container:java11") ext.dataflowLegacyEnvironmentMajorVersion = '8' ext.dataflowFnapiEnvironmentMajorVersion = '8' -ext.dataflowLegacyContainerVersion = 'beam-master-20260424' -ext.dataflowFnapiContainerVersion = 'beam-master-20260424' +ext.dataflowLegacyContainerVersion = 'beam-master-20260513' +ext.dataflowFnapiContainerVersion = 'beam-master-20260513' ext.dataflowContainerBaseRepository = 'gcr.io/cloud-dataflow/v1beta3' processResources { From a47497fa1d7ca60211f73e9b72a18f9126062d61 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Thu, 14 May 2026 12:50:22 +0400 Subject: [PATCH 160/490] Fix gradle command --- .github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml index eb2d65afc2d8..9a34099cebb9 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml @@ -56,7 +56,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 240 strategy: - matrix: + matrix: job_name: [beam_PostCommit_Java_PVR_Spark4_Batch] job_phrase: [Run Java Spark4 PortableValidatesRunner Batch] if: | @@ -82,7 +82,7 @@ jobs: with: gradle-command: | :runners:spark:4:job-server:validatesPortableRunnerBatch \ - :runners:spark:4:job-server:validatesPortableRunnerDocker + :runners:spark:4:job-server:validatesPortableRunnerDocker \ - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} From 9bc67ec45e202fb18bf6a4a1bba3da3c2ee3218f Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 14 May 2026 10:22:06 -0400 Subject: [PATCH 161/490] Fix iceberg gcs dependency after gcsio 3.0 upgrade (#38488) --- .github/trigger_files/beam_PostCommit_SQL.json | 2 +- sdks/java/extensions/sql/iceberg/build.gradle | 1 + sdks/java/io/expansion-service/build.gradle | 10 ++++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_SQL.json b/.github/trigger_files/beam_PostCommit_SQL.json index 833fd9b0d174..5df3841d2363 100644 --- a/.github/trigger_files/beam_PostCommit_SQL.json +++ b/.github/trigger_files/beam_PostCommit_SQL.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run ", - "modification": 2 + "modification": 3 } diff --git a/sdks/java/extensions/sql/iceberg/build.gradle b/sdks/java/extensions/sql/iceberg/build.gradle index 1e319c97a8e2..893a485e7d86 100644 --- a/sdks/java/extensions/sql/iceberg/build.gradle +++ b/sdks/java/extensions/sql/iceberg/build.gradle @@ -48,6 +48,7 @@ dependencies { testImplementation library.java.google_api_services_bigquery testImplementation "org.apache.iceberg:iceberg-api:1.9.2" testImplementation "org.apache.iceberg:iceberg-core:1.9.2" + testRuntimeOnly library.java.bigdataoss_util_hadoop testImplementation project(":sdks:java:io:google-cloud-platform") testImplementation project(":sdks:java:extensions:google-cloud-platform-core") } diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 4e4c15f367cb..77a062c5b9af 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -83,12 +83,10 @@ dependencies { permitUnusedDeclared project(":sdks:java:extensions:kafka-factories") runtimeOnly project(":sdks:java:io:amazon-web-services2") // FileSystem may be used by Iceberg AddFiles - if (JavaVersion.current().compareTo(JavaVersion.VERSION_11) >= 0 && project.findProperty('testJavaVersion') != '8') { - // iceberg ended support for Java 8 in 1.7.0 - runtimeOnly project(":sdks:java:io:iceberg") - runtimeOnly project(":sdks:java:io:iceberg:hive") - runtimeOnly project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") - } + runtimeOnly project(":sdks:java:io:iceberg") + runtimeOnly project(":sdks:java:io:iceberg:hive") + runtimeOnly project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") + runtimeOnly library.java.bigdataoss_util_hadoop runtimeOnly library.java.kafka_clients runtimeOnly library.java.slf4j_jdk14 From 7f47ab60e06297e57f3616f046722e8c958b7099 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 14 May 2026 12:36:13 -0400 Subject: [PATCH 162/490] Isolate tests in multi_process_shared with unique temp path. (#38498) --- .../utils/multi_process_shared_test.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/sdks/python/apache_beam/utils/multi_process_shared_test.py b/sdks/python/apache_beam/utils/multi_process_shared_test.py index 18ed49c6fa17..c597e459e1a1 100644 --- a/sdks/python/apache_beam/utils/multi_process_shared_test.py +++ b/sdks/python/apache_beam/utils/multi_process_shared_test.py @@ -25,9 +25,29 @@ from typing import Any from unittest.mock import patch +import pytest + from apache_beam.utils import multi_process_shared +@pytest.fixture(autouse=True) +def isolate_multi_process_shared_tests(tmp_path, monkeypatch): + """Isolates MultiProcessShared tests by using a unique temp directory per test. + + This prevents tests running in parallel (e.g. with pytest-xdist) from + interfering with each other by writing to the same shared default temp directory. + """ + orig_init = multi_process_shared.MultiProcessShared.__init__ + + def mock_init(self, constructor, tag, *args, **kwargs): + if 'path' not in kwargs: + kwargs['path'] = str(tmp_path) + return orig_init(self, constructor, tag, *args, **kwargs) + + monkeypatch.setattr( + multi_process_shared.MultiProcessShared, '__init__', mock_init) + + class CallableCounter(object): def __init__(self, start=0): self.running = start @@ -285,23 +305,6 @@ def test_release_always_proxy(self): class MultiProcessSharedSpawnProcessTest(unittest.TestCase): - def setUp(self): - tempdir = tempfile.gettempdir() - for tag in ['basic', - 'main', - 'to_delete', - 'to_keep', - 'mix1', - 'mix2', - 'test_process_exit', - 'thundering_herd_test', - 'transient_test']: - for ext in ['', '.address', '.address.error']: - try: - os.remove(os.path.join(tempdir, tag + ext)) - except OSError: - pass - def tearDown(self): for p in multiprocessing.active_children(): if p.is_alive(): From 5a720273ed5d5803cded1facab924609945bbe72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 15:00:52 -0400 Subject: [PATCH 163/490] Bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in /sdks/typescript (#38475) Bumps [@protobufjs/utf8](https://github.com/dcodeIO/protobuf.js) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/dcodeIO/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/dcodeIO/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1) --- updated-dependencies: - dependency-name: "@protobufjs/utf8" dependency-version: 1.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/typescript/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index 98132f91db38..7f3a6bddd023 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -711,9 +711,9 @@ "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==" }, "node_modules/@tootallnate/once": { "version": "3.0.1", @@ -5192,9 +5192,9 @@ "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==" }, "@tootallnate/once": { "version": "3.0.1", From a71dc7914e100de07cbf07d5866411e550bcd78c Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu, 14 May 2026 15:13:28 -0400 Subject: [PATCH 164/490] [Iceberg] pin hadoop to 3.3.6 (#38500) * pin hadoop to 3.3.6 * pin in expansion service too --- .../trigger_files/IO_Iceberg_Integration_Tests.json | 2 +- .../IO_Iceberg_Integration_Tests_Dataflow.json | 2 +- sdks/java/io/expansion-service/build.gradle | 10 ++++++++++ sdks/java/io/iceberg/build.gradle | 11 ++++++++++- .../apache/beam/sdk/io/iceberg/IcebergScanConfig.java | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index b73af5e61a43..7ab7bcd9a9c6 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 2 } diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json b/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json index 5abe02fc09c7..3a009261f4f9 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 2 } diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 77a062c5b9af..4683b4fd6f50 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -57,6 +57,16 @@ configurations.runtimeClasspath { // Pin nimbus-jose-jwt to 9.37.4 to fix CVE-2025-53864 (transitive via hadoop-auth) resolutionStrategy.force 'com.nimbusds:nimbus-jose-jwt:9.37.4' + + // [iceberg] + // bigdataoss:gcs-connector and parquet:parquet-hadoop have conflicts with global hadoop-common:3.4.2 + // upgrading gcs-connector to 4.0.0 would be fine, because it uses hadoop-common 3.4.2 + // but parquet-hadoop is still at 3.3.0 + // so for now we need to pin hadoop to 3.3.6 until parquet-hadoop releases a version that uses hadoop 3.4.2+ + resolutionStrategy.force 'org.apache.hadoop:hadoop-common:3.3.6' + resolutionStrategy.force 'org.apache.hadoop:hadoop-client:3.3.6' + resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs:3.3.6' + resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs-client:3.3.6' } shadowJar { diff --git a/sdks/java/io/iceberg/build.gradle b/sdks/java/io/iceberg/build.gradle index bbd55fee2fc8..2dae0335f9c9 100644 --- a/sdks/java/io/iceberg/build.gradle +++ b/sdks/java/io/iceberg/build.gradle @@ -59,7 +59,7 @@ dependencies { implementation "org.apache.iceberg:iceberg-parquet:$iceberg_version" implementation "org.apache.iceberg:iceberg-orc:$iceberg_version" implementation "org.apache.iceberg:iceberg-data:$iceberg_version" - implementation library.java.hadoop_common + implementation "org.apache.hadoop:hadoop-common:3.3.6" // TODO(https://github.com/apache/beam/issues/21156): Determine how to build without this dependency provided "org.immutables:value:2.8.8" permitUnusedDeclared "org.immutables:value:2.8.8" @@ -70,6 +70,7 @@ dependencies { runtimeOnly "org.apache.iceberg:iceberg-azure:$iceberg_version" runtimeOnly "org.apache.iceberg:iceberg-azure-bundle:$iceberg_version" runtimeOnly library.java.bigdataoss_gcs_connector + runtimeOnly library.java.bigdataoss_util_hadoop runtimeOnly library.java.hadoop_client testImplementation project(":sdks:java:managed") @@ -117,6 +118,14 @@ dependencies { configurations.all { // iceberg-core needs avro:1.12.0 resolutionStrategy.force 'org.apache.avro:avro:1.12.0' + // bigdataoss:gcs-connector and parquet:parquet-hadoop have conflicts with global hadoop-common:3.4.2 + // upgrading gcs-connector to 4.0.0 would be fine, because it uses hadoop-common 3.4.2 + // but parquet-hadoop is still at 3.3.0 + // so for now we need to pin hadoop to 3.3.6 until parquet-hadoop releases a version that uses hadoop 3.4.2+ + resolutionStrategy.force 'org.apache.hadoop:hadoop-common:3.3.6' + resolutionStrategy.force 'org.apache.hadoop:hadoop-client:3.3.6' + resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs:3.3.6' + resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs-client:3.3.6' } hadoopVersions.each {kv -> diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java index 56f21d2bc8b5..45ecc7cf71c3 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java @@ -19,7 +19,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.util.Sets.newHashSet; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets.newHashSet; import com.google.auto.value.AutoValue; import java.io.Serializable; From 17ae9eb10f8efb650b2ba8307e08477049ff99a0 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 14 May 2026 22:53:07 -0400 Subject: [PATCH 165/490] Set torch upper bound --- sdks/python/setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdks/python/setup.py b/sdks/python/setup.py index 45781a44c4b1..4c1384c31517 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -198,7 +198,8 @@ def cythonize(*args, **kwargs): # Drop this cap once transformers updates the CLIP call site to # `cls_token=` or tokenizers reinstates `cls=` as a deprecation alias. 'tokenizers<0.23', - 'torch', + # Avoid torch 2.12.0+ which fails to run unit tests with segfault. + 'torch<2.12.0', # Match tested transformers range. 'transformers>=4.28.0,<4.56.0', # Keep tokenizers compatible with this transformers range. From f1174b3b8f39800a8cbd7a55aa91b3edada2986e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 08:41:54 -0400 Subject: [PATCH 166/490] Bump google.golang.org/grpc from 1.81.0 to 1.81.1 in /sdks (#38506) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.0 to 1.81.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.81.0...v1.81.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.81.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 028923c21dd8..c72969b14bbd 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -62,7 +62,7 @@ require ( golang.org/x/text v0.37.0 google.golang.org/api v0.279.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 - google.golang.org/grpc v1.81.0 + google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/sdks/go.sum b/sdks/go.sum index fbe69acf027a..4290813b294a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1472,8 +1472,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From 462cec7e728a1ace964410aafe39f984fa2a10a5 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 15 May 2026 09:44:17 -0400 Subject: [PATCH 167/490] Enable multi-release attribute for Spark job server jar (#38449) --- runners/spark/job-server/spark_job_server.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runners/spark/job-server/spark_job_server.gradle b/runners/spark/job-server/spark_job_server.gradle index a35bbea377d5..500a8ba865b1 100644 --- a/runners/spark/job-server/spark_job_server.gradle +++ b/runners/spark/job-server/spark_job_server.gradle @@ -307,5 +307,7 @@ createCrossLanguageValidatesRunnerTask( ) shadowJar { - outputs.upToDateWhen { false } + manifest { + attributes(["Multi-Release": true]) + } } From 8efc2b8c9b8f6356977b118cdfa1624eb48a63c2 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 15 May 2026 09:58:15 -0400 Subject: [PATCH 168/490] Fix empty stream name encountered in StorageApiFinalizeWrtiesDoFn (#38410) * Potential fix of empty stream name encountered in StorageApiFinalizeWrtiesDoFn * Address comments * Address AI comments * comments --- .../StorageApiWritesShardedRecords.java | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java index cbace6e7ff40..b644d7aa752c 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java @@ -44,7 +44,6 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -559,8 +558,9 @@ private void handleAppendFailure( String shortTableId, AppendClientInfo appendClientInfo, Callable tryCreateTable, - BiConsumer>, Boolean> initializeContexts, - Consumer>> clearClients, + Consumer>> initializeContexts, + Runnable resetClient, + ValueState streamName, ValueState streamOffset, MultiOutputReceiver o) { // The first context is always the one that fails. @@ -659,15 +659,6 @@ private void handleAppendFailure( throw new RuntimeException(e); } - if (!quotaError) { - // For known errors (offset mismatch, not found) we must reestablish - // the streams. - // However we've seen that doing this fixes random stuckness issues by reestablishing - // gRPC connections, - // so we close the clients for all non-quota errors. - - clearClients.accept(failedContexts); - } appendFailures.inc(); int retriedRows = failedContext.protoRows.getSerializedRowsCount(); BigQuerySinkMetrics.appendRowsRowStatusCounter( @@ -722,11 +713,20 @@ private void handleAppendFailure( // Finalize the stream and clear streamName so a new stream will be created. o.get(flushTag) .output(KV.of(failedContext.streamName, new Operation(failedContext.offset - 1, true))); + + // Clear streamName so a new stream will be created in resetClient below. + streamName.write(""); + + // Re-establish the client with the new stream. + resetClient.run(); + // Reinitialize all contexts with the new stream and new offsets. - initializeContexts.accept(failedContexts, true); + initializeContexts.accept(failedContexts); // Offset failures imply that all subsequent parallel appends will also fail. // Retry them all. + } else if (!quotaError) { + resetClient.run(); } } @@ -912,13 +912,9 @@ public void process( // Initialize stream names and offsets for all contexts. This will be called initially, but // will also be called if we roll over to a new stream on a retry. - BiConsumer>, Boolean> initializeContexts = - (contexts, isFailure) -> { + Consumer>> initializeContexts = + (contexts) -> { try { - if (isFailure) { - // Clear the stream name, forcing a new one to be created. - streamName.write(""); - } String streamNameRead = Preconditions.checkArgumentNotNull(streamName.read()); long currentOffset = Preconditions.checkArgumentNotNull(streamOffset.read()); for (AppendRowsContext context : contexts) { @@ -933,8 +929,8 @@ public void process( } }; - Consumer>> clearClients = - (contexts) -> { + Runnable resetClient = + () -> { try { appendClientHolder.invalidateAndReset(); } catch (Exception e) { @@ -967,7 +963,8 @@ public void process( appendClientHolder.get(), tryCreateTable, initializeContexts, - clearClients, + resetClient, + streamName, streamOffset, o); return RetryType.RETRY_ALL_OPERATIONS; @@ -1068,7 +1065,7 @@ public void process( Iterable> contexts = retryManager.getRemainingContexts(); if (numAppends > 0) { - initializeContexts.accept(contexts, false); + initializeContexts.accept(contexts); retryManager.run(true); appendSplitDistribution.update(numAppends); From 601e464c5ce87c3d385646fc501cbb04ad83c1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Fri, 15 May 2026 22:13:50 +0200 Subject: [PATCH 169/490] Bump jackson_version - Fix GHSA-72hv-8253-57qq (#37969) * Bump jackson_version - Fix GHSA-72hv-8253-57qq jackson-core: Number Length Constraint Bypass in Async Parser Leads to Potential DoS Condition --- .../main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 2 +- sdks/java/container/license_scripts/dep_urls_java.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 4edcb0b84d6c..92e9c351945a 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -623,7 +623,7 @@ class BeamModulePlugin implements Plugin { def httpclient_version = "4.5.13" def httpcore_version = "4.4.14" def iceberg_bqms_catalog_version = "1.6.1-1.0.1" - def jackson_version = "2.15.4" + def jackson_version = "2.18.6" def jaxb_api_version = "2.3.3" def jsr305_version = "3.0.2" def everit_json_version = "1.14.2" diff --git a/sdks/java/container/license_scripts/dep_urls_java.yaml b/sdks/java/container/license_scripts/dep_urls_java.yaml index 725e70f227b7..29deb7165040 100644 --- a/sdks/java/container/license_scripts/dep_urls_java.yaml +++ b/sdks/java/container/license_scripts/dep_urls_java.yaml @@ -58,7 +58,7 @@ xz: '1.5': # The original repo is down. This license is taken from https://tukaani.org/xz/java.html. license: "file://{}/xz/COPYING" jackson-bom: - '2.15.4': + '2.18.6': license: "https://raw.githubusercontent.com/FasterXML/jackson-bom/master/LICENSE" type: "Apache License 2.0" org.eclipse.jgit: From 12bbcab84517d494b9407495d3081ab548ca8811 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 15 May 2026 22:59:33 +0200 Subject: [PATCH 170/490] Fix Dataflow archetype missing API client dependency (#38509) --- sdks/java/maven-archetypes/examples/build.gradle | 1 + .../src/main/resources/archetype-resources/pom.xml | 7 +++++++ sdks/java/maven-archetypes/gcp-bom-examples/build.gradle | 1 + .../src/main/resources/archetype-resources/pom.xml | 7 +++++++ 4 files changed, 16 insertions(+) diff --git a/sdks/java/maven-archetypes/examples/build.gradle b/sdks/java/maven-archetypes/examples/build.gradle index 1edb55a10f95..af4fe78e6461 100644 --- a/sdks/java/maven-archetypes/examples/build.gradle +++ b/sdks/java/maven-archetypes/examples/build.gradle @@ -29,6 +29,7 @@ processResources { 'project.version': version, 'bigquery.version': dependencies.create(project.library.java.google_api_services_bigquery).getVersion(), 'google-api-client.version': dependencies.create(project.library.java.google_api_client).getVersion(), + 'google-api-services-dataflow.version': dependencies.create(project.library.java.google_api_services_dataflow).getVersion(), 'guava.version': dependencies.create(project.library.java.guava).getVersion(), 'hamcrest.version': dependencies.create(project.library.java.hamcrest).getVersion(), 'jackson.version': dependencies.create(project.library.java.jackson_core).getVersion(), diff --git a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml index e734e98e7d40..60e3648f0992 100644 --- a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml @@ -31,6 +31,7 @@ @bigquery.version@ @google-api-client.version@ + @google-api-services-dataflow.version@ @guava.version@ @hamcrest.version@ @jackson.version@ @@ -200,6 +201,12 @@ ${beam.version} runtime + + com.google.apis + google-api-services-dataflow + ${google-api-services-dataflow.version} + runtime + diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle b/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle index f9fabcfe19b0..2b5a9ace344d 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle +++ b/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle @@ -30,6 +30,7 @@ processResources { 'project.version': version, 'bigquery.version': dependencies.create(project.library.java.google_api_services_bigquery).getVersion(), 'google-api-client.version': dependencies.create(project.library.java.google_api_client).getVersion(), + 'google-api-services-dataflow.version': dependencies.create(project.library.java.google_api_services_dataflow).getVersion(), 'hamcrest.version': dependencies.create(project.library.java.hamcrest).getVersion(), 'jackson.version': dependencies.create(project.library.java.jackson_core).getVersion(), 'joda.version': dependencies.create(project.library.java.joda_time).getVersion(), diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml index d93a6b09284f..146e07bdfe99 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml @@ -31,6 +31,7 @@ @bigquery.version@ @google-api-client.version@ + @google-api-services-dataflow.version@ @hamcrest.version@ @jackson.version@ @joda.version@ @@ -195,6 +196,12 @@ beam-runners-google-cloud-dataflow-java runtime + + com.google.apis + google-api-services-dataflow + ${google-api-services-dataflow.version} + runtime + From 31453b83cea31414f46ce602dca464f63a843800 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 15 May 2026 17:00:35 -0400 Subject: [PATCH 171/490] Fix pom involving maven-archetype and opentelemetry (#38518) * Fix opentelemetry version * Fix target platform version to be 11 --- .../org/apache/beam/gradle/BeamModulePlugin.groovy | 10 +++++----- .../src/main/resources/beam/beam-codestyle.xml | 6 +++--- .../resources/META-INF/maven/archetype-metadata.xml | 2 +- .../test/resources/projects/basic/archetype.properties | 2 +- .../test/resources/projects/basic/archetype.properties | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 92e9c351945a..733f32c6b573 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -859,13 +859,13 @@ class BeamModulePlugin implements Plugin { netty_tcnative_boringssl_static : "io.netty:netty-tcnative-boringssl-static:2.0.52.Final", netty_transport : "io.netty:netty-transport:$netty_version", netty_transport_native_epoll : "io.netty:netty-transport-native-epoll:$netty_version", - opentelemetry_api : "io.opentelemetry:opentelemetry-api", // google_cloud_platform_libraries_bom sets version + opentelemetry_api : "io.opentelemetry:opentelemetry-api", // opentelemetry-bom sets version opentelemetry_bom : "io.opentelemetry:opentelemetry-bom-alpha:$opentelemetry_version-alpha", // alpha required by extensions - opentelemetry_context : "io.opentelemetry:opentelemetry-context", // google_cloud_platform_libraries_bom sets version + opentelemetry_context : "io.opentelemetry:opentelemetry-context:$opentelemetry_version", // Set version explicitly as it's standalone runtime dep for Beam modules opentelemetry_gcp_auth : "io.opentelemetry.contrib:opentelemetry-gcp-auth-extension:$opentelemetry_contrib_version-alpha", - opentelemetry_sdk : "io.opentelemetry:opentelemetry-sdk", // google_cloud_platform_libraries_bom sets version - opentelemetry_exporter_otlp : "io.opentelemetry:opentelemetry-exporter-otlp", // google_cloud_platform_libraries_bom sets version - opentelemetry_extension_autoconfigure : "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", // google_cloud_platform_libraries_bom sets version + opentelemetry_sdk : "io.opentelemetry:opentelemetry-sdk", // opentelemetry-bom sets version + opentelemetry_exporter_otlp : "io.opentelemetry:opentelemetry-exporter-otlp", // opentelemetry-bom sets version + opentelemetry_extension_autoconfigure : "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", // opentelemetry-bom sets version postgres : "org.postgresql:postgresql:$postgres_version", protobuf_java : "com.google.protobuf:protobuf-java:$protobuf_version", protobuf_java_util : "com.google.protobuf:protobuf-java-util:$protobuf_version", diff --git a/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml b/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml index f722a0e2e525..3f913778ab71 100644 --- a/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml +++ b/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml @@ -192,7 +192,7 @@ - + @@ -214,7 +214,7 @@ - + @@ -284,7 +284,7 @@ - + diff --git a/sdks/java/maven-archetypes/examples/src/main/resources/META-INF/maven/archetype-metadata.xml b/sdks/java/maven-archetypes/examples/src/main/resources/META-INF/maven/archetype-metadata.xml index 0c95fd85725b..532d84922ba0 100644 --- a/sdks/java/maven-archetypes/examples/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/sdks/java/maven-archetypes/examples/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -23,7 +23,7 @@ - 1.8 + 11 diff --git a/sdks/java/maven-archetypes/examples/src/test/resources/projects/basic/archetype.properties b/sdks/java/maven-archetypes/examples/src/test/resources/projects/basic/archetype.properties index ee81f8fbc5a0..5d442ba1a2c3 100644 --- a/sdks/java/maven-archetypes/examples/src/test/resources/projects/basic/archetype.properties +++ b/sdks/java/maven-archetypes/examples/src/test/resources/projects/basic/archetype.properties @@ -18,4 +18,4 @@ package=it.pkg version=0.1 groupId=archetype.it artifactId=basic -targetPlatform=1.8 +targetPlatform=11 diff --git a/sdks/java/maven-archetypes/starter/src/test/resources/projects/basic/archetype.properties b/sdks/java/maven-archetypes/starter/src/test/resources/projects/basic/archetype.properties index ee81f8fbc5a0..5d442ba1a2c3 100644 --- a/sdks/java/maven-archetypes/starter/src/test/resources/projects/basic/archetype.properties +++ b/sdks/java/maven-archetypes/starter/src/test/resources/projects/basic/archetype.properties @@ -18,4 +18,4 @@ package=it.pkg version=0.1 groupId=archetype.it artifactId=basic -targetPlatform=1.8 +targetPlatform=11 From df04e1e369c435084c03e73870ca79c17ebefc12 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 15 May 2026 17:02:41 -0400 Subject: [PATCH 172/490] Revert #37631 and #38497 on HEAD (#38516) * Revert "Adds a new coder translator for Java SchemaCoder. (#37631)" This reverts commit 81769cbfd132ed3c257685fbdc87f076b903e9f5. * Revert "Sickbay two failed tests due to new schema coder urn. (#38497)" This reverts commit 3dbd7c8c35b6a396c5e6c6fed2a3b37d4f731252. --- ...m_PostCommit_Java_ValidatesRunner_ULR.json | 3 +- CHANGES.md | 5 +- .../dataflow/DataflowPipelineTranslator.java | 2 +- .../beam/runners/dataflow/DataflowRunner.java | 59 ++++++-------- .../DataflowPipelineTranslatorTest.java | 77 ++++-------------- .../control/ProcessBundleDescriptorsTest.java | 6 +- runners/portability/java/build.gradle | 5 -- .../util/construction/CoderTranslation.java | 62 +-------------- .../util/construction/CoderTranslator.java | 1 - .../CoderTranslatorRegistrar.java | 16 ---- .../util/construction/CoderTranslators.java | 79 ------------------- .../construction/ModelCoderRegistrar.java | 28 +------ .../sdk/util/construction/ModelCoders.java | 2 - .../construction/RehydratedComponents.java | 3 +- .../sdk/util/construction/SdkComponents.java | 39 ++++----- .../construction/CoderTranslationTest.java | 38 +-------- .../expansion/service/ExpansionService.java | 2 +- .../avro/AvroGenericCoderRegistrar.java | 18 ----- .../fn/harness/state/StateBackedIterable.java | 22 ------ 19 files changed, 73 insertions(+), 394 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json index fbd81891f93b..6e2f429dd24e 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_ULR.json @@ -2,6 +2,5 @@ "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", "comment": "Modify this file in a trivial way to cause this test suite to run", "https://github.com/apache/beam/pull/31156": "noting that PR #31156 should run this test", - "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface", - "https://github.com/apache/beam/pull/38497": "sickbay two failed tests" + "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" } diff --git a/CHANGES.md b/CHANGES.md index ca911e52a7ad..52475a99d8e1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -85,9 +85,6 @@ ## Breaking Changes -* Portable Java SDK now encodes SchemaCoders in a portable way ([#34672](https://github.com/apache/beam/issues/34672)). - - Original custom Java coder encoding can still be obtained using [StreamingOptions.setUpdateCompatibilityVersion("2.73")](https://github.com/apache/beam/blob/2cf0930e7ae1aa389c26ce6639b584877a3e31d9/sdks/java/core/src/main/java/org/apache/beam/sdk/options/StreamingOptions.java#L47) ([#34672](https://github.com/apache/beam/issues/34672)). - - Fixes ([#36496](https://github.com/apache/beam/issues/36496)), ([#30276](https://github.com/apache/beam/issues/30276)), ([#29245](https://github.com/apache/beam/issues/29245)). * (Python) Made Beartype the default fallback type checking tool. This can be disabled with the `--disable_beartype` pipeline option. ([#38275](https://github.com/apache/beam/issues/38275)) * X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). @@ -2441,4 +2438,4 @@ Schema Options, it will be removed in version `2.23.0`. ([BEAM-9704](https://iss ## Highlights -- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). +- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). \ No newline at end of file diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index 1609cf6ea238..4016f31a5475 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -221,7 +221,7 @@ public Boolean visit(OrFinallyTrigger trigger) { private static byte[] serializeWindowingStrategy( WindowingStrategy windowingStrategy, PipelineOptions options) { try { - SdkComponents sdkComponents = SdkComponents.create(options); + SdkComponents sdkComponents = SdkComponents.create(); String workerHarnessContainerImageURL = DataflowRunner.getContainerImageForJob(options.as(DataflowPipelineOptions.class)); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index c19673a3117e..299e7fa21ed1 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1333,20 +1333,19 @@ public DataflowPipelineJob run(Pipeline pipeline) { // with the SDK harness image (which implements Fn API). // // The same Environment is used in different and contradictory ways, depending on whether - // it is a portable or non-portable job submission. + // it is a v1 or v2 job submission. RunnerApi.Environment defaultEnvironmentForDataflow = Environments.createDockerEnvironment(workerHarnessContainerImageURL); - // The SdkComponents for portable and non-portable job submission must be kept distinct. Both + // The SdkComponents for portable an non-portable job submission must be kept distinct. Both // need the default environment. - SdkComponents portableComponents = - SdkComponents.create( - options, - defaultEnvironmentForDataflow - .toBuilder() - .addAllDependencies(getDefaultArtifacts()) - .addAllCapabilities(Environments.getJavaCapabilities()) - .build()); + SdkComponents portableComponents = SdkComponents.create(); + portableComponents.registerEnvironment( + defaultEnvironmentForDataflow + .toBuilder() + .addAllDependencies(getDefaultArtifacts()) + .addAllCapabilities(Environments.getJavaCapabilities()) + .build()); RunnerApi.Pipeline portablePipelineProto = PipelineTranslation.toProto(pipeline, portableComponents, false); @@ -1375,30 +1374,28 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.as(SdkHarnessOptions.class).setPipelineProtoHash(pipelineProtoHash); if (useUnifiedWorker(options)) { - LOG.info( - "Skipping non-portable transform replacements since job will run on portable worker."); + LOG.info("Skipping v1 transform replacements since job will run on v2."); } else { - // Now rewrite things to be as needed for non-portable (mutates the pipeline). - // This way the job submitted is valid for portable and non-portable, simultaneously. + // Now rewrite things to be as needed for v1 (mutates the pipeline) + // This way the job submitted is valid for v1 and v2, simultaneously replaceV1Transforms(pipeline); } - // Capture the SdkComponents for look up during step translations. - SdkComponents dataflowNonPortableComponents = - SdkComponents.create( - options, - defaultEnvironmentForDataflow - .toBuilder() - .addAllDependencies(getDefaultArtifacts()) - .addAllCapabilities(Environments.getJavaCapabilities()) - .build()); - // No need to perform transform upgrading for the non-portable runner proto. - RunnerApi.Pipeline dataflowNonPortablePipelineProto = - PipelineTranslation.toProto(pipeline, dataflowNonPortableComponents, true, false); + // Capture the SdkComponents for look up during step translations + SdkComponents dataflowV1Components = SdkComponents.create(); + dataflowV1Components.registerEnvironment( + defaultEnvironmentForDataflow + .toBuilder() + .addAllDependencies(getDefaultArtifacts()) + .addAllCapabilities(Environments.getJavaCapabilities()) + .build()); + // No need to perform transform upgrading for the Runner v1 proto. + RunnerApi.Pipeline dataflowV1PipelineProto = + PipelineTranslation.toProto(pipeline, dataflowV1Components, true, false); if (LOG.isDebugEnabled()) { LOG.debug( - "Dataflow non-portable worker pipeline proto:\n{}", - TextFormat.printer().printToString(dataflowNonPortablePipelineProto)); + "Dataflow v1 pipeline proto:\n{}", + TextFormat.printer().printToString(dataflowV1PipelineProto)); } // Set a unique client_request_id in the CreateJob request. @@ -1418,11 +1415,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { JobSpecification jobSpecification = translator.translate( - pipeline, - dataflowNonPortablePipelineProto, - dataflowNonPortableComponents, - this, - packages); + pipeline, dataflowV1PipelineProto, dataflowV1Components, this, packages); if (!isNullOrEmpty(dataflowOptions.getDataflowWorkerJar()) && !useUnifiedWorker(options)) { List experiments = diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java index 953f7a638ede..f8818931a68f 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java @@ -47,7 +47,6 @@ import com.google.api.services.dataflow.model.Job; import com.google.api.services.dataflow.model.Step; import com.google.api.services.dataflow.model.WorkerPool; -import com.google.auto.value.AutoValue; import java.io.File; import java.io.IOException; import java.io.Serializable; @@ -93,8 +92,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.options.ValueProvider; -import org.apache.beam.sdk.schemas.AutoValueSchema; -import org.apache.beam.sdk.schemas.annotations.DefaultSchema; import org.apache.beam.sdk.state.StateSpec; import org.apache.beam.sdk.state.StateSpecs; import org.apache.beam.sdk.state.ValueState; @@ -169,11 +166,15 @@ public class DataflowPipelineTranslatorTest implements Serializable { @Rule public transient ExpectedException thrown = ExpectedException.none(); private SdkComponents createSdkComponents(PipelineOptions options) { + SdkComponents sdkComponents = SdkComponents.create(); + String containerImageURL = DataflowRunner.getContainerImageForJob(options.as(DataflowPipelineOptions.class)); RunnerApi.Environment defaultEnvironmentForDataflow = Environments.createDockerEnvironment(containerImageURL); - return SdkComponents.create(options, defaultEnvironmentForDataflow); + + sdkComponents.registerEnvironment(defaultEnvironmentForDataflow); + return sdkComponents; } // A Custom Mockito matcher for an initial Job that checks that all @@ -1293,16 +1294,15 @@ public String apply(byte[] input) { file1.deleteOnExit(); File file2 = File.createTempFile("file2-", ".txt"); file2.deleteOnExit(); - SdkComponents sdkComponents = - SdkComponents.create( - options, - Environments.createDockerEnvironment(DataflowRunner.getContainerImageForJob(options)) - .toBuilder() - .addAllDependencies( - Environments.getArtifacts( - ImmutableList.of("file1.txt=" + file1, "file2.txt=" + file2))) - .addAllCapabilities(Environments.getJavaCapabilities()) - .build()); + SdkComponents sdkComponents = SdkComponents.create(); + sdkComponents.registerEnvironment( + Environments.createDockerEnvironment(DataflowRunner.getContainerImageForJob(options)) + .toBuilder() + .addAllDependencies( + Environments.getArtifacts( + ImmutableList.of("file1.txt=" + file1, "file2.txt=" + file2))) + .addAllCapabilities(Environments.getJavaCapabilities()) + .build()); RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline, sdkComponents, true); @@ -1870,53 +1870,4 @@ public OffsetRange getInitialRange(@SuppressWarnings("unused") @Element String e return null; } } - - @AutoValue - @DefaultSchema(AutoValueSchema.class) - public abstract static class SimpleAutoValue { - public abstract String getString(); - - public abstract int getInt32(); - - public abstract long getInt64(); - - public static DataflowPipelineTranslatorTest.SimpleAutoValue of( - String string, int int32, long int64) { - return new AutoValue_DataflowPipelineTranslatorTest_SimpleAutoValue(string, int32, int64); - } - } - - @Test - public void testSchemaCoderTranslation() throws Exception { - DataflowPipelineOptions options = buildPipelineOptions(); - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply(Impulse.create()) - .apply( - MapElements.via( - new SimpleFunction() { - @Override - public SimpleAutoValue apply(byte[] input) { - return SimpleAutoValue.of("foo", 5, 10L); - } - })) - .apply(Window.into(FixedWindows.of(Duration.standardMinutes(1)))); - { - SdkComponents sdkComponents = createSdkComponents(options); - RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline, sdkComponents, true); - Map coders = pipelineProto.getComponents().getCodersMap(); - assertTrue(coders.containsKey("SchemaCoder")); - assertEquals("beam:coder:schema:v1", coders.get("SchemaCoder").getSpec().getUrn()); - } - - // Prior to version 2.74, SchemaCoders are translated as custom java coders. - { - options.as(StreamingOptions.class).setUpdateCompatibilityVersion("2.73"); - SdkComponents sdkComponents = createSdkComponents(options); - RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(pipeline, sdkComponents, true); - Map coders = pipelineProto.getComponents().getCodersMap(); - assertTrue(coders.containsKey("SchemaCoder")); - assertEquals("beam:coders:javasdk:0.1", coders.get("SchemaCoder").getSpec().getUrn()); - } - } } diff --git a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java index 9ea7404053d6..21d7550c38b9 100644 --- a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java +++ b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/ProcessBundleDescriptorsTest.java @@ -78,8 +78,7 @@ public void testLengthPrefixingOfKeyCoderInStatefulExecutableStage() throws Exce // Add another stateful stage with a non-standard key coder Pipeline p = Pipeline.create(); Coder keycoder = VoidCoder.of(); - ModelCoderRegistrar coderRegistrar = new ModelCoderRegistrar(); - assertThat(coderRegistrar.isKnownCoder(keycoder, p.getOptions()), is(false)); + assertThat(ModelCoderRegistrar.isKnownCoder(keycoder), is(false)); p.apply("impulse", Impulse.create()) .apply( "create", @@ -166,8 +165,7 @@ public void onTimer() {} public void testLengthPrefixingOfInputCoderExecutableStage() throws Exception { Pipeline p = Pipeline.create(); Coder voidCoder = VoidCoder.of(); - ModelCoderRegistrar coderRegistrar = new ModelCoderRegistrar(); - assertThat(coderRegistrar.isKnownCoder(voidCoder, p.getOptions()), is(false)); + assertThat(ModelCoderRegistrar.isKnownCoder(voidCoder), is(false)); p.apply("impulse", Impulse.create()) .apply( ParDo.of( diff --git a/runners/portability/java/build.gradle b/runners/portability/java/build.gradle index aa147e8426c4..6e3b431e802b 100644 --- a/runners/portability/java/build.gradle +++ b/runners/portability/java/build.gradle @@ -214,11 +214,6 @@ def createUlrValidatesRunnerTask = { name, environmentType, dockerImageTask = "" // TODO(https://github.com/apache/beam/issues/31231) excludeTestsMatching 'org.apache.beam.sdk.transforms.RedistributeTest.testRedistributePreservesMetadata' - // TODO(https://github.com/apache/beam/issues/33859): Failed with "KeyError: 'beam:coder:schema:v1'". - // New schema coder urn is not yet supported in runners other than dataflow - excludeTestsMatching 'org.apache.beam.sdk.transforms.PerKeyOrderingTest.testMultipleStatefulOrderingWithShuffle' - excludeTestsMatching 'org.apache.beam.sdk.transforms.PerKeyOrderingTest.testMultipleStatefulOrderingWithoutShuffle' - for (String test : sickbayTests) { excludeTestsMatching test } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java index 2cc4bf0c6a03..22859dc68b93 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslation.java @@ -25,13 +25,11 @@ import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.model.pipeline.v1.RunnerApi.FunctionSpec; import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.BiMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableBiMap; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.dataflow.qual.Deterministic; @@ -64,8 +62,6 @@ private static class DefaultTranslationContext implements TranslationContext {} private static @MonotonicNonNull BiMap, String> knownCoderUrns; - private static @MonotonicNonNull List coderTranslatorRegistrars; - private static @MonotonicNonNull Map, CoderTranslator> knownTranslators; @@ -84,53 +80,6 @@ static BiMap, String> getKnownCoderUrns() { return knownCoderUrns; } - private static void initializeCoderTranslatorRegistrars() { - ImmutableList.Builder registrars = ImmutableList.builder(); - for (CoderTranslatorRegistrar coderTranslatorRegistrar : - ServiceLoader.load(CoderTranslatorRegistrar.class)) { - registrars.add(coderTranslatorRegistrar); - } - coderTranslatorRegistrars = registrars.build(); - } - - static boolean isKnownCoder(Coder coder, PipelineOptions options) { - if (coderTranslatorRegistrars == null) { - initializeCoderTranslatorRegistrars(); - } - for (CoderTranslatorRegistrar registrar : coderTranslatorRegistrars) { - if (registrar.isKnownCoder(coder, options)) { - return true; - } - } - return false; - } - - static CoderTranslator getCoderTranslator(Class coderClass) { - if (coderTranslatorRegistrars == null) { - initializeCoderTranslatorRegistrars(); - } - for (CoderTranslatorRegistrar registrar : coderTranslatorRegistrars) { - CoderTranslator translator = registrar.getCoderTranslator(coderClass); - if (translator != null) { - return translator; - } - } - return null; - } - - static Class getCoderForUrn(String coderUrn) { - if (coderTranslatorRegistrars == null) { - initializeCoderTranslatorRegistrars(); - } - for (CoderTranslatorRegistrar registrar : coderTranslatorRegistrars) { - Class coder = registrar.getCoderForUrn(coderUrn); - if (coder != null) { - return coder; - } - } - return null; - } - @VisibleForTesting @Deterministic static Map, CoderTranslator> getKnownTranslators() { @@ -158,7 +107,7 @@ public static RunnerApi.MessageWithComponents toProto(Coder coder) throws IOE public static RunnerApi.Coder toProto(Coder coder, SdkComponents components) throws IOException { - if (isKnownCoder(coder, components.getPipelineOptions())) { + if (getKnownCoderUrns().containsKey(coder.getClass())) { return toKnownCoder(coder, components); } @@ -180,10 +129,7 @@ private static RunnerApi.Coder toUnknownCoderWrapper(UnknownCoderWrapper coder) private static RunnerApi.Coder toKnownCoder(Coder coder, SdkComponents components) throws IOException { - CoderTranslator translator = getCoderTranslator(coder.getClass()); - if (translator == null) { - throw new IOException("Unable to find CoderTranslator for known Coder"); - } + CoderTranslator translator = getKnownTranslators().get(coder.getClass()); List componentIds = registerComponents(coder, translator, components); return RunnerApi.Coder.newBuilder() .addAllComponentCoderIds(componentIds) @@ -240,8 +186,8 @@ private static Coder fromKnownCoder( components.getComponents().getCodersOrThrow(componentId), components, context); coderComponents.add(innerCoder); } - Class coderType = getCoderForUrn(coderUrn); - CoderTranslator translator = getCoderTranslator(coderType); + Class coderType = getKnownCoderUrns().inverse().get(coderUrn); + CoderTranslator translator = getKnownTranslators().get(coderType); if (translator != null) { return translator.fromComponents( coderComponents, coder.getSpec().getPayload().toByteArray(), context); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java index 78f5b61c0f0e..3d89c4c7ff4a 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslator.java @@ -28,7 +28,6 @@ * additional payload, which is not currently supported. This exists as a temporary measure. */ public interface CoderTranslator> { - /** Extract all component {@link Coder coders} within a coder. */ List> getComponents(T from); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java index 44e8c2956aee..b69d0290de52 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslatorRegistrar.java @@ -19,8 +19,6 @@ import java.util.Map; import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.options.PipelineOptions; -import org.checkerframework.checker.nullness.qual.Nullable; /** A registrar of {@link Coder} URNs to the associated {@link CoderTranslator}. */ @SuppressWarnings({ @@ -36,18 +34,4 @@ public interface CoderTranslatorRegistrar { /** Returns a mapping of URN to {@link CoderTranslator}. */ Map, CoderTranslator> getCoderTranslators(); - - /** - * Returns whether the given Coder is known to this CoderTranslatorRegistrar. If the Coder is - * known, then getCoderTranslator() will return a non-null CoderTranslator. - */ - boolean isKnownCoder(Coder coder, PipelineOptions options); - - /** Returns the CoderTranslator to use for this Coder, or null if the Coder is not known. */ - @Nullable - CoderTranslator getCoderTranslator(Class coderClass); - - /** Returns the Coder to use for the given Urn, or null if the Urn is for an unknown Coder. */ - @Nullable - Class getCoderForUrn(String coderUrn); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java index a847bf780dff..84a90721a983 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/CoderTranslators.java @@ -19,7 +19,6 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import java.io.IOException; import java.util.Collections; import java.util.List; import org.apache.beam.model.pipeline.v1.SchemaApi; @@ -31,19 +30,12 @@ import org.apache.beam.sdk.coders.RowCoder; import org.apache.beam.sdk.coders.TimestampPrefixingWindowCoder; import org.apache.beam.sdk.schemas.Schema; -import org.apache.beam.sdk.schemas.SchemaCoder; import org.apache.beam.sdk.schemas.SchemaTranslation; -import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.InstanceBuilder; -import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.util.ShardedKey; -import org.apache.beam.sdk.util.construction.CoderTranslation.TranslationContext; -import org.apache.beam.sdk.values.Row; -import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.InvalidProtocolBufferException; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -185,77 +177,6 @@ public RowCoder fromComponents( }; } - static CoderTranslator> schema() { - return new CoderTranslator>() { - private static final String TO_ROW_FUNCTION_URN = "beam:torowfn:javasdk:v1"; - private static final String FROM_ROW_FUNCTION_URN = "beam:fromrowfn:javasdk:v1"; - private static final String TYPE_DESCRIPTOR_URN = "beam:typedescriptor:javasdk:v1"; - - @Override - public ImmutableList> getComponents(SchemaCoder from) { - return ImmutableList.of(); - } - - @Override - public byte[] getPayload(SchemaCoder from) { - SchemaApi.SchemaCoderPayload.Builder payload = SchemaApi.SchemaCoderPayload.newBuilder(); - payload.setSchema(SchemaTranslation.schemaToProto(from.getSchema(), true)); - payload - .getToRowFnBuilder() - .setUrn(TO_ROW_FUNCTION_URN) - .setPayload( - ByteString.copyFrom( - SerializableUtils.serializeToByteArray(from.getToRowFunction()))); - payload - .getFromRowFnBuilder() - .setUrn(FROM_ROW_FUNCTION_URN) - .setPayload( - ByteString.copyFrom( - SerializableUtils.serializeToByteArray(from.getFromRowFunction()))); - payload - .addAdditionalCoderInfosBuilder() - .setUrn(TYPE_DESCRIPTOR_URN) - .setPayload( - ByteString.copyFrom( - SerializableUtils.serializeToByteArray(from.getEncodedTypeDescriptor()))); - return payload.build().toByteArray(); - } - - @Override - public SchemaCoder fromComponents( - List> components, byte[] payload, TranslationContext context) { - checkArgument( - components.isEmpty(), "Expected empty component list, but received: %s", components); - try { - SchemaApi.SchemaCoderPayload schemaCoderPayload = - SchemaApi.SchemaCoderPayload.parseFrom(payload); - if (schemaCoderPayload.getAdditionalCoderInfosCount() == 0) { - throw new IllegalArgumentException("Missing serialized typeDescriptor"); - } - TypeDescriptor typeDescriptor = - (TypeDescriptor) - SerializableUtils.deserializeFromByteArray( - schemaCoderPayload.getAdditionalCoderInfos(0).getPayload().toByteArray(), - "typeDescriptor"); - SerializableFunction toRowFunction = - (SerializableFunction) - SerializableUtils.deserializeFromByteArray( - schemaCoderPayload.getToRowFn().getPayload().toByteArray(), "toRowFunction"); - SerializableFunction fromRowFunction = - (SerializableFunction) - SerializableUtils.deserializeFromByteArray( - schemaCoderPayload.getFromRowFn().getPayload().toByteArray(), - "fromRowFunction"); - - Schema schema = SchemaTranslation.schemaFromProto(schemaCoderPayload.getSchema()); - return SchemaCoder.of(schema, typeDescriptor, toRowFunction, fromRowFunction); - } catch (IOException | IllegalArgumentException e) { - throw new RuntimeException(e); - } - } - }; - } - static CoderTranslator> shardedKey() { return new SimpleStructuredCoderTranslator>() { @Override diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java index 1f9f1eaafbed..5b0d5aedd619 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoderRegistrar.java @@ -34,9 +34,6 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.coders.TimestampPrefixingWindowCoder; import org.apache.beam.sdk.coders.VarLongCoder; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.StreamingOptions; -import org.apache.beam.sdk.schemas.SchemaCoder; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.transforms.windowing.IntervalWindow.IntervalWindowCoder; import org.apache.beam.sdk.util.ShardedKey; @@ -74,7 +71,6 @@ public class ModelCoderRegistrar implements CoderTranslatorRegistrar { ModelCoders.PARAM_WINDOWED_VALUE_CODER_URN) .put(DoubleCoder.class, ModelCoders.DOUBLE_CODER_URN) .put(RowCoder.class, ModelCoders.ROW_CODER_URN) - .put(SchemaCoder.class, ModelCoders.SCHEMA_CODER_URN) .put(ShardedKey.Coder.class, ModelCoders.SHARDED_KEY_CODER_URN) .put(TimestampPrefixingWindowCoder.class, ModelCoders.CUSTOM_WINDOW_CODER_URN) .put(NullableCoder.class, ModelCoders.NULLABLE_CODER_URN) @@ -100,7 +96,6 @@ public class ModelCoderRegistrar implements CoderTranslatorRegistrar { CoderTranslators.paramWindowedValue()) .put(DoubleCoder.class, CoderTranslators.atomic(DoubleCoder.class)) .put(RowCoder.class, CoderTranslators.row()) - .put(SchemaCoder.class, CoderTranslators.schema()) .put(ShardedKey.Coder.class, CoderTranslators.shardedKey()) .put(TimestampPrefixingWindowCoder.class, CoderTranslators.timestampPrefixingWindow()) .put(NullableCoder.class, CoderTranslators.nullable()) @@ -128,6 +123,10 @@ public class ModelCoderRegistrar implements CoderTranslatorRegistrar { Coder.class.getSimpleName()); } + public static boolean isKnownCoder(Coder coder) { + return BEAM_MODEL_CODER_URNS.containsKey(coder.getClass()); + } + @Override public Map, String> getCoderURNs() { return BEAM_MODEL_CODER_URNS; @@ -137,23 +136,4 @@ public Map, String> getCoderURNs() { public Map, CoderTranslator> getCoderTranslators() { return BEAM_MODEL_CODERS; } - - @Override - public boolean isKnownCoder(Coder coder, PipelineOptions options) { - if (coder.getClass() == SchemaCoder.class - && StreamingOptions.updateCompatibilityVersionLessThan(options, "2.74")) { - return false; - } - return BEAM_MODEL_CODER_URNS.containsKey(coder.getClass()); - } - - @Override - public CoderTranslator getCoderTranslator(Class coderClass) { - return BEAM_MODEL_CODERS.getOrDefault(coderClass, null); - } - - @Override - public Class getCoderForUrn(String coderUrn) { - return BEAM_MODEL_CODER_URNS.inverse().getOrDefault(coderUrn, null); - } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java index 5059cc1c6b83..7b7546aceb61 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/ModelCoders.java @@ -61,7 +61,6 @@ private ModelCoders() {} getUrn(StandardCoders.Enum.PARAM_WINDOWED_VALUE); public static final String ROW_CODER_URN = getUrn(StandardCoders.Enum.ROW); - public static final String SCHEMA_CODER_URN = getUrn(StandardCoders.Enum.SCHEMA); public static final String STATE_BACKED_ITERABLE_CODER_URN = "beam:coder:state_backed_iterable:v1"; @@ -91,7 +90,6 @@ private ModelCoders() {} WINDOWED_VALUE_CODER_URN, DOUBLE_CODER_URN, ROW_CODER_URN, - SCHEMA_CODER_URN, PARAM_WINDOWED_VALUE_CODER_URN, STATE_BACKED_ITERABLE_CODER_URN, SHARDED_KEY_CODER_URN, diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java index 64c7898a37b9..f79696214368 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/RehydratedComponents.java @@ -189,7 +189,6 @@ public SdkComponents getSdkComponents(Collection requirements) { windowingStrategies.asMap(), coders.asMap(), Collections.emptyMap(), - requirements, - pipeline.getOptions()); + requirements); } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java index 6288649aba3d..446697f24a81 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SdkComponents.java @@ -63,7 +63,6 @@ public class SdkComponents { private final BiMap environmentIds = HashBiMap.create(); private final BiMap coderProtoToId = HashBiMap.create(); private final Set requirements; - private final PipelineOptions pipelineOptions; private final Set reservedIds = new HashSet<>(); @@ -72,7 +71,17 @@ public class SdkComponents { /** Create a new {@link SdkComponents} with no components. */ public static SdkComponents create() { - return new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, "", null); + return new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, ""); + } + + /** + * Create new {@link SdkComponents} importing all items from provided {@link Components} object. + * + *

    WARNING: This action might cause some of duplicate items created. + */ + public static SdkComponents create( + RunnerApi.Components components, Collection requirements) { + return new SdkComponents(components, requirements, ""); } /*package*/ static SdkComponents create( @@ -82,9 +91,8 @@ public static SdkComponents create() { Map> windowingStrategies, Map> coders, Map environments, - Collection requirements, - PipelineOptions pipelineOptions) { - SdkComponents sdkComponents = new SdkComponents(components, requirements, "", pipelineOptions); + Collection requirements) { + SdkComponents sdkComponents = SdkComponents.create(components, requirements); sdkComponents.transformIds.inverse().putAll(transforms); sdkComponents.pCollectionIds.inverse().putAll(pCollections); sdkComponents.windowingStrategyIds.inverse().putAll(windowingStrategies); @@ -95,28 +103,19 @@ public static SdkComponents create() { public static SdkComponents create(PipelineOptions options) { SdkComponents sdkComponents = - new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, "", options); + new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, ""); PortablePipelineOptions portablePipelineOptions = options.as(PortablePipelineOptions.class); sdkComponents.registerEnvironment( Environments.createOrGetDefaultEnvironment(portablePipelineOptions)); return sdkComponents; } - public static SdkComponents create(PipelineOptions options, Environment environment) { - SdkComponents sdkComponents = - new SdkComponents(RunnerApi.Components.getDefaultInstance(), null, "", options); - sdkComponents.registerEnvironment(environment); - return sdkComponents; - } - private SdkComponents( @Nullable Components components, @Nullable Collection requirements, - String newIdPrefix, - @Nullable PipelineOptions pipelineOptions) { + String newIdPrefix) { this.newIdPrefix = newIdPrefix; this.requirements = new HashSet<>(); - this.pipelineOptions = pipelineOptions; if (components == null) { if (requirements != null) { @@ -154,7 +153,7 @@ public void mergeFrom( */ public SdkComponents withNewIdPrefix(String newIdPrefix) { SdkComponents sdkComponents = - new SdkComponents(componentsBuilder.build(), requirements, newIdPrefix, pipelineOptions); + new SdkComponents(componentsBuilder.build(), requirements, newIdPrefix); sdkComponents.transformIds.putAll(transformIds); sdkComponents.pCollectionIds.putAll(pCollectionIds); sdkComponents.windowingStrategyIds.putAll(windowingStrategyIds); @@ -175,7 +174,7 @@ public String registerPTransform( throws IOException { String name = getApplicationName(appliedPTransform); // If this transform is present in the components, nothing to do. return the existing name. - // Otherwise, the transform must be translated and added to the components. + // Otherwise the transform must be translated and added to the components. if (componentsBuilder.getTransformsOrDefault(name, null) != null) { return name; } @@ -376,8 +375,4 @@ public RunnerApi.Components toComponents() { public Collection requirements() { return ImmutableSet.copyOf(requirements); } - - public PipelineOptions getPipelineOptions() { - return pipelineOptions; - } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java index 1ec0a74f5be1..b8f92ff0053e 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/construction/CoderTranslationTest.java @@ -22,7 +22,6 @@ import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; -import com.google.auto.value.AutoValue; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -46,20 +45,14 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.coders.TimestampPrefixingWindowCoder; import org.apache.beam.sdk.coders.VarLongCoder; -import org.apache.beam.sdk.schemas.AutoValueSchema; -import org.apache.beam.sdk.schemas.NoSuchSchemaException; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; -import org.apache.beam.sdk.schemas.SchemaCoder; -import org.apache.beam.sdk.schemas.SchemaRegistry; -import org.apache.beam.sdk.schemas.annotations.DefaultSchema; import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.transforms.windowing.IntervalWindow.IntervalWindowCoder; import org.apache.beam.sdk.util.ShardedKey; import org.apache.beam.sdk.util.construction.CoderTranslation.TranslationContext; -import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -77,34 +70,6 @@ "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) }) public class CoderTranslationTest { - @AutoValue - @DefaultSchema(AutoValueSchema.class) - public abstract static class SimpleAutoValue { - public abstract String getString(); - - public abstract int getInt32(); - - public abstract long getInt64(); - - public static SimpleAutoValue of(String string, Integer int32, Long int64) { - return new AutoValue_CoderTranslationTest_SimpleAutoValue(string, int32, int64); - } - } - - private static final SchemaRegistry REGISTRY = SchemaRegistry.createDefault(); - - private static SchemaCoder schemaCoderFrom(TypeDescriptor typeDescriptor) { - try { - return SchemaCoder.of( - REGISTRY.getSchema(typeDescriptor), - typeDescriptor, - REGISTRY.getToRowFunction(typeDescriptor), - REGISTRY.getFromRowFunction(typeDescriptor)); - } catch (NoSuchSchemaException e) { - throw new RuntimeException(e); - } - } - private static final Set> KNOWN_CODERS = ImmutableSet.>builder() .add(ByteArrayCoder.of()) @@ -129,7 +94,6 @@ private static SchemaCoder schemaCoderFrom(TypeDescriptor typeDescriptor) { Field.of("array", FieldType.array(FieldType.STRING)), Field.of("map", FieldType.map(FieldType.STRING, FieldType.INT32)), Field.of("bar", FieldType.logicalType(FixedBytes.of(123)))))) - .add(schemaCoderFrom(TypeDescriptor.of(SimpleAutoValue.class))) .add(ShardedKey.Coder.of(StringUtf8Coder.of())) .add(TimestampPrefixingWindowCoder.of(IntervalWindowCoder.of())) .add(NullableCoder.of(ByteArrayCoder.of())) @@ -163,7 +127,7 @@ public void validateKnownCoders() { } @Test - public void validateModelCoderTranslators() { + public void validateCoderTranslators() { assertThat( "Every Model Coder must have a Translator", new ModelCoderRegistrar().getCoderURNs().keySet(), diff --git a/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java b/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java index c93de2014798..6ecb029c5d97 100644 --- a/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java +++ b/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/ExpansionService.java @@ -600,7 +600,7 @@ private Map loadRegisteredTransforms() { pipeline.getOptions().as(ExperimentalOptions.class), "use_sdf_read"); } else { LOG.warn( - "Using use_deprecated_read in portable runners is runner-dependent. The " + "Using use_depreacted_read in portable runners is runner-dependent. The " + "ExpansionService will respect that, but if your runner does not have support for " + "native Read transform, your Pipeline will fail during Pipeline submission."); } diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java index 8bd18fd8e250..14ab48f66699 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/AvroGenericCoderRegistrar.java @@ -21,11 +21,9 @@ import java.util.Map; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.extensions.avro.coders.AvroGenericCoder; -import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.util.construction.CoderTranslator; import org.apache.beam.sdk.util.construction.CoderTranslatorRegistrar; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.checkerframework.checker.nullness.qual.Nullable; /** Coder registrar for AvroGenericCoder. */ @AutoService(CoderTranslatorRegistrar.class) @@ -44,20 +42,4 @@ public Map, String> getCoderURNs() { public Map, CoderTranslator> getCoderTranslators() { return ImmutableMap.of(AvroGenericCoder.class, new AvroGenericCoderTranslator()); } - - @Override - public boolean isKnownCoder(Coder coder, PipelineOptions options) { - return coder.getClass() == AvroGenericCoder.class; - } - - @Override - public @Nullable CoderTranslator getCoderTranslator( - Class coderClass) { - return coderClass == AvroGenericCoder.class ? new AvroGenericCoderTranslator() : null; - } - - @Override - public @Nullable Class getCoderForUrn(String coderUrn) { - return AVRO_CODER_URN.equals(coderUrn) ? AvroGenericCoder.class : null; - } } diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java index 42a6f8d11c2a..ef8d69bc1ec3 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/state/StateBackedIterable.java @@ -38,7 +38,6 @@ import org.apache.beam.sdk.coders.IterableLikeCoder; import org.apache.beam.sdk.fn.stream.PrefetchableIterable; import org.apache.beam.sdk.fn.stream.PrefetchableIterators; -import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.util.BufferedElementCountingOutputStream; import org.apache.beam.sdk.util.VarInt; import org.apache.beam.sdk.util.common.ElementByteSizeObservableIterable; @@ -53,7 +52,6 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.io.ByteStreams; -import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -302,26 +300,6 @@ public Map, String> getCoderUR getCoderTranslators() { return ImmutableMap.of(StateBackedIterable.Coder.class, new Translator()); } - - @Override - public boolean isKnownCoder( - org.apache.beam.sdk.coders.Coder coder, PipelineOptions options) { - return coder.getClass() == StateBackedIterable.Coder.class; - } - - @Override - public @Nullable CoderTranslator getCoderTranslator( - Class coderClass) { - return coderClass == StateBackedIterable.Coder.class ? new Translator() : null; - } - - @Override - public @Nullable Class getCoderForUrn( - String coderUrn) { - return STATE_BACKED_ITERABLE_CODER_URN.equals(coderUrn) - ? StateBackedIterable.Coder.class - : null; - } } /** From 9fe4c931794c189002c509b1476cc0b4039837db Mon Sep 17 00:00:00 2001 From: Arpit Jain <3242828+arpitjain099@users.noreply.github.com> Date: Sat, 16 May 2026 06:13:39 +0900 Subject: [PATCH 173/490] Fix typo in README website description (#38469) Signed-off-by: Arpit Jain --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db254d407ea9..2cf529c53eb8 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Here are some resources actively maintained by the Beam community to help you ge Apache Beam Website - Offical website with documentation, concepts, and guides for Apache Beam. + Official website with documentation, concepts, and guides for Apache Beam. Java Quickstart From 2bead3e090aed9d5b254f5d173199120f8912fc0 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 15 May 2026 17:59:53 -0400 Subject: [PATCH 174/490] Revert "Fix Dataflow archetype missing API client dependency (#38509)" (#38519) This reverts commit 4ac7598df1436548d7a8411994c1b2f0dddcfdfa. --- sdks/java/maven-archetypes/examples/build.gradle | 1 - .../src/main/resources/archetype-resources/pom.xml | 7 ------- sdks/java/maven-archetypes/gcp-bom-examples/build.gradle | 1 - .../src/main/resources/archetype-resources/pom.xml | 7 ------- 4 files changed, 16 deletions(-) diff --git a/sdks/java/maven-archetypes/examples/build.gradle b/sdks/java/maven-archetypes/examples/build.gradle index af4fe78e6461..1edb55a10f95 100644 --- a/sdks/java/maven-archetypes/examples/build.gradle +++ b/sdks/java/maven-archetypes/examples/build.gradle @@ -29,7 +29,6 @@ processResources { 'project.version': version, 'bigquery.version': dependencies.create(project.library.java.google_api_services_bigquery).getVersion(), 'google-api-client.version': dependencies.create(project.library.java.google_api_client).getVersion(), - 'google-api-services-dataflow.version': dependencies.create(project.library.java.google_api_services_dataflow).getVersion(), 'guava.version': dependencies.create(project.library.java.guava).getVersion(), 'hamcrest.version': dependencies.create(project.library.java.hamcrest).getVersion(), 'jackson.version': dependencies.create(project.library.java.jackson_core).getVersion(), diff --git a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml index 60e3648f0992..e734e98e7d40 100644 --- a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml @@ -31,7 +31,6 @@ @bigquery.version@ @google-api-client.version@ - @google-api-services-dataflow.version@ @guava.version@ @hamcrest.version@ @jackson.version@ @@ -201,12 +200,6 @@ ${beam.version} runtime - - com.google.apis - google-api-services-dataflow - ${google-api-services-dataflow.version} - runtime - diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle b/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle index 2b5a9ace344d..f9fabcfe19b0 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle +++ b/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle @@ -30,7 +30,6 @@ processResources { 'project.version': version, 'bigquery.version': dependencies.create(project.library.java.google_api_services_bigquery).getVersion(), 'google-api-client.version': dependencies.create(project.library.java.google_api_client).getVersion(), - 'google-api-services-dataflow.version': dependencies.create(project.library.java.google_api_services_dataflow).getVersion(), 'hamcrest.version': dependencies.create(project.library.java.hamcrest).getVersion(), 'jackson.version': dependencies.create(project.library.java.jackson_core).getVersion(), 'joda.version': dependencies.create(project.library.java.joda_time).getVersion(), diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml index 146e07bdfe99..d93a6b09284f 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml @@ -31,7 +31,6 @@ @bigquery.version@ @google-api-client.version@ - @google-api-services-dataflow.version@ @hamcrest.version@ @jackson.version@ @joda.version@ @@ -196,12 +195,6 @@ beam-runners-google-cloud-dataflow-java runtime - - com.google.apis - google-api-services-dataflow - ${google-api-services-dataflow.version} - runtime - From 128ce66280babf14cde6e8a5ea7affa0844cf52f Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Sun, 17 May 2026 07:41:18 -0400 Subject: [PATCH 175/490] Fix SubprocessServer cache thread-safety and test isolation (#38501) --- .../beam_PostCommit_Python_Versions.json | 4 ++++ sdks/python/apache_beam/transforms/external_test.py | 8 +++++++- sdks/python/apache_beam/utils/subprocess_server.py | 11 ++++++----- 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .github/trigger_files/beam_PostCommit_Python_Versions.json diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json new file mode 100644 index 000000000000..a975cd1cd104 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "revision": 1 +} diff --git a/sdks/python/apache_beam/transforms/external_test.py b/sdks/python/apache_beam/transforms/external_test.py index 137c92861ed7..0af89367fcc2 100644 --- a/sdks/python/apache_beam/transforms/external_test.py +++ b/sdks/python/apache_beam/transforms/external_test.py @@ -799,7 +799,13 @@ def test_implicit_builder_with_constructor_method(self): class JavaJarExpansionServiceTest(unittest.TestCase): def setUp(self): - SubprocessServer._cache._live_owners = set() + # Temporarily override _live_owners with an empty set for this test, + # preventing contamination of the process-wide global cache and avoiding + # side effects on other tests. + patcher = mock.patch.object( + SubprocessServer._cache, '_live_owners', new=set()) + patcher.start() + self.addCleanup(patcher.stop) def test_classpath(self): with tempfile.TemporaryDirectory() as temp_dir: diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index fed5ee591bcd..d21cb486b8f4 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -78,13 +78,14 @@ def __init__(self, constructor, destructor): self._counter = 0 def _next_id(self): - with self._lock: - self._counter += 1 - return self._counter + # Caller must hold self._lock. + self._counter += 1 + return self._counter def register(self): - owner = self._next_id() - self._live_owners.add(owner) + with self._lock: + owner = self._next_id() + self._live_owners.add(owner) return owner def purge(self, owner): From f3eec7b835dfe95397bfb396348f0de54c40ef4f Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Mon, 18 May 2026 01:52:33 -0700 Subject: [PATCH 176/490] [Dataflow Streaming] Add a job setting to limit value size in windmill state cache (#38458) * Add key, value size distribution to state cache stats * Add max value size limit to status page --- .../DataflowStreamingPipelineOptions.java | 10 ++ .../worker/StreamingDataflowWorker.java | 17 ++- .../worker/util/SimpleByteHistogram.java | 43 ++++++ .../windmill/state/WindmillStateCache.java | 122 +++++++++++++++--- .../worker/util/SimpleByteHistogramTest.java | 51 ++++++++ .../state/WindmillStateCacheTest.java | 62 +++++++++ .../windmill/src/main/proto/windmill.proto | 2 + 7 files changed, 285 insertions(+), 22 deletions(-) create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java index 9727048e47aa..90375ad445ae 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java @@ -249,6 +249,16 @@ public interface DataflowStreamingPipelineOptions extends PipelineOptions { void setIsWindmillServiceDirectPathEnabled(boolean isWindmillServiceDirectPathEnabled); + /** + * The maximum size of cached entries in bytes. Entries (eg: values, bags) larger than this limit + * will not be cached by the windmill state cache + */ + @Description("The maximum size of cached entries in bytes.") + @Default.Long(Long.MAX_VALUE) + Long getMaxWindmillStateCacheEntryBytes(); + + void setMaxWindmillStateCacheEntryBytes(Long value); + /** * Factory for creating local Windmill address. Reads from system propery 'windmill.hostport' for * backwards compatibility. diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java index f5e5adab1556..4d070da995b3 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java @@ -50,6 +50,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.WeightedSemaphore; import org.apache.beam.runners.dataflow.worker.streaming.WorkHeartbeatResponseProcessor; import org.apache.beam.runners.dataflow.worker.streaming.config.ComputationConfig; +import org.apache.beam.runners.dataflow.worker.streaming.config.ComputationConfig.Fetcher; import org.apache.beam.runners.dataflow.worker.streaming.config.FixedGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingApplianceComputationConfigFetcher; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingEngineComputationConfigFetcher; @@ -113,6 +114,7 @@ import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.io.gcp.bigquery.BigQuerySinkMetrics; import org.apache.beam.sdk.metrics.MetricsEnvironment; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.util.construction.CoderTranslation; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.auth.MoreCallCredentials; @@ -633,6 +635,10 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o WindmillStateCache.builder() .setSizeMb(options.getWorkerCacheMb()) .setSupportMapViaMultimap(options.isEnableStreamingEngine()) + .setMaxCachedEntryBytes(options.getMaxWindmillStateCacheEntryBytes()) + .setEnableHistogram( + !ExperimentalOptions.hasExperiment( + options, "disable_windmill_user_state_cache_histogram")) .build(); GrpcWindmillStreamFactory.Builder windmillStreamFactoryBuilder = @@ -651,6 +657,15 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o windmillStateCache::forComputation, ID_GENERATOR)); + Fetcher configFetcher = configFetcherComputationStateCacheAndWindmillClient.configFetcher(); + configFetcher + .getGlobalConfigHandle() + .registerConfigObserver( + config -> { + windmillStateCache.setMaxCachedEntryBytesOverride( + config.userWorkerJobSettings().getMaxCachedEntryBytes()); + }); + ComputationStateCache computationStateCache = configFetcherComputationStateCacheAndWindmillClient.computationStateCache(); WindmillServerStub windmillServer = @@ -689,7 +704,7 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o return new StreamingDataflowWorker( windmillServer, clientId, - configFetcherComputationStateCacheAndWindmillClient.configFetcher(), + configFetcher, computationStateCache, windmillStateCache, workExecutor, diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java new file mode 100644 index 000000000000..6b90ca8df9ef --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +/** A simple histogram to track byte sizes. */ +public class SimpleByteHistogram { + private final long[] buckets = new long[7]; + + public void add(long weight) { + buckets[getBucket(weight)]++; + } + + private int getBucket(long weight) { + if (weight < 128) return 0; + if (weight < 256) return 1; + if (weight < 512) return 2; + if (weight < 1024) return 3; + if (weight < 10 * 1024) return 4; + if (weight < 1024 * 1024) return 5; + return 6; + } + + public String format() { + return String.format( + "[<128B:%d, <256B:%d, <512B:%d, <1KB:%d, <10KB:%d, <1MB:%d, >=1MB:%d]", + buckets[0], buckets[1], buckets[2], buckets[3], buckets[4], buckets[5], buckets[6]); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java index 07c9599c866a..7515db000852 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java @@ -32,6 +32,7 @@ import org.apache.beam.runners.dataflow.worker.status.BaseStatusServlet; import org.apache.beam.runners.dataflow.worker.status.StatusDataProvider; import org.apache.beam.runners.dataflow.worker.streaming.ShardedKey; +import org.apache.beam.runners.dataflow.worker.util.SimpleByteHistogram; import org.apache.beam.runners.dataflow.worker.util.common.worker.InternedByteString; import org.apache.beam.sdk.state.State; import org.apache.beam.sdk.util.Weighted; @@ -75,9 +76,18 @@ public class WindmillStateCache implements StatusDataProvider { private final ConcurrentMap keyIndex; private final long workerCacheBytes; // Copy workerCacheMb and convert to bytes. private final boolean supportMapViaMultimap; - - WindmillStateCache(long sizeMb, boolean supportMapViaMultimap) { + private final long defaultMaxCachedEntryBytes; + private final boolean enableHistogram; + private volatile long maxCachedEntryBytesOverride = -1L; + + WindmillStateCache( + long sizeMb, + boolean supportMapViaMultimap, + long maxCachedEntryBytes, + boolean enableHistogram) { this.workerCacheBytes = sizeMb * MEGABYTES; + this.defaultMaxCachedEntryBytes = maxCachedEntryBytes; + this.enableHistogram = enableHistogram; int stateCacheConcurrencyLevel = Math.max(STATE_CACHE_CONCURRENCY_LEVEL, Runtime.getRuntime().availableProcessors()); this.stateCache = @@ -99,11 +109,27 @@ public interface Builder { Builder setSupportMapViaMultimap(boolean supportMapViaMultimap); + Builder setMaxCachedEntryBytes(long maxCachedEntryBytes); + + Builder setEnableHistogram(boolean enableHistogram); + WindmillStateCache build(); } public static Builder builder() { - return new AutoBuilder_WindmillStateCache_Builder().setSupportMapViaMultimap(false); + return new AutoBuilder_WindmillStateCache_Builder() + .setSupportMapViaMultimap(false) + .setMaxCachedEntryBytes(Long.MAX_VALUE) + .setEnableHistogram(true); + } + + public void setMaxCachedEntryBytesOverride(long limit) { + this.maxCachedEntryBytesOverride = limit; + } + + private long getMaxCachedEntryBytesLimit() { + long override = maxCachedEntryBytesOverride; + return override >= 0 ? override : defaultMaxCachedEntryBytes; } private EntryStats calculateEntryStats() { @@ -111,10 +137,20 @@ private EntryStats calculateEntryStats() { BiConsumer consumer = (stateId, stateCacheEntry) -> { stats.entries++; - stats.idWeight += stateId.getWeight(); - stats.entryWeight += stateCacheEntry.getWeight(); + long idWeight = stateId.getWeight(); + stats.idWeight += idWeight; + long entryWeight = stateCacheEntry.getWeight(); + stats.entryWeight += entryWeight; stats.entryValues += stateCacheEntry.values.size(); stats.maxEntryValues = Math.max(stats.maxEntryValues, stateCacheEntry.values.size()); + if (enableHistogram) { + stats.addKeyWeight(idWeight); + stats.addEntryWeight(entryWeight); + stateCacheEntry.values.forEach( + (encodedAddress, weightedValue) -> { + stats.addValueWeight(weightedValue.weight); + }); + } }; stateCache.asMap().forEach(consumer); return stats; @@ -142,23 +178,44 @@ public ForComputation forComputation(String computation) { @Override public void appendSummaryHtml(PrintWriter response) { response.println("Cache Stats:
    "); - response.println( - "" - + "" - + "" - + ""); CacheStats cacheStats = stateCache.stats(); EntryStats entryStats = calculateEntryStats(); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println("
    Hit RatioEvictionsEntriesEntry ValuesMax Entry ValuesId WeightEntry WeightMax WeightKeys
    " + cacheStats.hitRate() + "" + cacheStats.evictionCount() + "" + entryStats.entries + "(" + stateCache.size() + " inc. weak) " + entryStats.entryValues + "" + entryStats.maxEntryValues + "" + entryStats.idWeight / MEGABYTES + "MB" + entryStats.entryWeight / MEGABYTES + "MB" + getMaxWeight() / MEGABYTES + "MB" + keyIndex.size() + "

    "); + + response.println("Hit Ratio" + cacheStats.hitRate() + ""); + response.println("Evictions" + cacheStats.evictionCount() + ""); + response.println( + "Entries" + + entryStats.entries + + " (" + + stateCache.size() + + " inc. weak)"); + response.println("Entry Values" + entryStats.entryValues + ""); + response.println( + "Max Entry Values" + entryStats.maxEntryValues + ""); + response.println( + "Id Weight" + entryStats.idWeight / MEGABYTES + "MB"); + response.println( + "Entry Weight" + entryStats.entryWeight / MEGABYTES + "MB"); + response.println("Max Weight" + getMaxWeight() / MEGABYTES + "MB"); + response.println("Keys" + keyIndex.size() + ""); + response.println( + "Entry Size Limit" + getMaxCachedEntryBytesLimit() + " bytes"); + if (enableHistogram) { + response.println( + "Entry Weight Dist" + + entryStats.entryWeightHistogram.format() + + ""); + response.println( + "Value Weight Dist" + + entryStats.valueWeightHistogram.format() + + ""); + response.println( + "Key Weight Dist" + + entryStats.keyWeightHistogram.format() + + ""); + } + + response.println("
    "); } public BaseStatusServlet statusServlet() { @@ -180,6 +237,21 @@ private static class EntryStats { long entryWeight; long entryValues; long maxEntryValues; + SimpleByteHistogram entryWeightHistogram = new SimpleByteHistogram(); + SimpleByteHistogram valueWeightHistogram = new SimpleByteHistogram(); + SimpleByteHistogram keyWeightHistogram = new SimpleByteHistogram(); + + void addEntryWeight(long weight) { + entryWeightHistogram.add(weight); + } + + void addValueWeight(long weight) { + valueWeightHistogram.add(weight); + } + + void addKeyWeight(long weight) { + keyWeightHistogram.add(weight); + } } /** @@ -413,7 +485,15 @@ public void put( } public void persist() { - localCache.forEach(stateCache::put); + long limit = WindmillStateCache.this.getMaxCachedEntryBytesLimit(); + localCache.forEach( + (id, entry) -> { + if (entry.getWeight() <= limit) { + stateCache.put(id, entry); + } else { + stateCache.invalidate(id); + } + }); } } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java new file mode 100644 index 000000000000..252300a19550 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link SimpleByteHistogram}. */ +@RunWith(JUnit4.class) +public class SimpleByteHistogramTest { + + @Test + public void testHistogram() { + SimpleByteHistogram histogram = new SimpleByteHistogram(); + histogram.add(10); // <128B + histogram.add(127); // <128B + histogram.add(128); // <256B + histogram.add(255); // <256B + histogram.add(256); // <512B + histogram.add(511); // <512B + histogram.add(512); // <1KB + histogram.add(1023); // <1KB + histogram.add(1024); // <10KB + histogram.add(10240 - 1); // <10KB + histogram.add(10240); // <1MB + histogram.add(1048576 - 1); // <1MB + histogram.add(1048576); // >=1MB + histogram.add(2000000); // >=1MB + + String expected = "[<128B:2, <256B:2, <512B:2, <1KB:2, <10KB:2, <1MB:2, >=1MB:2]"; + assertEquals(expected, histogram.format()); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java index bbb8e4c93c07..2d3d9b5ccff2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java @@ -270,6 +270,68 @@ public void testMaxWeight() throws Exception { assertEquals(400 * MEGABYTES, cache.getMaxWeight()); } + @Test + public void testMaxCachedEntryBytes() throws Exception { + cache.setMaxCachedEntryBytesOverride( + 100); // Set limit to 100 bytes, per cache entry overhead is 136. + + WindmillStateCache.ForKeyAndFamily keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 1L).forFamily(STATE_FAMILY); + + TestStateTag tag1 = new TestStateTag("tag1"); + TestStateTag tag2 = new TestStateTag("tag2"); + + putInCache(keyCache, StateNamespaces.global(), tag1, new TestState("g1"), 10); + keyCache.persist(); + + // It should not be in global cache because it's too large. + keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 2L).forFamily(STATE_FAMILY); + assertEquals(Optional.empty(), getFromCache(keyCache, StateNamespaces.global(), tag1)); + + // Now set limit larger. + cache.setMaxCachedEntryBytesOverride(1000); + + putInCache(keyCache, StateNamespaces.global(), tag2, new TestState("g2"), 10); + keyCache.persist(); + + // It should be in global cache. + keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 3L).forFamily(STATE_FAMILY); + assertEquals( + Optional.of(new TestState("g2")), getFromCache(keyCache, StateNamespaces.global(), tag2)); + + // Now update it to be larger than limit. + putInCache(keyCache, StateNamespaces.global(), tag2, new TestState("g2_large"), 2000); + keyCache.persist(); + + // It should be removed from global cache. + keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 4L).forFamily(STATE_FAMILY); + assertEquals(Optional.empty(), getFromCache(keyCache, StateNamespaces.global(), tag2)); + } + + @Test + public void testDisableHistogram() throws Exception { + WindmillStateCache noHistogramCache = + WindmillStateCache.builder().setSizeMb(400).setEnableHistogram(false).build(); + WindmillStateCache.ForKeyAndFamily keyCache = + noHistogramCache + .forComputation(COMPUTATION) + .forKey(COMPUTATION_KEY, 0L, 1L) + .forFamily(STATE_FAMILY); + + putInCache( + keyCache, StateNamespaces.global(), new TestStateTag("tag1"), new TestState("g1"), 2); + keyCache.persist(); + + java.io.StringWriter writer = new java.io.StringWriter(); + noHistogramCache.appendSummaryHtml(new java.io.PrintWriter(writer)); + String summary = writer.toString(); + + org.junit.Assert.assertFalse(summary.contains("Entry Weight Dist")); + } + /** Verifies that values are cached in the appropriate namespaces. */ @Test public void testInvalidation() throws Exception { diff --git a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto index b7579cbacb8e..58e4f7df3c34 100644 --- a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto +++ b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto @@ -986,6 +986,8 @@ message UserWorkerRunnerV1Settings { optional ConnectivityType connectivity_type = 4 [default = CONNECTIVITY_TYPE_DEFAULT]; + optional int64 max_cached_entry_bytes = 5 [default = -1]; + reserved 1, 2; } From a992682779b889faacebd69596e4176253ff0360 Mon Sep 17 00:00:00 2001 From: Subramanya V Date: Mon, 18 May 2026 19:54:51 +0530 Subject: [PATCH 177/490] Fix non-unique job names in TextIOReadTest (#38242) --- .../test/java/org/apache/beam/sdk/io/TextIOReadTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java index c475d6d203b8..e7d25608eb7b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java @@ -453,10 +453,12 @@ public void testReadLinesWithDefaultDelimiterOnSplittingSourceAndSlowReadChannel getTextSource(path.toString(), null, 0) .createForSubrangeOfFile(metadata, 0, metadata.sizeBytes()); - PipelineOptions options = PipelineOptionsFactory.create(); - // Check every possible split positions. for (int i = 0; i < line.length(); ++i) { + + PipelineOptions options = PipelineOptionsFactory.create(); + options.setJobName("textio-test-" + i + "-" + System.currentTimeMillis()); + double fraction = i * 1.0 / line.length(); FileBasedReader reader = source.createSingleFileReader(options); From c47887ee38af15657aaa1767670d25a70344448c Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Mon, 18 May 2026 12:10:08 -0400 Subject: [PATCH 178/490] Rename run_pylint.sh (#38307) --- sdks/python/scripts/{run_pylint.sh => run_lint.sh} | 6 +++--- sdks/python/tox.ini | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename sdks/python/scripts/{run_pylint.sh => run_lint.sh} (96%) diff --git a/sdks/python/scripts/run_pylint.sh b/sdks/python/scripts/run_lint.sh similarity index 96% rename from sdks/python/scripts/run_pylint.sh rename to sdks/python/scripts/run_lint.sh index a9a0b0ec50d5..58d75cd7bace 100755 --- a/sdks/python/scripts/run_pylint.sh +++ b/sdks/python/scripts/run_lint.sh @@ -16,10 +16,10 @@ # limitations under the License. # -# This script will run pylint and pep8 on all module files. +# This script will run ruff and isort on all module files. # -# Use "pylint apache_beam" to run pylint all files. -# Use "pep8 apache_beam" to run pep8 all files. +# Use "ruff check apache_beam" to run ruff all files. +# Use "isort apache_beam" to run isort all files. # # The exit-code of the script indicates success or a failure. diff --git a/sdks/python/tox.ini b/sdks/python/tox.ini index ffc39a086efe..6dab85083a02 100644 --- a/sdks/python/tox.ini +++ b/sdks/python/tox.ini @@ -198,7 +198,7 @@ extras = dev commands = ruff --version - time {toxinidir}/scripts/run_pylint.sh + time {toxinidir}/scripts/run_lint.sh pyrefly --version time python setup.py pyrefly From cf72b33f6ad93ef78b9902fae69ac8db67ecaf8f Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Mon, 18 May 2026 14:02:48 -0400 Subject: [PATCH 179/490] add todo (#38517) --- sdks/java/io/expansion-service/build.gradle | 6 ++---- sdks/java/io/iceberg/build.gradle | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 4683b4fd6f50..1045ad4aeed0 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -59,10 +59,8 @@ configurations.runtimeClasspath { resolutionStrategy.force 'com.nimbusds:nimbus-jose-jwt:9.37.4' // [iceberg] - // bigdataoss:gcs-connector and parquet:parquet-hadoop have conflicts with global hadoop-common:3.4.2 - // upgrading gcs-connector to 4.0.0 would be fine, because it uses hadoop-common 3.4.2 - // but parquet-hadoop is still at 3.3.0 - // so for now we need to pin hadoop to 3.3.6 until parquet-hadoop releases a version that uses hadoop 3.4.2+ + // TODO(https://github.com/apache/beam/issues/38515): + // Remove below pins when parquet-hadoop upgrades to hadoop-common:3.4.2 resolutionStrategy.force 'org.apache.hadoop:hadoop-common:3.3.6' resolutionStrategy.force 'org.apache.hadoop:hadoop-client:3.3.6' resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs:3.3.6' diff --git a/sdks/java/io/iceberg/build.gradle b/sdks/java/io/iceberg/build.gradle index 2dae0335f9c9..8142c5f5b90b 100644 --- a/sdks/java/io/iceberg/build.gradle +++ b/sdks/java/io/iceberg/build.gradle @@ -118,10 +118,8 @@ dependencies { configurations.all { // iceberg-core needs avro:1.12.0 resolutionStrategy.force 'org.apache.avro:avro:1.12.0' - // bigdataoss:gcs-connector and parquet:parquet-hadoop have conflicts with global hadoop-common:3.4.2 - // upgrading gcs-connector to 4.0.0 would be fine, because it uses hadoop-common 3.4.2 - // but parquet-hadoop is still at 3.3.0 - // so for now we need to pin hadoop to 3.3.6 until parquet-hadoop releases a version that uses hadoop 3.4.2+ + // TODO(https://github.com/apache/beam/issues/38515): + // Remove below pins when parquet-hadoop upgrades to hadoop-common:3.4.2 resolutionStrategy.force 'org.apache.hadoop:hadoop-common:3.3.6' resolutionStrategy.force 'org.apache.hadoop:hadoop-client:3.3.6' resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs:3.3.6' From 72bf07cbecf30cc2ed1d75d05587620a1b811b48 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 18 May 2026 15:58:17 -0400 Subject: [PATCH 180/490] Fix interactive environment clean up failure at atexit. (#38526) * Fix interactive environment clean up failure at atexit. * Fix failed tests. * Formatting. --- .../runners/interactive/cache_manager.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/sdks/python/apache_beam/runners/interactive/cache_manager.py b/sdks/python/apache_beam/runners/interactive/cache_manager.py index 0dc79d4001aa..0b756a573698 100644 --- a/sdks/python/apache_beam/runners/interactive/cache_manager.py +++ b/sdks/python/apache_beam/runners/interactive/cache_manager.py @@ -19,6 +19,7 @@ import base64 import collections +import logging import os import tempfile from urllib.parse import quote @@ -29,9 +30,12 @@ from apache_beam.io import filesystems from apache_beam.io import textio from apache_beam.io import tfrecordio +from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.testing import test_stream from apache_beam.transforms import combiners +_LOGGER = logging.getLogger(__name__) + class CacheManager(object): """Abstract class for caching PCollections. @@ -286,13 +290,17 @@ def raw_source(self, *labels): return self._reader_class(self._glob_path(*labels)) def cleanup(self): - if self._cache_dir.startswith('gs://'): - from apache_beam.io.gcp import gcsfilesystem - from apache_beam.options.pipeline_options import PipelineOptions - fs = gcsfilesystem.GCSFileSystem(PipelineOptions()) - fs.delete([self._cache_dir + '/full/']) - elif filesystems.FileSystems.exists(self._cache_dir): - filesystems.FileSystems.delete([self._cache_dir]) + try: + if self._cache_dir.startswith('gs://'): + # Import GCP dependencies only when needed. + from apache_beam.io.gcp import gcsfilesystem + fs = gcsfilesystem.GCSFileSystem(PipelineOptions()) + fs.delete([self._cache_dir + '/full/']) + elif filesystems.FileSystems.exists(self._cache_dir): + filesystems.FileSystems.delete([self._cache_dir]) + except Exception as e: + _LOGGER.warning( + 'Failed to clean up cache directory %s: %s', self._cache_dir, e) self._saved_pcoders = {} def _glob_path(self, *labels): From 242701fac70aad61cdc329a70df664c48460e80e Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 19 May 2026 07:55:51 -0400 Subject: [PATCH 181/490] Fix PipelineOptions deserialization NPE (#38531) --- .../beam_PreCommit_Python_PVR_Flink.json | 4 +++ .../sdk/options/PipelineOptionsFactory.java | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 .github/trigger_files/beam_PreCommit_Python_PVR_Flink.json diff --git a/.github/trigger_files/beam_PreCommit_Python_PVR_Flink.json b/.github/trigger_files/beam_PreCommit_Python_PVR_Flink.json new file mode 100644 index 000000000000..616d37428c01 --- /dev/null +++ b/.github/trigger_files/beam_PreCommit_Python_PVR_Flink.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "revision": 1 +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/PipelineOptionsFactory.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/PipelineOptionsFactory.java index ac76a57b6b07..b9061e1734cf 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/PipelineOptionsFactory.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/PipelineOptionsFactory.java @@ -502,13 +502,6 @@ Class getProxyClass() { new ObjectMapper() .registerModules(ObjectMapper.findModules(ReflectHelpers.findClassLoader())); - private static final DefaultDeserializationContext DESERIALIZATION_CONTEXT = - new DefaultDeserializationContext.Impl(MAPPER.getDeserializationContext().getFactory()) - .createInstance( - MAPPER.getDeserializationConfig(), - new TokenBuffer(MAPPER, false).asParser(), - new InjectableValues.Std()); - static final DefaultSerializerProvider SERIALIZER_PROVIDER = new DefaultSerializerProvider.Impl() .createInstance(MAPPER.getSerializationConfig(), MAPPER.getSerializerFactory()); @@ -1733,7 +1726,12 @@ private static JsonDeserializer computeDeserializerForMethod(Method meth BeanProperty prop = createBeanProperty(method); AnnotatedMember annotatedMethod = prop.getMember(); - DefaultDeserializationContext context = DESERIALIZATION_CONTEXT.copy(); + // Initialize a new context that is properly associated with a dummy parser. + // Using copy() here would leave the context's transient parser field as null, + // causing NullPointerExceptions in Jackson 2.14+ when deserializers try to + // query the parser for format constraints or coercion validations. + JsonParser dummyParser = new TokenBuffer(MAPPER, false).asParser(); + DefaultDeserializationContext context = createDeserializationContext(dummyParser); Object maybeDeserializerClass = context.getAnnotationIntrospector().findDeserializer(annotatedMethod); @@ -1756,6 +1754,11 @@ private static JsonDeserializer computeDeserializerForMethod(Method meth } } + private static DefaultDeserializationContext createDeserializationContext(JsonParser parser) { + return ((DefaultDeserializationContext) MAPPER.getDeserializationContext()) + .createInstance(MAPPER.getDeserializationConfig(), parser, new InjectableValues.Std()); + } + private static Optional> computeCustomSerializerForMethod(Method method) { try { BeanProperty prop = createBeanProperty(method); @@ -1805,7 +1808,12 @@ static Object deserializeNode(JsonNode node, Method method) throws IOException { parser.nextToken(); JsonDeserializer jsonDeserializer = getDeserializerForMethod(method); - return jsonDeserializer.deserialize(parser, DESERIALIZATION_CONTEXT.copy()); + // Create a fresh context that is correctly associated with the active parser. + // Using copy() here would leave the context's transient parser field as null, + // causing NullPointerExceptions in Jackson 2.14+ deserializers during coercion + // validations and length checks. + DefaultDeserializationContext context = createDeserializationContext(parser); + return jsonDeserializer.deserialize(parser, context); } /** From d43fe62f0a1c6e11c567070947bf865c96fc4c80 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 19 May 2026 09:29:23 -0400 Subject: [PATCH 182/490] add yaml agent development skill (#38382) --- .agent/skills/yaml-development/SKILL.md | 107 ++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .agent/skills/yaml-development/SKILL.md diff --git a/.agent/skills/yaml-development/SKILL.md b/.agent/skills/yaml-development/SKILL.md new file mode 100644 index 000000000000..c71f91cae00d --- /dev/null +++ b/.agent/skills/yaml-development/SKILL.md @@ -0,0 +1,107 @@ +--- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: yaml-development +description: Guides YAML SDK development in Apache Beam, including environment setup, testing, and key concepts. Use when working with Beam YAML code in sdks/python/apache_beam/yaml/. +--- + +# YAML Development in Apache Beam + +## Project Structure + +### Key Files in `sdks/python/apache_beam/yaml/` +- `integration_tests.py` - Runs integration tests defined in YAML files or using testcontainers. +- `main.py` - Entry point for running YAML pipelines from the command line. +- `pipeline.schema.yaml` - JSON schema defining the valid structure for Beam YAML pipelines. +- `standard_io.yaml` - Declarations of standard IO transforms and their mappings to providers. +- `standard_providers.yaml` - Configuration for standard providers (e.g., Java expansion services). +- `yaml_combine.py` - Implementations for aggregation and combining operations. +- `yaml_io.py` - Mappings and logic for IO transforms (e.g., PubSub, BigQuery, Iceberg). +- `yaml_join.py` - Implementations for join operations. +- `yaml_mapping.py` - Implementations for mapping operations (e.g., `MapToFields`). +- `yaml_provider.py` - Manages providers (Python, Java cross-language) that implement transforms. +- `yaml_transform.py` - Core YAML expansion logic, parsing, and translation to Beam pipelines. + +## Environment Setup +Since Beam YAML is implemented within the Python SDK, the environment setup is identical to Python development. Refer to the `python-development` skill for details on using `pyenv` and installing in editable mode (e.g., use `pip install -e sdks/python[gcp,test]` from the root directory). + +## Running YAML Pipelines + +You can run Beam YAML pipelines using the `main.py` script in the YAML directory. + +### Using `main.py` directly +```bash +python -m apache_beam.yaml.main --yaml_pipeline_file=/path/to/pipeline.yaml [pipeline_options] +``` + +### Example: Running locally +```bash +python -m apache_beam.yaml.main \ + --yaml_pipeline_file=sdks/python/apache_beam/yaml/examples/simple_filter.yaml \ + --runner=DirectRunner +``` + +### Example: Running on Dataflow +```bash +python -m apache_beam.yaml.main \ + --yaml_pipeline_file=sdks/python/apache_beam/yaml/examples/simple_filter.yaml \ + --runner=DataflowRunner \ + --project=my-project \ + --region=us-central1 \ + --temp_location=gs://my-bucket/temp +``` + +## Running Tests + +### Unit Tests +Beam YAML has extensive unit tests covering parsing, expansion, and specific transforms. +```bash +# Run all tests in a file +pytest sdks/python/apache_beam/yaml/yaml_transform_test.py + +# Run a specific test +pytest sdks/python/apache_beam/yaml/yaml_transform_test.py::YamlTransformTest::test_simple_pipeline +``` + +### Integration Tests +Integration tests often spin up Docker containers (via `testcontainers`) for external services like MongoDB, Kafka, or databases. +```bash +# Run integration tests matching a specific keyword (e.g., mongodb) +pytest sdks/python/apache_beam/yaml/integration_tests.py -k mongodb +``` + +## Key Concepts + +### Providers +Beam YAML uses "providers" to find implementations for transforms requested in the YAML file. +- **Inline/Python Providers**: Leverage Python functions or PTransforms directly. +- **Java/External Providers**: Use Beam's cross-language capabilities to invoke Java transforms via an expansion service. + +### Preprocessing +Before execution, a YAML pipeline is preprocessed to resolve schemas, match transforms to providers, and expand shorthand notations (like `chain` or `source`/`sink` composites). + +## Common Issues + +### Cross-language Failures +If a test requires a Java transform, ensure that: +1. Docker is running (if using testcontainers). +2. The correct expansion service is available or can be started. +3. Java environment is correctly configured (sometimes requires specific Java versions like Java 17/21). + +### Schema Mismatches +YAML relies heavily on Beam schemas. Ensure that fields produced by a transform match the fields expected by the next transform. Use explicit mapping if necessary. From c02383dbe33bdd799f72b857ea6b4acb152a6b5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 14:05:01 -0400 Subject: [PATCH 183/490] Bump protobufjs in /sdks/typescript (#38538) Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) to 7.6.0 and updates ancestor dependency . These dependencies need to be updated together. Updates `protobufjs` from 7.5.5 to 7.6.0 - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.0/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.5...protobufjs-v7.6.0) Updates `protobufjs` from 8.0.1 to 8.4.0 - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.0/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.5...protobufjs-v7.6.0) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 7.6.0 dependency-type: indirect - dependency-name: protobufjs dependency-version: 8.4.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/typescript/package-lock.json | 182 +++++++++++++----------------- sdks/typescript/package.json | 2 +- 2 files changed, 80 insertions(+), 104 deletions(-) diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index 7f3a6bddd023..7bdc88efaf90 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "apache-beam", - "version": "2.74.0-SNAPSHOT", + "version": "2.75.0-SNAPSHOT", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "apache-beam", - "version": "2.74.0-SNAPSHOT", + "version": "2.75.0-SNAPSHOT", "dependencies": { "@google-cloud/pubsub": "^4.2.0", "@grpc/grpc-js": "^1.14.3", @@ -19,7 +19,7 @@ "fast-deep-equal": "^3.1.3", "find-git-root": "^1.0.4", "long": "^4.0.0", - "protobufjs": "~8.0.1", + "protobufjs": "~8.4.0", "queue-typescript": "^1.0.1", "serialize-closures": "^0.2.7", "ts-closure-transform": "^0.1.7", @@ -360,23 +360,23 @@ "license": "Apache-2.0" }, "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -672,9 +672,9 @@ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", @@ -682,12 +682,11 @@ "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -696,9 +695,9 @@ "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", @@ -2535,23 +2534,23 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "node_modules/google-gax/node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -3770,46 +3769,35 @@ "license": "Apache-2.0" }, "node_modules/proto3-json-serializer/node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" } }, "node_modules/protobufjs": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.1.tgz", - "integrity": "sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", + "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", "hasInstallScript": true, "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -4930,22 +4918,22 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" } } } @@ -5153,9 +5141,9 @@ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==" }, "@protobufjs/eventemitter": { "version": "1.1.0", @@ -5163,12 +5151,11 @@ "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "@protobufjs/float": { @@ -5177,9 +5164,9 @@ "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==" }, "@protobufjs/path": { "version": "1.1.2", @@ -6507,22 +6494,22 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" } }, "retry-request": { @@ -7391,43 +7378,32 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.0.tgz", + "integrity": "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" } } } }, "protobufjs": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.1.tgz", - "integrity": "sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", + "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "dependencies": { "long": { diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index d052111248ed..6d9ffdb4aaae 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -47,7 +47,7 @@ "fast-deep-equal": "^3.1.3", "find-git-root": "^1.0.4", "long": "^4.0.0", - "protobufjs": "~8.0.1", + "protobufjs": "~8.4.0", "queue-typescript": "^1.0.1", "serialize-closures": "^0.2.7", "ts-closure-transform": "^0.1.7", From 144fb398c478bbd1b84795781bdf6809797e850d Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Tue, 19 May 2026 16:31:35 -0400 Subject: [PATCH 184/490] Make sure session creation happens before starting agent (#38477) * Make sure session creation happens before starting agent * fix var * Fix up * Fix text parsing * yapf --- .../ml/inference/agent_development_kit.py | 36 ++++++++++-------- .../inference/agent_development_kit_test.py | 38 ++++++++++++------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/sdks/python/apache_beam/ml/inference/agent_development_kit.py b/sdks/python/apache_beam/ml/inference/agent_development_kit.py index 1130598f06f8..386955b0dfae 100644 --- a/sdks/python/apache_beam/ml/inference/agent_development_kit.py +++ b/sdks/python/apache_beam/ml/inference/agent_development_kit.py @@ -229,17 +229,6 @@ def run_inference( for element in batch: session_id: str = inference_args.get("session_id", str(uuid.uuid4())) - # Ensure a session exists for this invocation - try: - model.session_service.create_session( - app_name=self._app_name, - user_id=user_id, - session_id=session_id, - ) - except sessions.SessionExistsError: - # It's okay if the session already exists for shared session IDs. - pass - # Wrap plain strings in a Content object if isinstance(element, str): # pyrefly: ignore[bad-instantiation] @@ -249,7 +238,8 @@ def run_inference( message = element agent_invocations.append( - self._invoke_agent(model, user_id, session_id, message)) + self._invoke_agent( + model, user_id, session_id, self._app_name, message)) elements_with_sessions.append(element) # Run all agent invocations concurrently @@ -274,6 +264,7 @@ async def _invoke_agent( runner: "Runner", user_id: str, session_id: str, + app_name: str, message: genai_Content, ) -> Optional[str]: """Drives the ADK event loop and returns the final response text. @@ -288,15 +279,30 @@ async def _invoke_agent( The text of the agent's final response, or ``None`` if the agent produced no final text response. """ + # Check for your specific session ID + try: + # Attempt to get the specific session + await runner.session_service.get_session(session_id) + except Exception as e: + await runner.session_service.create_session( + app_name=app_name, + user_id=user_id, + session_id=session_id, + ) + async for event in runner.run_async( user_id=user_id, session_id=session_id, new_message=message, ): if event.is_final_response(): - if event.content: - return event.content.text - return None + if event.content and event.content.parts: + return "".join([p.text for p in event.content.parts]) + raise ValueError( + f"Agent {runner.agent.name} did not return a response, " + f"final event: {event}") + + raise ValueError(f"Agent {runner.agent.name} did not return a response") def get_metrics_namespace(self) -> str: return "ADKAgentModelHandler" diff --git a/sdks/python/apache_beam/ml/inference/agent_development_kit_test.py b/sdks/python/apache_beam/ml/inference/agent_development_kit_test.py index 6d59bceb9d39..6c8b5c5b351c 100644 --- a/sdks/python/apache_beam/ml/inference/agent_development_kit_test.py +++ b/sdks/python/apache_beam/ml/inference/agent_development_kit_test.py @@ -41,9 +41,11 @@ def _make_mock_runner( final_text: str = "Hello from agent", ) -> mock.MagicMock: """Returns a mock Runner whose run_async yields one final-response event.""" - # Build a mock event that looks like a final response + part = mock.MagicMock() + part.text = final_text + content = mock.MagicMock() - content.text = final_text + content.parts = [part] event = mock.MagicMock() event.is_final_response.return_value = True @@ -56,6 +58,9 @@ async def _async_gen(*args, **kwargs): runner.agent = agent runner.run_async = mock.MagicMock(side_effect=_async_gen) runner.session_service = mock.MagicMock() + runner.session_service.get_session = mock.AsyncMock( + side_effect=Exception("Session not found")) + runner.session_service.create_session = mock.AsyncMock() return runner @@ -251,8 +256,8 @@ def test_session_created_with_correct_app_name(self): class TestResponseExtraction(unittest.TestCase): """Tests for extraction of the final response from the event stream.""" - def test_returns_none_when_no_final_response(self): - """Agent emits only non-final events; inference should be None.""" + def test_raises_when_no_final_response(self): + """Agent emits only non-final events; should raise ValueError.""" agent = _make_mock_agent() # Build a runner that yields only non-final events @@ -266,14 +271,14 @@ async def _async_gen(*args, **kwargs): runner.agent = agent runner.run_async = mock.MagicMock(side_effect=_async_gen) runner.session_service = mock.MagicMock() + runner.session_service.get_session = mock.AsyncMock(side_effect=Exception()) + runner.session_service.create_session = mock.AsyncMock() handler = ADKAgentModelHandler(agent=agent) - results = list(handler.run_inference(batch=["hello"], model=runner)) - - self.assertEqual(len(results), 1) - self.assertIsNone(results[0].inference) + with self.assertRaisesRegex(ValueError, "did not return a response"): + list(handler.run_inference(batch=["hello"], model=runner)) - def test_returns_none_when_final_event_has_no_content(self): + def test_raises_when_final_event_has_no_content(self): agent = _make_mock_agent() event = mock.MagicMock() @@ -287,19 +292,22 @@ async def _async_gen(*args, **kwargs): runner.agent = agent runner.run_async = mock.MagicMock(side_effect=_async_gen) runner.session_service = mock.MagicMock() + runner.session_service.get_session = mock.AsyncMock(side_effect=Exception()) + runner.session_service.create_session = mock.AsyncMock() handler = ADKAgentModelHandler(agent=agent) - results = list(handler.run_inference(batch=["hello"], model=runner)) - - self.assertIsNone(results[0].inference) + with self.assertRaisesRegex(ValueError, "did not return a response"): + list(handler.run_inference(batch=["hello"], model=runner)) def test_stops_after_first_final_response(self): """Multiple final events: only the first one's text should be used.""" agent = _make_mock_agent() def _make_event(text: str): + part = mock.MagicMock() + part.text = text content = mock.MagicMock() - content.text = text + content.parts = [part] event = mock.MagicMock() event.is_final_response.return_value = True event.content = content @@ -313,6 +321,8 @@ async def _async_gen(*args, **kwargs): runner.agent = agent runner.run_async = mock.MagicMock(side_effect=_async_gen) runner.session_service = mock.MagicMock() + runner.session_service.get_session = mock.AsyncMock(side_effect=Exception()) + runner.session_service.create_session = mock.AsyncMock() handler = ADKAgentModelHandler(agent=agent) results = list(handler.run_inference(batch=["hi"], model=runner)) @@ -326,7 +336,7 @@ def test_invoke_agent_static_method_directly(self): result = asyncio.run( ADKAgentModelHandler._invoke_agent( - runner, "user", "session-1", mock.MagicMock())) + runner, "user", "session-1", "test_app", mock.MagicMock())) self.assertEqual(result, "direct result") From b13e9caa6116419afa70512a086c99eaaaa4a304 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 19 May 2026 16:43:10 -0400 Subject: [PATCH 185/490] Remove Java 8 variant of beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions (#38541) --- ...am_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml index e5d4a3d26f83..d32e22e319be 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml @@ -60,7 +60,7 @@ jobs: matrix: job_name: [beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions] job_phrase: [Run Dataflow ValidatesRunner Java] - java_version: ['8', '25'] + java_version: ['25'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || From 9c15d09faf893295f2fbfcc284532551b171a127 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Tue, 19 May 2026 22:51:31 +0200 Subject: [PATCH 186/490] Pin tensor_rt digest for PyTorch sentiment Dataflow benchmarks (#38374) * Fix DistilBERT config compatibility in sentiment benchmark Pin tensor_rt digest for PyTorch sentiment Dataflow benchmarks * Harden Dataflow PyTorch sentiment benchmark worker compatibility * Added default for DisplayData for table row inference batch benchmark * used tensor_rt:latest for sentiment dataflow --- ...entiment_Batch_DistilBert_Base_Uncased.txt | 3 + ...ment_Streaming_DistilBert_Base_Uncased.txt | 3 + .../examples/inference/pytorch_sentiment.py | 80 ++++++++++++++----- .../table_row_inference_benchmark.py | 4 +- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Batch_DistilBert_Base_Uncased.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Batch_DistilBert_Base_Uncased.txt index 4642156d57c4..8dbdfa5cd0e1 100644 --- a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Batch_DistilBert_Base_Uncased.txt +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Batch_DistilBert_Base_Uncased.txt @@ -18,6 +18,7 @@ --machine_type=n1-standard-2 --num_workers=20 --max_num_workers=250 +--timeout_ms=600000 --disk_size_gb=50 --autoscaling_algorithm=THROUGHPUT_BASED --staging_location=gs://temp-storage-for-perf-tests/loadtests @@ -31,5 +32,7 @@ --device=CPU --input_file=gs://apache-beam-ml/testing/inputs/sentences_50k.txt --runner=DataflowRunner +--sdk_location=container +--sdk_container_image=us.gcr.io/apache-beam-testing/python-postcommit-it/tensor_rt:latest --model_path=distilbert-base-uncased-finetuned-sst-2-english --model_state_dict_path=gs://apache-beam-ml/models/huggingface.sentiment.distilbert-base-uncased.pth \ No newline at end of file diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Streaming_DistilBert_Base_Uncased.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Streaming_DistilBert_Base_Uncased.txt index d10b9bb2dfcb..4d285bbe0eb9 100644 --- a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Streaming_DistilBert_Base_Uncased.txt +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Sentiment_Streaming_DistilBert_Base_Uncased.txt @@ -18,6 +18,7 @@ --machine_type=n1-standard-2 --num_workers=20 --max_num_workers=250 +--timeout_ms=600000 --disk_size_gb=50 --autoscaling_algorithm=THROUGHPUT_BASED --staging_location=gs://temp-storage-for-perf-tests/loadtests @@ -31,6 +32,8 @@ --device=CPU --input_file=gs://apache-beam-ml/testing/inputs/sentences_50k.txt --runner=DataflowRunner +--sdk_location=container +--sdk_container_image=us.gcr.io/apache-beam-testing/python-postcommit-it/tensor_rt:latest --dataflow_service_options=worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver --model_path=distilbert-base-uncased-finetuned-sst-2-english --model_state_dict_path=gs://apache-beam-ml/models/huggingface.sentiment.distilbert-base-uncased.pth diff --git a/sdks/python/apache_beam/examples/inference/pytorch_sentiment.py b/sdks/python/apache_beam/examples/inference/pytorch_sentiment.py index 3bb36930a045..95baca9c0308 100644 --- a/sdks/python/apache_beam/examples/inference/pytorch_sentiment.py +++ b/sdks/python/apache_beam/examples/inference/pytorch_sentiment.py @@ -47,9 +47,6 @@ class SentimentPostProcessor(beam.DoFn): """Processes PredictionResult to extract sentiment label and confidence.""" - def __init__(self, tokenizer: DistilBertTokenizerFast): - self.tokenizer = tokenizer - def process(self, element: tuple[str, PredictionResult]) -> Iterable[dict]: text, prediction_result = element logits = prediction_result.inference['logits'] @@ -62,16 +59,34 @@ def process(self, element: tuple[str, PredictionResult]) -> Iterable[dict]: } -def tokenize_text(text: str, - tokenizer: DistilBertTokenizerFast) -> tuple[str, dict]: - """Tokenizes input text using the specified tokenizer.""" - tokenized = tokenizer( - text, - padding='max_length', - truncation=True, - max_length=128, - return_tensors="pt") - return text, {k: torch.squeeze(v) for k, v in tokenized.items()} +class TokenizeTextDoFn(beam.DoFn): + """Initializes tokenizer per worker and tokenizes input text.""" + def __init__(self, model_path: str): + self.model_path = model_path + self.tokenizer = None + + def setup(self): + self.tokenizer = DistilBertTokenizerFast.from_pretrained(self.model_path) + if self.tokenizer.pad_token is None: + self.tokenizer.pad_token = '[PAD]' + + def process(self, text: str) -> Iterable[tuple[str, dict]]: + tokenized = self.tokenizer( + text, + padding='max_length', + truncation=True, + max_length=128, + return_tensors="pt") + yield text, {k: torch.squeeze(v, 0) for k, v in tokenized.items()} + + +class DistilBertForSequenceClassificationCompat( + DistilBertForSequenceClassification): + """Builds config in worker runtime to avoid cross-env config drift.""" + def __init__(self, model_name: str, num_labels: int = 2): + config = _ensure_transformers_config_compat( + DistilBertConfig.from_pretrained(model_name, num_labels=num_labels)) + super().__init__(config) class RateLimitDoFn(beam.DoFn): @@ -83,6 +98,31 @@ def process(self, element): yield element +def _ensure_transformers_config_compat( + config: DistilBertConfig) -> DistilBertConfig: + """Adds missing config attributes for cross-version transformers compatibility. + + The benchmark can run with container images whose transformers version differs + from the launcher environment. Some versions assume these attributes exist. + """ + # Use a default config instance as the source of canonical attributes for the + # transformers version available on the worker. This avoids chasing one + # missing field at a time (e.g. torchscript, output_attentions). + default_config = DistilBertConfig() + for key, value in default_config.to_dict().items(): + if not hasattr(config, key): + setattr(config, key, value) + + # Keep non-serialized fields explicitly for older/newer transformers mixes. + if not hasattr(config, 'pruned_heads'): + config.pruned_heads = {} + if not hasattr(config, 'torchscript'): + config.torchscript = False + if not hasattr(config, 'return_dict'): + config.return_dict = True + return config + + def parse_known_args(argv): """Parses command-line arguments for pipeline execution.""" parser = argparse.ArgumentParser() @@ -235,13 +275,14 @@ def run( pipeline_options.view_as(StandardOptions).streaming = True model_handler = PytorchModelHandlerKeyedTensor( - model_class=DistilBertForSequenceClassification, - model_params={'config': DistilBertConfig(num_labels=2)}, + model_class=DistilBertForSequenceClassificationCompat, + model_params={ + 'model_name': known_args.model_path, + 'num_labels': 2, + }, state_dict_path=known_args.model_state_dict_path, device='GPU') - tokenizer = DistilBertTokenizerFast.from_pretrained(known_args.model_path) - pipeline = test_pipeline or beam.Pipeline(options=pipeline_options) # Main pipeline: read, process, write result to BigQuery output table @@ -264,9 +305,9 @@ def run( _ = ( input - | 'Tokenize' >> beam.Map(lambda text: tokenize_text(text, tokenizer)) + | 'Tokenize' >> beam.ParDo(TokenizeTextDoFn(known_args.model_path)) | 'RunInference' >> RunInference(KeyedModelHandler(model_handler)) - | 'PostProcess' >> beam.ParDo(SentimentPostProcessor(tokenizer)) + | 'PostProcess' >> beam.ParDo(SentimentPostProcessor()) | 'WriteToBigQuery' >> beam.io.WriteToBigQuery( known_args.output_table, schema='text:STRING, sentiment:STRING, confidence:FLOAT', @@ -277,6 +318,7 @@ def run( result = pipeline.run() result.wait_until_finish(duration=1800000) # 30 min result.cancel() + result.wait_until_finish(duration=600000) # up to 10 min to settle cancel cleanup_pubsub_resources( project=known_args.project, diff --git a/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py b/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py index bca5263b9f9d..b8591a0fea83 100644 --- a/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py +++ b/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py @@ -44,8 +44,8 @@ class TableRowInferenceOptions( @classmethod def _add_argparse_args(cls, parser): parser.add_argument('--mode', default='batch') - parser.add_argument('--input_subscription') - parser.add_argument('--input_file') + parser.add_argument('--input_subscription', default='') + parser.add_argument('--input_file', default='') parser.add_argument('--output_table') parser.add_argument('--model_path') parser.add_argument('--feature_columns') From 44d9fcbb4e953f7e03ca88a1aabd17d9959feaaf Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Tue, 19 May 2026 19:19:37 -0700 Subject: [PATCH 187/490] Update Dataflow Dependency Java (#38542) --- .../main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 733f32c6b573..27b9cef9637a 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -743,7 +743,7 @@ class BeamModulePlugin implements Plugin { google_api_common : "com.google.api:api-common", // google_cloud_platform_libraries_bom sets version google_api_services_bigquery : "com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0", // [bomupgrader] sets version google_api_services_cloudresourcemanager : "com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0", // [bomupgrader] sets version - google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260405-$google_clients_version", + google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260503-$google_clients_version", google_api_services_healthcare : "com.google.apis:google-api-services-healthcare:v1-rev20240130-$google_clients_version", google_api_services_pubsub : "com.google.apis:google-api-services-pubsub:v1-rev20220904-$google_clients_version", google_api_services_storage : "com.google.apis:google-api-services-storage:v1-rev20260204-2.0.0", // [bomupgrader] sets version From 04d35fd2eb26599ceb25ccdcb906de0bd15f9bab Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Tue, 19 May 2026 19:20:01 -0700 Subject: [PATCH 188/490] Update Dataflow Proto (#38540) --- .../internal/clients/dataflow/dataflow_v1b3_messages.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py index 582fb30b57b1..19822d4428b0 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py @@ -4477,10 +4477,15 @@ class Package(_messages.Message): type is: Google Cloud Storage: storage.googleapis.com/{bucket} bucket.storage.googleapis.com/ name: The name of the package. + sha256: Optional. The hex-encoded SHA256 checksum of the package. If the + checksum is provided, the worker will verify the checksum of the package + before using it. If the checksum does not match, the worker will fail to + start. """ location = _messages.StringField(1) name = _messages.StringField(2) + sha256 = _messages.StringField(3) class ParDoInstruction(_messages.Message): From 5d70e8c539cadce6b20d126bb16e1445a41b52b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 07:22:23 -0400 Subject: [PATCH 189/490] Bump google.golang.org/api from 0.279.0 to 0.280.0 in /sdks (#38563) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.279.0 to 0.280.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.279.0...v0.280.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.280.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index c72969b14bbd..5a22c2f395a9 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.44.0 golang.org/x/text v0.37.0 - google.golang.org/api v0.279.0 + google.golang.org/api v0.280.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 @@ -209,5 +209,5 @@ require ( golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index 4290813b294a..b08343d4051c 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1342,8 +1342,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74= -google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk= +google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1441,8 +1441,8 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From 6b34f13a3a2d7ec4478aa2ecf9bcac32a5aaed35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 08:16:59 -0400 Subject: [PATCH 190/490] Bump cloud.google.com/go/datastore from 1.23.0 to 1.24.0 in /sdks (#38564) Bumps [cloud.google.com/go/datastore](https://github.com/googleapis/google-cloud-go) from 1.23.0 to 1.24.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/documentai/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/kms/v1.23.0...kms/v1.24.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/datastore dependency-version: 1.24.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 5a22c2f395a9..fd3c1fa8e75e 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -27,7 +27,7 @@ toolchain go1.26.2 require ( cloud.google.com/go/bigquery v1.77.0 cloud.google.com/go/bigtable v1.47.0 - cloud.google.com/go/datastore v1.23.0 + cloud.google.com/go/datastore v1.24.0 cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 @@ -144,7 +144,7 @@ require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.7.0 // indirect - cloud.google.com/go/longrunning v0.9.0 // indirect + cloud.google.com/go/longrunning v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect @@ -208,6 +208,6 @@ require ( golang.org/x/mod v0.35.0 // indirect golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index b08343d4051c..863ea382f54c 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -58,8 +58,8 @@ cloud.google.com/go/datacatalog v1.27.0 h1:AnghhtHKCqYIe62gTPHcn9nJr5jtxjZHV4D/F cloud.google.com/go/datacatalog v1.27.0/go.mod h1:YTI11pFlC5HCj4CphEf+qWCy/z9udd7o0HVN6c2Povg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.23.0 h1:mAlWN3tnQe1OqVM3UtYBIbWTz9aU83RgW4hXOrfm9P8= -cloud.google.com/go/datastore v1.23.0/go.mod h1:bOvQQekv4VACRJmH/MBy12MT6M3udfTuCyxw+tzY+8s= +cloud.google.com/go/datastore v1.24.0 h1:auNUPJTT9gFcHNj2iKOEeE23nrjf7dE7VA6TO3jw8h0= +cloud.google.com/go/datastore v1.24.0/go.mod h1:cEkLhU6Ti/gauQ7DFrUrG8bQjiMIxi++b5ePiThi5So= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= @@ -72,8 +72,8 @@ cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= -cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= +cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= +cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= @@ -1439,8 +1439,8 @@ google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2I google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= From d48d3966393123688e6623fdc2eafcfc79356103 Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Wed, 20 May 2026 05:48:56 -0700 Subject: [PATCH 191/490] [Dataflow Streaming] Add experiment to use trigger state to know if a window is new and optimize reads (#37574) If unstable_not_update_compatible_new_window_optimization experiment is set always write trigger state so it's absence can be used to know that the window has never existed to avoid reads. This is only performed for non-merging windows. Additionally optimize for all cases that trigger state is not rewritten if it is unchanged. This logic already existed but was broken due to missing equals method implementation. --- .../beam/runners/core/ReduceFnRunner.java | 49 ++++- .../beam/runners/core/WatermarkHold.java | 17 ++ .../core/triggers/FinishedTriggersBitSet.java | 14 ++ .../triggers/TriggerStateMachineRunner.java | 43 ++++- .../beam/runners/core/ReduceFnRunnerTest.java | 58 ++++++ .../beam/runners/core/ReduceFnTester.java | 13 ++ .../TriggerStateMachineRunnerTest.java | 117 ++++++++++++ .../windmill/state/WindmillWatermarkHold.java | 94 ++++++---- .../state/WindmillStateInternalsTest.java | 167 +++++++++++++++++- .../beam/sdk/state/WatermarkHoldState.java | 10 ++ 10 files changed, 535 insertions(+), 47 deletions(-) create mode 100644 runners/core-java/src/test/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunnerTest.java diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java index 1ae0c52f853a..78505f3c65f6 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java @@ -37,6 +37,7 @@ import org.apache.beam.runners.core.triggers.TriggerStateMachineRunner; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.state.CombiningState; import org.apache.beam.sdk.state.TimeDomain; @@ -94,6 +95,11 @@ }) // TODO(https://github.com/apache/beam/issues/20497) public class ReduceFnRunner { + // Experiments guarding optimizations in development. No backward compatibility guarantees. + public static final String UNSTABLE_NOT_UPDATE_COMPATIBLE_NEW_WINDOW_OPTIMIZATION = + "unstable_not_update_compatible_new_window_optimization"; + public static final String UNSTABLE_DISABLE_WATERMARK_KNOWN_EMPTY_OPTIMIZATION = + "unstable_disable_watermark_known_empty_optimization"; /** * The {@link ReduceFnRunner} depends on most aspects of the {@link WindowingStrategy}. * @@ -218,6 +224,9 @@ public class ReduceFnRunner { */ private final NonEmptyPanes nonEmptyPanes; + private final boolean useNewWindowOptimization; + private final boolean disableWatermarkKnownEmptyOptimization; + public ReduceFnRunner( K key, WindowingStrategy windowingStrategy, @@ -244,6 +253,15 @@ public ReduceFnRunner( this.nonEmptyPanes = NonEmptyPanes.create(this.windowingStrategy, this.reduceFn); + this.useNewWindowOptimization = + windowingStrategy.getWindowFn().isNonMerging() + && ExperimentalOptions.hasExperiment( + options, UNSTABLE_NOT_UPDATE_COMPATIBLE_NEW_WINDOW_OPTIMIZATION); + + this.disableWatermarkKnownEmptyOptimization = + ExperimentalOptions.hasExperiment( + options, UNSTABLE_DISABLE_WATERMARK_KNOWN_EMPTY_OPTIMIZATION); + // Note this may incur I/O to load persisted window set data. this.activeWindows = createActiveWindowSet(); @@ -263,7 +281,8 @@ public ReduceFnRunner( new TriggerStateMachineRunner<>( triggerStateMachine, new TriggerStateMachineContextFactory<>( - windowingStrategy.getWindowFn(), stateInternals, activeWindows)); + windowingStrategy.getWindowFn(), stateInternals, activeWindows), + this.useNewWindowOptimization); } private ActiveWindowSet createActiveWindowSet() { @@ -282,6 +301,16 @@ boolean hasNoActiveWindows() { return activeWindows.getActiveAndNewWindows().isEmpty(); } + @VisibleForTesting + TriggerStateMachineRunner getTriggerRunner() { + return triggerRunner; + } + + @VisibleForTesting + ReduceFnContextFactory getContextFactory() { + return contextFactory; + } + private Set windowsThatAreOpen(Collection windows) { Set result = new HashSet<>(); for (W window : windows) { @@ -626,6 +655,20 @@ private void processElement(Map windowToMergeResult, WindowedValue StateStyle.RENAMED, value.causedByDrain()); + // TODO: Make sure the NewWindowOptimization does not create unbounded trigger state + // in GlobalWindow + if (useNewWindowOptimization && triggerRunner.isNew(directContext.state())) { + // Blindly clear state to ensure Windmill doesn't do unnecessary reads. + // TODO: Instead of the clears here, we could mark these states as empty locally + // in the state cache and/or explicitly tell that the entries are non-existent via api + reduceFn.clearState(renamedContext); + paneInfoTracker.clear(directContext.state()); + if (!disableWatermarkKnownEmptyOptimization) { + watermarkHold.setKnownEmpty(renamedContext); + } + nonEmptyPanes.clearPane(renamedContext.state()); + } + nonEmptyPanes.recordContent(renamedContext.state()); scheduleGarbageCollectionTimer(directContext); @@ -761,7 +804,7 @@ public void onTimers(Iterable timers) throws Exception { // Perform prefetching of state to determine if the trigger should fire. if (windowActivation.isGarbageCollection) { - triggerRunner.prefetchIsClosed(directContext.state()); + triggerRunner.prefetchFinishedSet(directContext.state()); } else { triggerRunner.prefetchShouldFire(directContext.window(), directContext.state()); } @@ -941,7 +984,7 @@ private void prefetchEmit( ReduceFn.Context directContext, ReduceFn.Context renamedContext) { triggerRunner.prefetchShouldFire(directContext.window(), directContext.state()); - triggerRunner.prefetchIsClosed(directContext.state()); + triggerRunner.prefetchFinishedSet(directContext.state()); prefetchOnTrigger(directContext, renamedContext); } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/WatermarkHold.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/WatermarkHold.java index 15ae8dfe5f1a..b9185ccfba3f 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/WatermarkHold.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/WatermarkHold.java @@ -466,6 +466,23 @@ public void clearHolds(ReduceFn.Context context) { context.state().access(EXTRA_HOLD_TAG).clear(); } + /** + * For internal use only; no backwards-compatibility guarantees. + * + *

    Permit marking the watermark holds as empty locally, without necessarily clearing them in + * the backend. + */ + public void setKnownEmpty(ReduceFn.Context context) { + WindowTracing.debug( + "WatermarkHold.setKnownEmpty: For key:{}; window:{}; inputWatermark:{}; outputWatermark:{}", + context.key(), + context.window(), + timerInternals.currentInputWatermarkTime(), + timerInternals.currentOutputWatermarkTime()); + context.state().access(elementHoldTag).setKnownEmpty(); + context.state().access(EXTRA_HOLD_TAG).setKnownEmpty(); + } + /** Return the current data hold, or null if none. Does not clear. For debugging only. */ public @Nullable Instant getDataCurrent(ReduceFn.Context context) { return context.state().access(elementHoldTag).read(); diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java index 7eebb4474c6c..967e1ef43f08 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java @@ -18,6 +18,7 @@ package org.apache.beam.runners.core.triggers; import java.util.BitSet; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link FinishedTriggers} implementation based on an underlying {@link BitSet}. */ public class FinishedTriggersBitSet implements FinishedTriggers { @@ -60,4 +61,17 @@ public void clearRecursively(ExecutableTriggerStateMachine trigger) { public FinishedTriggersBitSet copy() { return new FinishedTriggersBitSet((BitSet) bitSet.clone()); } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof FinishedTriggersBitSet)) { + return false; + } + return bitSet.equals(((FinishedTriggersBitSet) obj).bitSet); + } + + @Override + public int hashCode() { + return bitSet.hashCode(); + } } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java index e3791821b728..90daa5ac75b2 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java @@ -63,13 +63,16 @@ public class TriggerStateMachineRunner { private final ExecutableTriggerStateMachine rootTrigger; private final TriggerStateMachineContextFactory contextFactory; + private final boolean useNewWindowOptimization; public TriggerStateMachineRunner( ExecutableTriggerStateMachine rootTrigger, - TriggerStateMachineContextFactory contextFactory) { + TriggerStateMachineContextFactory contextFactory, + boolean useNewWindowOptimization) { checkState(rootTrigger.getTriggerIndex() == 0); this.rootTrigger = rootTrigger; this.contextFactory = contextFactory; + this.useNewWindowOptimization = useNewWindowOptimization; } private FinishedTriggersBitSet readFinishedBits(ValueState state) { @@ -81,9 +84,11 @@ private FinishedTriggersBitSet readFinishedBits(ValueState state) { } @Nullable BitSet bitSet = state.read(); - return bitSet == null - ? FinishedTriggersBitSet.emptyWithCapacity(rootTrigger.getFirstIndexAfterSubtree()) - : FinishedTriggersBitSet.fromBitSet(bitSet); + if (bitSet == null) { + return FinishedTriggersBitSet.emptyWithCapacity(rootTrigger.getFirstIndexAfterSubtree()); + } + + return FinishedTriggersBitSet.fromBitSet(bitSet); } private void clearFinishedBits(ValueState state) { @@ -99,19 +104,29 @@ public boolean isClosed(StateAccessor state) { return readFinishedBits(state.access(FINISHED_BITS_TAG)).isFinished(rootTrigger); } - public void prefetchIsClosed(StateAccessor state) { + /** Return true if the window is new (no trigger state has ever been persisted). */ + public boolean isNew(StateAccessor state) { + return isFinishedSetNeeded() && state.access(FINISHED_BITS_TAG).read() == null; + } + + @VisibleForTesting + public BitSet getFinishedBits(StateAccessor state) { + return readFinishedBits(state.access(FINISHED_BITS_TAG)).getBitSet(); + } + + public void prefetchFinishedSet(StateAccessor state) { if (isFinishedSetNeeded()) { state.access(FINISHED_BITS_TAG).readLater(); } } public void prefetchForValue(W window, StateAccessor state) { - prefetchIsClosed(state); + prefetchFinishedSet(state); rootTrigger.invokePrefetchOnElement(contextFactory.createPrefetchContext(window, rootTrigger)); } public void prefetchShouldFire(W window, StateAccessor state) { - prefetchIsClosed(state); + prefetchFinishedSet(state); rootTrigger.invokePrefetchShouldFire(contextFactory.createPrefetchContext(window, rootTrigger)); } @@ -180,13 +195,23 @@ public void onFire(W window, Timers timers, StateAccessor state) throws Excep persistFinishedSet(state, finishedSet); } - private void persistFinishedSet( - StateAccessor state, FinishedTriggersBitSet modifiedFinishedSet) { + @VisibleForTesting + void persistFinishedSet(StateAccessor state, FinishedTriggersBitSet modifiedFinishedSet) { if (!isFinishedSetNeeded()) { return; } ValueState finishedSetState = state.access(FINISHED_BITS_TAG); + + if (useNewWindowOptimization) { + if (finishedSetState.read() == null + || !readFinishedBits(finishedSetState).equals(modifiedFinishedSet)) { + // Write a value even if the bitset was empty + finishedSetState.write(modifiedFinishedSet.getBitSet()); + } + return; + } + if (!readFinishedBits(finishedSetState).equals(modifiedFinishedSet)) { if (modifiedFinishedSet.getBitSet().isEmpty()) { finishedSetState.clear(); diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnRunnerTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnRunnerTest.java index 85f6573be23e..72b379a7c71b 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnRunnerTest.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnRunnerTest.java @@ -40,15 +40,19 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; +import org.apache.beam.runners.core.ReduceFnContextFactory.StateStyle; import org.apache.beam.runners.core.metrics.MetricsContainerImpl; import org.apache.beam.runners.core.triggers.DefaultTriggerStateMachine; import org.apache.beam.runners.core.triggers.TriggerStateMachine; import org.apache.beam.sdk.coders.VarIntCoder; import org.apache.beam.sdk.metrics.MetricName; import org.apache.beam.sdk.metrics.MetricsEnvironment; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.state.TimeDomain; @@ -2343,4 +2347,58 @@ public interface TestOptions extends PipelineOptions { void setValue(int value); } + + @Test + public void testNewWindowOptimization() throws Exception { + WindowingStrategy strategy = + WindowingStrategy.of(FixedWindows.of(Duration.millis(10))) + .withTrigger(AfterPane.elementCountAtLeast(2)) + .withMode(AccumulationMode.ACCUMULATING_FIRED_PANES); + + PipelineOptions options = PipelineOptionsFactory.create(); + options + .as(ExperimentalOptions.class) + .setExperiments( + Collections.singletonList("unstable_not_update_compatible_new_window_optimization")); + ReduceFnTester, IntervalWindow> tester = + ReduceFnTester.nonCombining(strategy, options); + + IntervalWindow window = new IntervalWindow(new Instant(0), new Instant(10)); + + assertTrue( + "Window should be new", + tester + .createRunner() + .getTriggerRunner() + .isNew( + tester.createRunner().getContextFactory().base(window, StateStyle.DIRECT).state())); + + // 1. First element for a new window. + tester.injectElements(TimestampedValue.of(1, new Instant(1))); + + BitSet bitSet = + tester + .createRunner() + .getTriggerRunner() + .getFinishedBits( + tester.createRunner().getContextFactory().base(window, StateStyle.DIRECT).state()); + assertTrue("Bitset should be empty", bitSet.isEmpty()); + assertFalse("Trigger should not be finished", bitSet.get(0)); + + assertFalse( + "Window should no longer be new", + tester + .createRunner() + .getTriggerRunner() + .isNew( + tester.createRunner().getContextFactory().base(window, StateStyle.DIRECT).state())); + + // 2. Second element for the same window. + tester.injectElements(TimestampedValue.of(2, new Instant(2))); + + // Extract output. + List>> output = tester.extractOutput(); + // 2 elements fired at end of window. + assertThat(output, contains(isSingleWindowedValue(containsInAnyOrder(1, 2), 9, 0, 10))); + } } diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java index 2326a1c77d38..fe2c0a253467 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java @@ -129,6 +129,19 @@ ReduceFnTester, W> nonCombining( NullSideInputReader.empty()); } + public static + ReduceFnTester, W> nonCombining( + WindowingStrategy windowingStrategy, PipelineOptions options) throws Exception { + return new ReduceFnTester<>( + windowingStrategy, + TriggerStateMachines.stateMachineForTrigger( + TriggerTranslation.toProto(windowingStrategy.getTrigger())), + SystemReduceFn.buffering(VarIntCoder.of()), + IterableCoder.of(VarIntCoder.of()), + options, + NullSideInputReader.empty()); + } + /** * Creates a {@link ReduceFnTester} for the given {@link WindowingStrategy} and {@link * TriggerStateMachine}, for mocking the interactions between {@link ReduceFnRunner} and the diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunnerTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunnerTest.java new file mode 100644 index 000000000000..81c807532756 --- /dev/null +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunnerTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core.triggers; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.BitSet; +import org.apache.beam.runners.core.StateAccessor; +import org.apache.beam.sdk.state.ValueState; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(JUnit4.class) +public class TriggerStateMachineRunnerTest { + + @Mock private StateAccessor mockState; + @Mock private ValueState mockFinishedSetState; + @Mock private TriggerStateMachineContextFactory mockContextFactory; + @Mock private TriggerStateMachine mockTriggerStateMachine; + + private ExecutableTriggerStateMachine rootTrigger; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockState.access(TriggerStateMachineRunner.FINISHED_BITS_TAG)) + .thenReturn((ValueState) mockFinishedSetState); + rootTrigger = ExecutableTriggerStateMachine.create(mockTriggerStateMachine); + } + + @Test + public void testPersistFinishedSet_emptyAndOptimizationEnabled() throws Exception { + when(mockFinishedSetState.read()).thenReturn(null); + + TriggerStateMachineRunner runner = + new TriggerStateMachineRunner<>( + rootTrigger, + (TriggerStateMachineContextFactory) mockContextFactory, + true /* useNewWindowOptimization */); + + FinishedTriggersBitSet modifiedFinishedSet = FinishedTriggersBitSet.emptyWithCapacity(1); + + runner.persistFinishedSet(mockState, modifiedFinishedSet); + + // Should write empty bitset because optimization is enabled + verify(mockFinishedSetState).write(modifiedFinishedSet.getBitSet()); + } + + @Test + public void testPersistFinishedSet_emptyAndOptimizationDisabled() throws Exception { + when(mockFinishedSetState.read()).thenReturn(null); + + TriggerStateMachineRunner runner = + new TriggerStateMachineRunner<>( + rootTrigger, + (TriggerStateMachineContextFactory) mockContextFactory, + false /* useNewWindowOptimization */); + + FinishedTriggersBitSet modifiedFinishedSet = FinishedTriggersBitSet.emptyWithCapacity(1); + + runner.persistFinishedSet(mockState, modifiedFinishedSet); + + // Should NOT write empty bitset because optimization is disabled and it was already empty (read + // returned null) + verify(mockFinishedSetState, never()).write(modifiedFinishedSet.getBitSet()); + } + + private void runTestPersistFinishedSet_nonEmpty(boolean useNewWindowOptimization) + throws Exception { + when(mockFinishedSetState.read()).thenReturn(null); + + TriggerStateMachineRunner runner = + new TriggerStateMachineRunner<>( + rootTrigger, + (TriggerStateMachineContextFactory) mockContextFactory, + useNewWindowOptimization); + + FinishedTriggersBitSet modifiedFinishedSet = FinishedTriggersBitSet.emptyWithCapacity(1); + modifiedFinishedSet.setFinished(rootTrigger, true); + + runner.persistFinishedSet(mockState, modifiedFinishedSet); + + // Should write non-empty bitset + verify(mockFinishedSetState).write(modifiedFinishedSet.getBitSet()); + } + + @Test + public void testPersistFinishedSet_nonEmpty() throws Exception { + runTestPersistFinishedSet_nonEmpty(false); + } + + @Test + public void testPersistFinishedSet_nonEmptyAndOptimizationEnabled() throws Exception { + runTestPersistFinishedSet_nonEmpty(true); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java index b426b96cb5b9..843a7347bdc6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java @@ -17,6 +17,8 @@ */ package org.apache.beam.runners.dataflow.worker.windmill.state; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + import java.io.Closeable; import java.io.IOException; import java.util.concurrent.ExecutionException; @@ -37,6 +39,7 @@ "nullness" // TODO(https://github.com/apache/beam/issues/20497) }) public class WindmillWatermarkHold extends WindmillState implements WatermarkHoldState { + // The encoded size of an Instant. private static final int ENCODED_SIZE = 8; @@ -46,6 +49,7 @@ public class WindmillWatermarkHold extends WindmillState implements WatermarkHol private final String stateFamily; private boolean cleared = false; + private boolean knownEmpty = false; /** * If non-{@literal null}, the known current hold value, or absent if we know there are no output * watermark holds. If {@literal null}, the current hold value could depend on holds in Windmill @@ -81,6 +85,17 @@ public void clear() { cachedValue = Optional.absent(); } + @Override + public void setKnownEmpty() { + checkState(localAdditions == null, "setKnownEmpty called with local additions"); + checkState(!cleared, "setKnownEmpty called after clearing"); + checkState( + cachedValue == null || !cachedValue.isPresent(), + "setKnownEmpty called with a cached value"); + cachedValue = Optional.absent(); + knownEmpty = true; + } + @Override @SuppressWarnings("FutureReturnValueIgnored") public WindmillWatermarkHold readLater() { @@ -137,48 +152,67 @@ public Future persist( Future result; - if (!cleared && localAdditions == null) { - // No changes, so no need to update Windmill and no need to cache any value. - return Futures.immediateFuture(Windmill.WorkItemCommitRequest.newBuilder().buildPartial()); - } - - if (cleared && localAdditions == null) { - // Just clearing the persisted state; blind delete - Windmill.WorkItemCommitRequest.Builder commitBuilder = - Windmill.WorkItemCommitRequest.newBuilder(); - commitBuilder - .addWatermarkHoldsBuilder() - .setTag(stateKey.byteString()) - .setStateFamily(stateFamily) - .setReset(true); + if (knownEmpty) { + if (localAdditions != null) { + // 1. We know it's empty, so we can just update with localAdditions + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + commitBuilder + .addWatermarkHoldsBuilder() + .setTag(stateKey.byteString()) + .setStateFamily(stateFamily) + .addTimestamps(WindmillTimeUtils.harnessToWindmillTimestamp(localAdditions)); - result = Futures.immediateFuture(commitBuilder.buildPartial()); - } else if (cleared && localAdditions != null) { - // Since we cleared before adding, we can do a blind overwrite of persisted state - Windmill.WorkItemCommitRequest.Builder commitBuilder = - Windmill.WorkItemCommitRequest.newBuilder(); - commitBuilder - .addWatermarkHoldsBuilder() - .setTag(stateKey.byteString()) - .setStateFamily(stateFamily) - .setReset(true) - .addTimestamps(WindmillTimeUtils.harnessToWindmillTimestamp(localAdditions)); + cachedValue = Optional.of(localAdditions); + result = Futures.immediateFuture(commitBuilder.buildPartial()); + } else { + // 2. State is known to be empty and there are no local additions. + // Whether 'cleared' was called or not, the desired state is empty. + // So no need to update Windmill. + result = + Futures.immediateFuture(Windmill.WorkItemCommitRequest.newBuilder().buildPartial()); + } + } else if (cleared) { + if (localAdditions == null) { + // 3. Just clearing the persisted state; blind delete + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + commitBuilder + .addWatermarkHoldsBuilder() + .setTag(stateKey.byteString()) + .setStateFamily(stateFamily) + .setReset(true); - cachedValue = Optional.of(localAdditions); + result = Futures.immediateFuture(commitBuilder.buildPartial()); + } else { + // 4. Since we cleared before adding, we can do an overwrite of persisted state + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + commitBuilder + .addWatermarkHoldsBuilder() + .setTag(stateKey.byteString()) + .setStateFamily(stateFamily) + .setReset(true) + .addTimestamps(WindmillTimeUtils.harnessToWindmillTimestamp(localAdditions)); - result = Futures.immediateFuture(commitBuilder.buildPartial()); - } else if (!cleared && localAdditions != null) { - // Otherwise, we need to combine the local additions with the already persisted data + cachedValue = Optional.of(localAdditions); + result = Futures.immediateFuture(commitBuilder.buildPartial()); + } + } else if (localAdditions != null) { + // 5. Otherwise, we need to combine the local additions with the already persisted data result = combineWithPersisted(); } else { - throw new IllegalStateException("Unreachable condition"); + // No changes, so no need to update Windmill and no need to cache any value. + return Futures.immediateFuture(Windmill.WorkItemCommitRequest.newBuilder().buildPartial()); } final int estimatedByteSize = ENCODED_SIZE + stateKey.byteString().size(); + return Futures.lazyTransform( result, result1 -> { cleared = false; + knownEmpty = false; localAdditions = null; if (cachedValue != null) { cache.put(namespace, stateKey, WindmillWatermarkHold.this, estimatedByteSize); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java index 5fbae493581b..87b746089f11 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java @@ -3025,18 +3025,175 @@ public void testWatermarkClearBeforeRead() throws Exception { StateTag addr = StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); - WatermarkHoldState bag = underTest.state(NAMESPACE, addr); + WatermarkHoldState hold = underTest.state(NAMESPACE, addr); - bag.clear(); - assertThat(bag.read(), Matchers.nullValue()); + hold.clear(); + assertThat(hold.read(), Matchers.nullValue()); - bag.add(new Instant(300)); - assertThat(bag.read(), Matchers.equalTo(new Instant(300))); + hold.add(new Instant(300)); + assertThat(hold.read(), Matchers.equalTo(new Instant(300))); // Shouldn't need to read from windmill because the value is already available. Mockito.verifyNoMoreInteractions(mockReader); } + @Test + public void testWatermarkSetKnownEmptyBeforeRead() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTest.state(NAMESPACE, addr); + + hold.setKnownEmpty(); + assertThat(hold.read(), Matchers.nullValue()); + + hold.add(new Instant(300)); + assertThat(hold.read(), Matchers.equalTo(new Instant(300))); + + // Shouldn't need to read from windmill because the value is already available. + Mockito.verifyNoMoreInteractions(mockReader); + } + + @Test + public void testWatermarkSetKnownEmptyThenAddPersist() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTest.state(NAMESPACE, addr); + + hold.setKnownEmpty(); + hold.add(new Instant(1000)); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTest.persist(commitBuilder); + + assertEquals(1, commitBuilder.getWatermarkHoldsCount()); + + Windmill.WatermarkHold watermarkHold = commitBuilder.getWatermarkHolds(0); + assertEquals(key(NAMESPACE, "watermark"), watermarkHold.getTag()); + assertEquals(TimeUnit.MILLISECONDS.toMicros(1000), watermarkHold.getTimestamps(0)); + + Mockito.verifyNoInteractions(mockReader); + + assertBuildable(commitBuilder); + } + + @Test + public void testNewWatermarkAddPersist() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTestNewKey.state(NAMESPACE, addr); + + hold.add(new Instant(1000)); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTestNewKey.persist(commitBuilder); + + assertEquals(1, commitBuilder.getWatermarkHoldsCount()); + + Windmill.WatermarkHold watermarkHold = commitBuilder.getWatermarkHolds(0); + assertEquals(key(NAMESPACE, "watermark"), watermarkHold.getTag()); + assertEquals(TimeUnit.MILLISECONDS.toMicros(1000), watermarkHold.getTimestamps(0)); + + Mockito.verifyNoInteractions(mockReader); + + assertBuildable(commitBuilder); + } + + @Test + public void testNewWatermarkClearPersist() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTestNewKey.state(NAMESPACE, addr); + + hold.add(new Instant(1000)); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTestNewKey.persist(commitBuilder); + + assertEquals(1, commitBuilder.getWatermarkHoldsCount()); + + Windmill.WatermarkHold watermarkHold = commitBuilder.getWatermarkHolds(0); + assertEquals(key(NAMESPACE, "watermark"), watermarkHold.getTag()); + assertEquals(TimeUnit.MILLISECONDS.toMicros(1000), watermarkHold.getTimestamps(0)); + + Mockito.verifyNoInteractions(mockReader); + + assertBuildable(commitBuilder); + } + + @Test + public void testWatermarkSetKnownEmptyThenClearPersist() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTest.state(NAMESPACE, addr); + + hold.setKnownEmpty(); + hold.clear(); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTest.persist(commitBuilder); + + assertEquals(0, commitBuilder.getWatermarkHoldsCount()); + + Mockito.verifyNoInteractions(mockReader); + + assertBuildable(commitBuilder); + } + + @Test + public void testWatermarkSetKnownEmptyThenPersist() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTest.state(NAMESPACE, addr); + + hold.setKnownEmpty(); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTest.persist(commitBuilder); + + assertEquals(0, commitBuilder.getWatermarkHoldsCount()); + + Mockito.verifyNoInteractions(mockReader); + + assertBuildable(commitBuilder); + } + + @Test + public void testWatermarkSetKnownEmptyThenClearThenAddPersist() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + + WatermarkHoldState hold = underTest.state(NAMESPACE, addr); + + hold.setKnownEmpty(); + hold.clear(); + hold.add(new Instant(1000)); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTest.persist(commitBuilder); + + assertEquals(1, commitBuilder.getWatermarkHoldsCount()); + + Windmill.WatermarkHold watermarkHold = commitBuilder.getWatermarkHolds(0); + assertEquals(key(NAMESPACE, "watermark"), watermarkHold.getTag()); + assertEquals(TimeUnit.MILLISECONDS.toMicros(1000), watermarkHold.getTimestamps(0)); + + Mockito.verifyNoInteractions(mockReader); + + assertBuildable(commitBuilder); + } + @Test public void testWatermarkPersistEarliest() throws Exception { StateTag addr = diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/state/WatermarkHoldState.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/state/WatermarkHoldState.java index 6d4183da101f..f8b09bcf03a6 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/state/WatermarkHoldState.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/state/WatermarkHoldState.java @@ -38,4 +38,14 @@ public interface WatermarkHoldState extends GroupingState { @Override WatermarkHoldState readLater(); + + /** + * For internal use only; no backwards-compatibility guarantees. + * + *

    Permit marking the state as empty locally, without necessarily clearing it in the backend. + * + *

    This may be used by runners to optimize out unnecessary state reads. + */ + @Internal + default void setKnownEmpty() {} } From f30ae220c67cb44f6b5e633ad36796fabd3e274d Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 20 May 2026 12:06:07 -0400 Subject: [PATCH 192/490] Disable grpc fork support on some test suites. (#38566) * Disable grpc fork support in some test suite. * Disable GRPC fork support. * Add TODOs --- .../runners/portability/portable_runner_test.py | 17 +++++++++++++++++ .../apache_beam/utils/subprocess_server_test.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/sdks/python/apache_beam/runners/portability/portable_runner_test.py b/sdks/python/apache_beam/runners/portability/portable_runner_test.py index 0f44afb2f123..a1ed7448f050 100644 --- a/sdks/python/apache_beam/runners/portability/portable_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/portable_runner_test.py @@ -18,6 +18,7 @@ import inspect import logging +import os import socket import subprocess import sys @@ -328,6 +329,22 @@ class PortableRunnerTestWithSubprocessesAndMultiWorkers( PortableRunnerTestWithSubprocesses): _use_subprocesses = True + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + @classmethod + def setUpClass(cls): + cls._old_fork_support = os.environ.get('GRPC_ENABLE_FORK_SUPPORT') + os.environ['GRPC_ENABLE_FORK_SUPPORT'] = 'false' + super().setUpClass() + + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + @classmethod + def tearDownClass(cls): + if cls._old_fork_support is None: + os.environ.pop('GRPC_ENABLE_FORK_SUPPORT', None) + else: + os.environ['GRPC_ENABLE_FORK_SUPPORT'] = cls._old_fork_support + super().tearDownClass() + def create_options(self): options = super() \ .create_options() diff --git a/sdks/python/apache_beam/utils/subprocess_server_test.py b/sdks/python/apache_beam/utils/subprocess_server_test.py index efd357c4c136..073b8b3bcbe8 100644 --- a/sdks/python/apache_beam/utils/subprocess_server_test.py +++ b/sdks/python/apache_beam/utils/subprocess_server_test.py @@ -37,6 +37,23 @@ class JavaJarServerTest(unittest.TestCase): + + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + @classmethod + def setUpClass(cls): + cls._old_fork_support = os.environ.get('GRPC_ENABLE_FORK_SUPPORT') + os.environ['GRPC_ENABLE_FORK_SUPPORT'] = 'false' + super().setUpClass() + + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + @classmethod + def tearDownClass(cls): + if cls._old_fork_support is None: + os.environ.pop('GRPC_ENABLE_FORK_SUPPORT', None) + else: + os.environ['GRPC_ENABLE_FORK_SUPPORT'] = cls._old_fork_support + super().tearDownClass() + def test_gradle_jar_release(self): self.assertEqual( 'https://repo.maven.apache.org/maven2/org/apache/beam/' From 5924255ae5e57e2342ff46b8528b0776f9420c9a Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 20 May 2026 12:16:33 -0400 Subject: [PATCH 193/490] Fix race condition in UserPipelineTracker.clear() and various problems (#38537) * Fix race condition in UserPipelineTracker.clear() * Address comments. * Fix lints. * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Fix failed test RecordingTest.test_describe * Fix failed tests test_instrument_example_pipeline_to_write_cache and test_instrument_example_pipeline_to_read_cache. * Formatting. * Fix InteractiveBeamTest.test_recordings_clear and test_recordings_record. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../interactive/interactive_environment.py | 10 +- .../interactive/user_pipeline_tracker.py | 109 ++++++++++-------- .../interactive/user_pipeline_tracker_test.py | 48 ++++++++ 3 files changed, 119 insertions(+), 48 deletions(-) diff --git a/sdks/python/apache_beam/runners/interactive/interactive_environment.py b/sdks/python/apache_beam/runners/interactive/interactive_environment.py index bfb1a7f11905..b243d20ff857 100644 --- a/sdks/python/apache_beam/runners/interactive/interactive_environment.py +++ b/sdks/python/apache_beam/runners/interactive/interactive_environment.py @@ -365,11 +365,17 @@ def set_cache_manager(self, cache_manager, pipeline): if self.get_cache_manager(pipeline) is cache_manager: # NOOP if setting to the same cache_manager. return + # Check if the pipeline is already tracked as a user pipeline before cleanup. + is_user_pipeline = self._tracked_user_pipelines.get_user_pipeline( + pipeline) is pipeline if self.get_cache_manager(pipeline): # Invoke cleanup routine when a new cache_manager is forcefully set and # current cache_manager is not None. self.cleanup(pipeline) self._cache_managers[str(id(pipeline))] = cache_manager + if is_user_pipeline: + # Re-track the user pipeline because the self.cleanup() call above evicts it. + self.add_user_pipeline(pipeline) def get_cache_manager(self, pipeline, create_if_absent=False): """Gets the cache manager held by current Interactive Environment for the @@ -468,8 +474,8 @@ def evict_recording_manager(self, pipeline): def describe_all_recordings(self): """Returns a description of the recording for all watched pipelnes.""" return { - self.pipeline_id_to_pipeline(pid): rm.describe() - for pid, rm in self._recording_managers.items() + rm.user_pipeline: rm.describe() + for rm in self._recording_managers.values() } def set_pipeline_result(self, pipeline, result): diff --git a/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker.py b/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker.py index 53ee54ac8a35..4c7871c02bed 100644 --- a/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker.py +++ b/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker.py @@ -25,6 +25,7 @@ """ import shutil +import threading from typing import Iterator from typing import Optional @@ -39,13 +40,16 @@ class UserPipelineTracker: derived pipelines. """ def __init__(self): + self._lock = threading.RLock() self._user_pipelines: dict[beam.Pipeline, list[beam.Pipeline]] = {} - self._derived_pipelines: dict[beam.Pipeline] = {} - self._pid_to_pipelines: dict[beam.Pipeline] = {} + self._derived_pipelines: dict[beam.Pipeline, beam.Pipeline] = {} + self._pid_to_pipelines: dict[str, beam.Pipeline] = {} def __iter__(self) -> Iterator[beam.Pipeline]: """Iterates through all the user pipelines.""" - for p in self._user_pipelines: + with self._lock: + pipelines = list(self._user_pipelines.keys()) + for p in pipelines: yield p def _key(self, pipeline: beam.Pipeline) -> str: @@ -57,45 +61,57 @@ def evict(self, pipeline: beam.Pipeline) -> None: Removes the given pipeline and derived pipelines if a user pipeline. Otherwise, removes the given derived pipeline. """ - user_pipeline = self.get_user_pipeline(pipeline) - if user_pipeline: - for d in self._user_pipelines[user_pipeline]: - del self._derived_pipelines[d] - del self._user_pipelines[user_pipeline] - elif pipeline in self._derived_pipelines: - del self._derived_pipelines[pipeline] + with self._lock: + if pipeline in self._user_pipelines: + for d in self._user_pipelines[pipeline]: + self._derived_pipelines.pop(d, None) + self._pid_to_pipelines.pop(self._key(d), None) + self._user_pipelines.pop(pipeline, None) + elif pipeline in self._derived_pipelines: + user_pipeline = self._derived_pipelines.pop(pipeline, None) + if user_pipeline in self._user_pipelines: + try: + self._user_pipelines[user_pipeline].remove(pipeline) + except ValueError: + pass + self._pid_to_pipelines.pop(self._key(pipeline), None) def clear(self) -> None: """Clears the tracker of all user and derived pipelines.""" # Remove all local_tempdir of created pipelines. - for p in self._pid_to_pipelines.values(): - shutil.rmtree(p.local_tempdir, ignore_errors=True) + with self._lock: + pipelines = list(self._pid_to_pipelines.values()) + self._user_pipelines.clear() + self._derived_pipelines.clear() + self._pid_to_pipelines.clear() - self._user_pipelines.clear() - self._derived_pipelines.clear() - self._pid_to_pipelines.clear() + for p in pipelines: + shutil.rmtree(p.local_tempdir, ignore_errors=True) def get_pipeline(self, pid: str) -> Optional[beam.Pipeline]: """Returns the pipeline corresponding to the given pipeline id.""" - return self._pid_to_pipelines.get(pid, None) + with self._lock: + return self._pid_to_pipelines.get(pid, None) def add_user_pipeline(self, p: beam.Pipeline) -> beam.Pipeline: """Adds a user pipeline with an empty set of derived pipelines.""" - self._memoize_pipieline(p) + with self._lock: + self._memoize_pipeline(p) - # Create a new node for the user pipeline if it doesn't exist already. - user_pipeline = self.get_user_pipeline(p) - if not user_pipeline: - user_pipeline = p - self._user_pipelines[p] = [] + # Create a new node for the user pipeline if it doesn't exist already. + user_pipeline = self.get_user_pipeline(p) + if not user_pipeline: + user_pipeline = p + self._user_pipelines[p] = [] - return user_pipeline + return user_pipeline - def _memoize_pipieline(self, p: beam.Pipeline) -> None: + def _memoize_pipeline(self, p: beam.Pipeline) -> None: """Memoizes the pid of the pipeline to the pipeline object.""" pid = self._key(p) - if pid not in self._pid_to_pipelines: - self._pid_to_pipelines[pid] = p + with self._lock: + if pid not in self._pid_to_pipelines: + self._pid_to_pipelines[pid] = p def add_derived_pipeline( self, maybe_user_pipeline: beam.Pipeline, @@ -119,20 +135,21 @@ def add_derived_pipeline( # Returns p. ut.get_user_pipeline(derived2) """ - self._memoize_pipieline(maybe_user_pipeline) - self._memoize_pipieline(derived_pipeline) + with self._lock: + self._memoize_pipeline(maybe_user_pipeline) + self._memoize_pipeline(derived_pipeline) - # Cannot add a derived pipeline twice. - assert derived_pipeline not in self._derived_pipelines + # Cannot add a derived pipeline twice. + assert derived_pipeline not in self._derived_pipelines - # Get the "true" user pipeline. This allows for the user to derive a - # pipeline from another derived pipeline, use both as arguments, and this - # method will still get the correct user pipeline. - user = self.add_user_pipeline(maybe_user_pipeline) + # Get the "true" user pipeline. This allows for the user to derive a + # pipeline from another derived pipeline, use both as arguments, and this + # method will still get the correct user pipeline. + user = self.add_user_pipeline(maybe_user_pipeline) - # Map the derived pipeline to the user pipeline. - self._derived_pipelines[derived_pipeline] = user - self._user_pipelines[user].append(derived_pipeline) + # Map the derived pipeline to the user pipeline. + self._derived_pipelines[derived_pipeline] = user + self._user_pipelines[user].append(derived_pipeline) def get_user_pipeline(self, p: beam.Pipeline) -> Optional[beam.Pipeline]: """Returns the user pipeline of the given pipeline. @@ -142,14 +159,14 @@ def get_user_pipeline(self, p: beam.Pipeline) -> Optional[beam.Pipeline]: returns the same pipeline. If the given pipeline is a derived pipeline then this returns the user pipeline. """ + with self._lock: + # If `p` is a user pipeline then return it. + if p in self._user_pipelines: + return p - # If `p` is a user pipeline then return it. - if p in self._user_pipelines: - return p - - # If `p` exists then return its user pipeline. - if p in self._derived_pipelines: - return self._derived_pipelines[p] + # If `p` exists then return its user pipeline. + if p in self._derived_pipelines: + return self._derived_pipelines[p] - # Otherwise, `p` is not in this tracker. - return None + # Otherwise, `p` is not in this tracker. + return None diff --git a/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker_test.py b/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker_test.py index f7025b8b75bf..6fb8e4dbad99 100644 --- a/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker_test.py +++ b/sdks/python/apache_beam/runners/interactive/user_pipeline_tracker_test.py @@ -15,7 +15,9 @@ # limitations under the License. # +import threading import unittest +from unittest.mock import patch import apache_beam as beam from apache_beam.runners.interactive.user_pipeline_tracker import UserPipelineTracker @@ -202,6 +204,52 @@ def test_can_evict_user_pipeline(self): self.assertIs(user2, ut.get_user_pipeline(derived21)) self.assertIs(user2, ut.get_user_pipeline(derived22)) + def test_clear_race_condition(self): + ut = UserPipelineTracker() + # Add a pipeline so clear() has at least one element to iterate over. + p1 = beam.Pipeline() + derived1 = beam.Pipeline() + ut.add_derived_pipeline(p1, derived1) + + # Set by the mock when clear() enters its loop. Signals the background + # worker to mutate. + in_loop_event = threading.Event() + # Set by the worker when mutation is complete. Signals mock that it can + # safely resume clear(). + mutate_done_event = threading.Event() + + def mock_rmtree(path, ignore_errors=False): + # Signal the worker that clear() is iterating. + in_loop_event.set() + # Pause here to give the worker thread time to perform the mutation. + mutate_done_event.wait(timeout=5) + + def worker(): + # Wait for clear() to start iterating. + if in_loop_event.wait(timeout=5): + # Concurrently mutate the tracker dictionary. + p2 = beam.Pipeline() + derived2 = beam.Pipeline() + try: + ut.add_derived_pipeline(p2, derived2) + finally: + # Resume the main thread. + mutate_done_event.set() + + thread = threading.Thread(target=worker) + thread.start() + + try: + # Intercept shutil.rmtree inside clear() to orchestrate the concurrent + # mutation. + with patch('shutil.rmtree', side_effect=mock_rmtree): + ut.clear() + finally: + # Avoid hanging tests if events are missed. + in_loop_event.set() + mutate_done_event.set() + thread.join() + if __name__ == '__main__': unittest.main() From fc74ca2ddd49c3ac3c3cde29dd315b68170d5c3e Mon Sep 17 00:00:00 2001 From: Sachin Ranjalkar <52783123+sachinnn99@users.noreply.github.com> Date: Wed, 20 May 2026 22:23:54 +0530 Subject: [PATCH 194/490] [Java SDK] Infer Beam logical types for JSR-310 and UUID fields (#38194) Add schema inference for java.time.LocalDate, LocalTime, LocalDateTime, Instant, and UUID as Beam logical types (SqlTypes.DATE, TIME, DATETIME, NanosInstant, SqlTypes.UUID) in both POJO and JavaBean schemas. This enables Beam Rows produced from POJOs with JSR-310 fields to be schema-assignable to rows from external systems (e.g. IcebergIO) that use the same logical types, fixing the incompatibility reported in #37524. The Avro extension overrides the new convertLogicalType hook to preserve existing Joda bridging for java.time.Instant and LocalDate. --- .../sdk/schemas/utils/ByteBuddyUtils.java | 128 ++++++++++++++++++ .../schemas/utils/StaticSchemaInference.java | 19 ++- .../beam/sdk/schemas/JavaBeanSchemaTest.java | 83 ++++++++++++ .../beam/sdk/schemas/JavaFieldSchemaTest.java | 122 +++++++++++++++++ .../sdk/schemas/transforms/ConvertTest.java | 99 ++++++++++++++ .../sdk/schemas/utils/JavaBeanUtilsTest.java | 10 ++ .../beam/sdk/schemas/utils/POJOUtilsTest.java | 32 +++++ .../beam/sdk/schemas/utils/TestJavaBeans.java | 89 ++++++++++++ .../beam/sdk/schemas/utils/TestPOJOs.java | 118 ++++++++++++++++ .../avro/schemas/utils/AvroUtils.java | 87 +++++++----- 10 files changed, 751 insertions(+), 36 deletions(-) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/ByteBuddyUtils.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/ByteBuddyUtils.java index 832090926919..2b7bb1ca0df6 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/ByteBuddyUtils.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/ByteBuddyUtils.java @@ -28,6 +28,9 @@ import java.lang.reflect.Parameter; import java.lang.reflect.Type; import java.nio.ByteBuffer; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -38,6 +41,7 @@ import java.util.Optional; import java.util.Set; import java.util.SortedMap; +import java.util.UUID; import net.bytebuddy.ByteBuddy; import net.bytebuddy.NamingStrategy; import net.bytebuddy.NamingStrategy.SuffixingRandom.BaseNameResolver; @@ -78,6 +82,9 @@ import org.apache.beam.sdk.schemas.FieldValueHaver; import org.apache.beam.sdk.schemas.FieldValueSetter; import org.apache.beam.sdk.schemas.FieldValueTypeInformation; +import org.apache.beam.sdk.schemas.Schema.LogicalType; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.util.Preconditions; import org.apache.beam.sdk.util.common.ReflectHelpers; import org.apache.beam.sdk.values.TypeDescriptor; @@ -120,6 +127,65 @@ public class ByteBuddyUtils { private static final ForLoadedType ENUM_TYPE = new ForLoadedType(Enum.class); private static final ForLoadedType BYTE_BUDDY_UTILS_TYPE = new ForLoadedType(ByteBuddyUtils.class); + private static final ForLoadedType LOGICAL_TYPE_TYPE = new ForLoadedType(LogicalType.class); + + // Static LogicalType instances used by codegen for JSR-310 and UUID POJO/Bean fields. The + // generated bytecode loads these via FieldAccess and invokes toBaseType / toInputType, so the + // fields must be public so generated classes in user packages can access them. + // See logicalTypeFieldName(...) for the type → field name mapping. + public static final LogicalType JAVA_LOCAL_DATE_LOGICAL_TYPE = SqlTypes.DATE; + public static final LogicalType JAVA_LOCAL_TIME_LOGICAL_TYPE = SqlTypes.TIME; + public static final LogicalType + JAVA_LOCAL_DATE_TIME_LOGICAL_TYPE = SqlTypes.DATETIME; + public static final LogicalType + JAVA_INSTANT_LOGICAL_TYPE = new NanosInstant(); + public static final LogicalType JAVA_UUID_LOGICAL_TYPE = + SqlTypes.UUID; + + /** + * Returns the {@link Schema.LogicalType} that {@link StaticSchemaInference} infers for the given + * Java raw type, or {@code null} if no JSR-310 / UUID inference applies. + */ + static @Nullable LogicalType inferredLogicalTypeFor(Class rawType) { + if (LocalDate.class.equals(rawType)) { + return JAVA_LOCAL_DATE_LOGICAL_TYPE; + } else if (LocalTime.class.equals(rawType)) { + return JAVA_LOCAL_TIME_LOGICAL_TYPE; + } else if (LocalDateTime.class.equals(rawType)) { + return JAVA_LOCAL_DATE_TIME_LOGICAL_TYPE; + } else if (java.time.Instant.class.equals(rawType)) { + return JAVA_INSTANT_LOGICAL_TYPE; + } else if (UUID.class.equals(rawType)) { + return JAVA_UUID_LOGICAL_TYPE; + } + return null; + } + + /** Maps a Java raw type to the static field name in {@link ByteBuddyUtils} that holds it. */ + private static String logicalTypeFieldName(Class rawType) { + if (LocalDate.class.equals(rawType)) { + return "JAVA_LOCAL_DATE_LOGICAL_TYPE"; + } else if (LocalTime.class.equals(rawType)) { + return "JAVA_LOCAL_TIME_LOGICAL_TYPE"; + } else if (LocalDateTime.class.equals(rawType)) { + return "JAVA_LOCAL_DATE_TIME_LOGICAL_TYPE"; + } else if (java.time.Instant.class.equals(rawType)) { + return "JAVA_INSTANT_LOGICAL_TYPE"; + } else if (UUID.class.equals(rawType)) { + return "JAVA_UUID_LOGICAL_TYPE"; + } + throw new IllegalArgumentException("Not an inferred logical type: " + rawType); + } + + /** Stack manipulation that pushes the static {@link LogicalType} for the given Java type. */ + private static StackManipulation loadLogicalType(Class rawType) { + return FieldAccess.forField( + BYTE_BUDDY_UTILS_TYPE + .getDeclaredFields() + .filter(ElementMatchers.named(logicalTypeFieldName(rawType))) + .getOnly()) + .read(); + } /** * A naming strategy for ByteBuddy classes. @@ -286,6 +352,8 @@ public T convert(TypeDescriptor typeDescriptor) { return convertDateTime(typeDescriptor); } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ReadablePartial.class))) { return convertDateTime(typeDescriptor); + } else if (inferredLogicalTypeFor(typeDescriptor.getRawType()) != null) { + return convertLogicalType(typeDescriptor); } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ByteBuffer.class))) { return convertByteBuffer(typeDescriptor); } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(CharSequence.class))) { @@ -324,6 +392,14 @@ protected StackManipulation shortCircuitReturnNull( protected abstract T convertDateTime(TypeDescriptor type); + /** + * Handles JSR-310 ({@link LocalDate}, {@link LocalTime}, {@link LocalDateTime}, {@link + * java.time.Instant}) and {@link UUID} fields, which {@link StaticSchemaInference} infers as + * Beam {@link LogicalType}s. Subclasses emit code that round-trips through the corresponding + * static {@link LogicalType} instance ({@link #JAVA_LOCAL_DATE_LOGICAL_TYPE} etc.). + */ + protected abstract T convertLogicalType(TypeDescriptor type); + protected abstract T convertByteBuffer(TypeDescriptor type); protected abstract T convertCharSequence(TypeDescriptor type); @@ -401,6 +477,15 @@ protected Type convertDateTime(TypeDescriptor type) { return Instant.class; } + @Override + protected Type convertLogicalType(TypeDescriptor type) { + // The codegen-generated getter returns the LogicalType's base value (Long for + // Date/Time, Row for DateTime/NanosInstant/UUID). Object.class is a safe upper bound + // for the FieldValueGetter signature; the framework's GetLogicalInputType wrapper + // converts back to the input type before exposing the value to user code. + return Object.class; + } + @Override protected Type convertByteBuffer(TypeDescriptor type) { return byte[].class; @@ -915,6 +1000,26 @@ protected StackManipulation convertDateTime(TypeDescriptor type) { return new ShortCircuitReturnNull(readValue, stackManipulation); } + @Override + protected StackManipulation convertLogicalType(TypeDescriptor type) { + // Equivalent code: return STATIC_LOGICAL_TYPE.toBaseType(value); + // where STATIC_LOGICAL_TYPE is one of the JAVA_*_LOGICAL_TYPE static fields on + // ByteBuddyUtils. The base type is Long (for LocalDate, LocalTime) or Row (for the + // others); both are reference types so no boxing/casting is needed beyond the invoke. + StackManipulation stackManipulation = + new Compound( + loadLogicalType(type.getRawType()), + readValue, + MethodInvocation.invoke( + LOGICAL_TYPE_TYPE + .getDeclaredMethods() + .filter( + ElementMatchers.named("toBaseType") + .and(ElementMatchers.takesArguments(1))) + .getOnly())); + return new ShortCircuitReturnNull(readValue, stackManipulation); + } + @Override protected StackManipulation convertByteBuffer(TypeDescriptor type) { // Generate the following code: @@ -1361,6 +1466,29 @@ protected StackManipulation convertEnum(TypeDescriptor type) { return new ShortCircuitReturnNull(readValue, stackManipulation); } + @Override + protected StackManipulation convertLogicalType(TypeDescriptor type) { + // Equivalent code: return (JavaType) STATIC_LOGICAL_TYPE.toInputType(value); + // FromRowUsingCreator already converted the row's input-type value (e.g. LocalDate) + // to the LogicalType's base value (e.g. Long) before invoking the generated creator, + // so we receive the base type here and need to project back to the POJO field's + // Java type. + ForLoadedType loadedType = new ForLoadedType(type.getRawType()); + StackManipulation stackManipulation = + new Compound( + loadLogicalType(type.getRawType()), + readValue, + MethodInvocation.invoke( + LOGICAL_TYPE_TYPE + .getDeclaredMethods() + .filter( + ElementMatchers.named("toInputType") + .and(ElementMatchers.takesArguments(1))) + .getOnly()), + TypeCasting.to(loadedType)); + return new ShortCircuitReturnNull(readValue, stackManipulation); + } + @Override protected StackManipulation convertDefault(TypeDescriptor type) { return readValue; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/StaticSchemaInference.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/StaticSchemaInference.java index 196ee6f86593..ffd07071679d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/StaticSchemaInference.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/StaticSchemaInference.java @@ -22,17 +22,23 @@ import java.lang.reflect.ParameterizedType; import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.beam.sdk.schemas.FieldValueTypeInformation; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.joda.time.ReadableInstant; @@ -127,7 +133,8 @@ public static Schema.FieldType fieldFromType( return fieldFromType(type, fieldValueTypeSupplier, new HashMap<>()); } - // TODO(https://github.com/apache/beam/issues/21567): support type inference for logical types + // TODO(https://github.com/apache/beam/issues/21567): support inference for additional/custom + // logical types private static Schema.FieldType fieldFromType( TypeDescriptor type, FieldValueTypeSupplier fieldValueTypeSupplier, @@ -177,6 +184,16 @@ private static Schema.FieldType fieldFromType( return FieldType.STRING; } else if (type.isSubtypeOf(TypeDescriptor.of(ReadableInstant.class))) { return FieldType.DATETIME; + } else if (type.getRawType().equals(LocalDate.class)) { + return FieldType.logicalType(SqlTypes.DATE); + } else if (type.getRawType().equals(LocalTime.class)) { + return FieldType.logicalType(SqlTypes.TIME); + } else if (type.getRawType().equals(LocalDateTime.class)) { + return FieldType.logicalType(SqlTypes.DATETIME); + } else if (type.getRawType().equals(java.time.Instant.class)) { + return FieldType.logicalType(new NanosInstant()); + } else if (type.getRawType().equals(UUID.class)) { + return FieldType.logicalType(SqlTypes.UUID); } else if (type.isSubtypeOf(TypeDescriptor.of(ByteBuffer.class))) { return FieldType.BYTES; } else if (type.isSubtypeOf(TypeDescriptor.of(Iterable.class))) { diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java index df6c9cf18e5a..de0953a0a088 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java @@ -24,6 +24,7 @@ import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.CASE_FORMAT_BEAM_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.FIELD_WITH_DESCRIPTION_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.ITERABLE_BEAM_SCHEMA; +import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.JAVA_TIME_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_ARRAYS_BEAM_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_ARRAY_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_BEAN_SCHEMA; @@ -46,9 +47,15 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.UUID; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.schemas.utils.SchemaTestUtils; import org.apache.beam.sdk.schemas.utils.TestJavaBeans; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.AllNullableBean; @@ -57,6 +64,7 @@ import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithNoCreateOption; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithRenamedFieldsAndSetters; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.IterableBean; +import org.apache.beam.sdk.schemas.utils.TestJavaBeans.JavaTimeBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.MismatchingNullableBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedArrayBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedArraysBean; @@ -179,6 +187,81 @@ public void testFromRow() throws NoSuchSchemaException { assertEquals("stringbuilder", bean.getStringBuilder().toString()); } + @Test + public void testJavaTimeSchema() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema schema = registry.getSchema(JavaTimeBean.class); + SchemaTestUtils.assertSchemaEquivalent(JAVA_TIME_BEAN_SCHEMA, schema); + Schema icebergStyleSchema = + Schema.builder() + .addLogicalTypeField("localDate", SqlTypes.DATE) + .addLogicalTypeField("localTime", SqlTypes.TIME) + .addLogicalTypeField("localDateTime", SqlTypes.DATETIME) + .addLogicalTypeField("instant", new NanosInstant()) + .addLogicalTypeField("uuid", SqlTypes.UUID) + .build(); + assertTrue(schema.assignableToIgnoreNullable(icebergStyleSchema)); + } + + @Test + public void testJavaTimeToRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + JavaTimeBean bean = new JavaTimeBean(); + bean.setLocalDate(LocalDate.of(2024, 1, 15)); + bean.setLocalTime(LocalTime.of(10, 30, 45)); + bean.setLocalDateTime(LocalDateTime.of(2024, 1, 15, 10, 30, 45)); + bean.setInstant(java.time.Instant.ofEpochSecond(1_705_315_845L, 123_456_789L)); + bean.setUuid(UUID.fromString("11111111-2222-3333-4444-555555555555")); + + Row row = registry.getToRowFunction(JavaTimeBean.class).apply(bean); + + assertEquals(5, row.getFieldCount()); + assertEquals(bean.getLocalDate(), row.getLogicalTypeValue("localDate", LocalDate.class)); + assertEquals(bean.getLocalTime(), row.getLogicalTypeValue("localTime", LocalTime.class)); + assertEquals( + bean.getLocalDateTime(), row.getLogicalTypeValue("localDateTime", LocalDateTime.class)); + assertEquals(bean.getInstant(), row.getLogicalTypeValue("instant", java.time.Instant.class)); + assertEquals(bean.getUuid(), row.getLogicalTypeValue("uuid", UUID.class)); + } + + @Test + public void testJavaTimeFromRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + LocalDate localDate = LocalDate.of(2024, 1, 15); + LocalTime localTime = LocalTime.of(10, 30, 45); + LocalDateTime localDateTime = LocalDateTime.of(2024, 1, 15, 10, 30, 45); + java.time.Instant instant = java.time.Instant.ofEpochSecond(1_705_315_845L, 123_456_789L); + UUID uuid = UUID.fromString("11111111-2222-3333-4444-555555555555"); + Row row = + Row.withSchema(JAVA_TIME_BEAN_SCHEMA) + .addValues(localDate, localTime, localDateTime, instant, uuid) + .build(); + + JavaTimeBean bean = registry.getFromRowFunction(JavaTimeBean.class).apply(row); + + assertEquals(localDate, bean.getLocalDate()); + assertEquals(localTime, bean.getLocalTime()); + assertEquals(localDateTime, bean.getLocalDateTime()); + assertEquals(instant, bean.getInstant()); + assertEquals(uuid, bean.getUuid()); + } + + @Test + public void testJavaTimeRoundTrip() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + JavaTimeBean original = new JavaTimeBean(); + original.setLocalDate(LocalDate.of(2024, 1, 15)); + original.setLocalTime(LocalTime.of(10, 30, 45)); + original.setLocalDateTime(LocalDateTime.of(2024, 1, 15, 10, 30, 45)); + original.setInstant(java.time.Instant.ofEpochSecond(1_705_315_845L, 123_456_789L)); + original.setUuid(UUID.fromString("11111111-2222-3333-4444-555555555555")); + + Row row = registry.getToRowFunction(JavaTimeBean.class).apply(original); + JavaTimeBean roundTripped = registry.getFromRowFunction(JavaTimeBean.class).apply(row); + + assertEquals(original, roundTripped); + } + @Test public void testNullableToRow() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java index 7a66decde017..c80b758adc31 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java @@ -21,12 +21,14 @@ import static org.apache.beam.sdk.schemas.utils.TestPOJOs.ANNOTATED_SIMPLE_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.CASE_FORMAT_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.ENUMERATION; +import static org.apache.beam.sdk.schemas.utils.TestPOJOs.JAVA_TIME_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_ARRAYS_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_ARRAY_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_MAP_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_NULLABLE_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NULLABLES_SCHEMA; +import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NULLABLE_JAVA_TIME_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NULLABLE_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.POJO_WITH_ENUM_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.POJO_WITH_ITERABLE; @@ -46,20 +48,28 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.UUID; import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.schemas.utils.SchemaTestUtils; import org.apache.beam.sdk.schemas.utils.TestPOJOs; import org.apache.beam.sdk.schemas.utils.TestPOJOs.AnnotatedSimplePojo; import org.apache.beam.sdk.schemas.utils.TestPOJOs.FirstCircularNestedPOJO; +import org.apache.beam.sdk.schemas.utils.TestPOJOs.JavaTimePOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedArrayPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedArraysPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedMapPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedPOJO; +import org.apache.beam.sdk.schemas.utils.TestPOJOs.NullableJavaTimePOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NullablePOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.POJOWithNestedNullable; import org.apache.beam.sdk.schemas.utils.TestPOJOs.POJOWithNullables; @@ -228,6 +238,118 @@ public void testFromRow() throws NoSuchSchemaException { assertEquals("stringbuilder", pojo.stringBuilder.toString()); } + @Test + public void testJavaTimeSchema() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema schema = registry.getSchema(JavaTimePOJO.class); + SchemaTestUtils.assertSchemaEquivalent(JAVA_TIME_POJO_SCHEMA, schema); + // Reproduces the failure mode in #37524: a POJO inferred from JSR-310 fields must be + // assignable to a row schema (e.g. one produced by IcebergIO) that uses the same logical + // types. + Schema icebergStyleSchema = + Schema.builder() + .addLogicalTypeField("localDate", SqlTypes.DATE) + .addLogicalTypeField("localTime", SqlTypes.TIME) + .addLogicalTypeField("localDateTime", SqlTypes.DATETIME) + .addLogicalTypeField("instant", new NanosInstant()) + .addLogicalTypeField("uuid", SqlTypes.UUID) + .build(); + assertTrue(schema.assignableToIgnoreNullable(icebergStyleSchema)); + } + + @Test + public void testJavaTimeToRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + LocalDate localDate = LocalDate.of(2024, 1, 15); + LocalTime localTime = LocalTime.of(10, 30, 45); + LocalDateTime localDateTime = LocalDateTime.of(2024, 1, 15, 10, 30, 45); + java.time.Instant instant = java.time.Instant.ofEpochSecond(1_705_315_845L, 123_456_789L); + UUID uuid = UUID.fromString("11111111-2222-3333-4444-555555555555"); + JavaTimePOJO pojo = new JavaTimePOJO(localDate, localTime, localDateTime, instant, uuid); + + Row row = registry.getToRowFunction(JavaTimePOJO.class).apply(pojo); + + assertEquals(5, row.getFieldCount()); + assertEquals(localDate, row.getLogicalTypeValue("localDate", LocalDate.class)); + assertEquals(localTime, row.getLogicalTypeValue("localTime", LocalTime.class)); + assertEquals(localDateTime, row.getLogicalTypeValue("localDateTime", LocalDateTime.class)); + assertEquals(instant, row.getLogicalTypeValue("instant", java.time.Instant.class)); + assertEquals(uuid, row.getLogicalTypeValue("uuid", UUID.class)); + } + + @Test + public void testJavaTimeFromRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + LocalDate localDate = LocalDate.of(2024, 1, 15); + LocalTime localTime = LocalTime.of(10, 30, 45); + LocalDateTime localDateTime = LocalDateTime.of(2024, 1, 15, 10, 30, 45); + java.time.Instant instant = java.time.Instant.ofEpochSecond(1_705_315_845L, 123_456_789L); + UUID uuid = UUID.fromString("11111111-2222-3333-4444-555555555555"); + Row row = + Row.withSchema(JAVA_TIME_POJO_SCHEMA) + .addValues(localDate, localTime, localDateTime, instant, uuid) + .build(); + + JavaTimePOJO pojo = registry.getFromRowFunction(JavaTimePOJO.class).apply(row); + + assertEquals(localDate, pojo.localDate); + assertEquals(localTime, pojo.localTime); + assertEquals(localDateTime, pojo.localDateTime); + assertEquals(instant, pojo.instant); + assertEquals(uuid, pojo.uuid); + } + + @Test + public void testJavaTimeRoundTrip() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + JavaTimePOJO original = + new JavaTimePOJO( + LocalDate.of(2024, 1, 15), + LocalTime.of(10, 30, 45), + LocalDateTime.of(2024, 1, 15, 10, 30, 45), + java.time.Instant.ofEpochSecond(1_705_315_845L, 123_456_789L), + UUID.fromString("11111111-2222-3333-4444-555555555555")); + + Row row = registry.getToRowFunction(JavaTimePOJO.class).apply(original); + JavaTimePOJO roundTripped = registry.getFromRowFunction(JavaTimePOJO.class).apply(row); + + assertEquals(original, roundTripped); + } + + @Test + public void testNullableJavaTimeSchema() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema schema = registry.getSchema(NullableJavaTimePOJO.class); + SchemaTestUtils.assertSchemaEquivalent(NULLABLE_JAVA_TIME_POJO_SCHEMA, schema); + } + + @Test + public void testNullableJavaTimeToRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + NullableJavaTimePOJO pojo = new NullableJavaTimePOJO(); + Row row = registry.getToRowFunction(NullableJavaTimePOJO.class).apply(pojo); + + assertEquals(5, row.getFieldCount()); + assertNull(row.getLogicalTypeValue("localDate", LocalDate.class)); + assertNull(row.getLogicalTypeValue("localTime", LocalTime.class)); + assertNull(row.getLogicalTypeValue("localDateTime", LocalDateTime.class)); + assertNull(row.getLogicalTypeValue("instant", java.time.Instant.class)); + assertNull(row.getLogicalTypeValue("uuid", UUID.class)); + } + + @Test + public void testNullableJavaTimeFromRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = Row.nullRow(NULLABLE_JAVA_TIME_POJO_SCHEMA); + + NullableJavaTimePOJO pojo = registry.getFromRowFunction(NullableJavaTimePOJO.class).apply(row); + assertNull(pojo.localDate); + assertNull(pojo.localTime); + assertNull(pojo.localDateTime); + assertNull(pojo.instant); + assertNull(pojo.uuid); + } + @Test public void testNullableSchema() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java index 93d6984d47e2..c206bd8f61fd 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas.transforms; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -24,6 +25,7 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.testing.NeedsRunner; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; @@ -211,6 +213,103 @@ public void testFromRows() { pipeline.run(); } + /** Reproducer for #37524: a Row with a logical-type DateTime field should convert to a POJO. */ + @DefaultSchema(JavaFieldSchema.class) + public static class TimePOJO { + public LocalDateTime time; + + public TimePOJO() {} + + public TimePOJO(LocalDateTime time) { + this.time = time; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TimePOJO)) { + return false; + } + TimePOJO that = (TimePOJO) o; + return Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(time); + } + } + + @Test + @Category(NeedsRunner.class) + public void testFromRowsWithLogicalTypeDateTime() { + Schema icebergStyleSchema = + Schema.builder().addLogicalTypeField("time", SqlTypes.DATETIME).build(); + LocalDateTime expected = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + Row inputRow = Row.withSchema(icebergStyleSchema).addValue(expected).build(); + + PCollection pojos = + pipeline + .apply(Create.of(inputRow).withRowSchema(icebergStyleSchema)) + .apply(Convert.fromRows(TimePOJO.class)); + + PAssert.that(pojos).containsInAnyOrder(new TimePOJO(expected)); + pipeline.run(); + } + + /** POJO mixing a logical-type field with a primitive field. */ + @DefaultSchema(JavaFieldSchema.class) + public static class MixedTimePOJO { + public LocalDateTime time; + public String name; + + public MixedTimePOJO() {} + + public MixedTimePOJO(LocalDateTime time, String name) { + this.time = time; + this.name = name; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MixedTimePOJO)) { + return false; + } + MixedTimePOJO that = (MixedTimePOJO) o; + return Objects.equals(time, that.time) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(time, name); + } + } + + @Test + @Category(NeedsRunner.class) + public void testFromRowsWithMixedLogicalAndPrimitiveTypes() { + Schema mixedSchema = + Schema.builder() + .addLogicalTypeField("time", SqlTypes.DATETIME) + .addStringField("name") + .build(); + LocalDateTime expectedTime = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + Row inputRow = Row.withSchema(mixedSchema).addValues(expectedTime, "hello").build(); + + PCollection pojos = + pipeline + .apply(Create.of(inputRow).withRowSchema(mixedSchema)) + .apply(Convert.fromRows(MixedTimePOJO.class)); + + PAssert.that(pojos).containsInAnyOrder(new MixedTimePOJO(expectedTime, "hello")); + pipeline.run(); + } + @Test @Category(NeedsRunner.class) public void testGeneralConvert() { diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java index 7e9cf9a894b9..57d4f905e6ec 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java @@ -19,6 +19,7 @@ import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.BEAN_WITH_BOXED_FIELDS_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.BEAN_WITH_BYTE_ARRAY_SCHEMA; +import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.JAVA_TIME_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_ARRAY_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_COLLECTION_BEAN_SCHEMA; @@ -44,6 +45,7 @@ import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.DefaultTypeConversionsFactory; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithBoxedFields; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithByteArray; +import org.apache.beam.sdk.schemas.utils.TestJavaBeans.JavaTimeBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedArrayBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedCollectionBean; @@ -79,6 +81,14 @@ public void testSimpleBean() { SchemaTestUtils.assertSchemaEquivalent(SIMPLE_BEAN_SCHEMA, schema); } + @Test + public void testJavaTimeBean() { + Schema schema = + JavaBeanUtils.schemaFromJavaBeanClass( + new TypeDescriptor() {}, GetterTypeSupplier.INSTANCE); + SchemaTestUtils.assertSchemaEquivalent(JAVA_TIME_BEAN_SCHEMA, schema); + } + @Test public void testNestedBean() { Schema schema = diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java index 6b9fbcd30a27..72407d965da4 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas.utils; +import static org.apache.beam.sdk.schemas.utils.TestPOJOs.JAVA_TIME_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_ARRAY_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_COLLECTION_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_MAP_POJO_SCHEMA; @@ -38,7 +39,11 @@ import org.apache.beam.sdk.schemas.FieldValueGetter; import org.apache.beam.sdk.schemas.JavaFieldSchema.JavaFieldTypeSupplier; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.schemas.Schema.TypeName; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.DefaultTypeConversionsFactory; +import org.apache.beam.sdk.schemas.utils.TestPOJOs.JavaTimePOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedArrayPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedCollectionPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedMapPOJO; @@ -82,6 +87,33 @@ public void testSimplePOJO() { assertEquals(SIMPLE_POJO_SCHEMA, schema); } + @Test + public void testJavaTimePOJO() { + Schema schema = + POJOUtils.schemaFromPojoClass( + new TypeDescriptor() {}, JavaFieldTypeSupplier.INSTANCE); + assertEquals(JAVA_TIME_POJO_SCHEMA, schema); + } + + /** + * Regression test for #37524: Joda {@link Instant} must continue to map to {@link + * FieldType#DATETIME}, not the new {@code java.time.Instant} logical type. This guards the branch + * ordering in {@code StaticSchemaInference.fieldFromType}. + */ + @Test + public void testJodaInstantStillInfersAsDatetime() { + FieldType jodaInferred = + StaticSchemaInference.fieldFromType( + TypeDescriptor.of(Instant.class), JavaFieldTypeSupplier.INSTANCE); + assertEquals(FieldType.DATETIME, jodaInferred); + + FieldType javaTimeInferred = + StaticSchemaInference.fieldFromType( + TypeDescriptor.of(java.time.Instant.class), JavaFieldTypeSupplier.INSTANCE); + assertEquals(TypeName.LOGICAL_TYPE, javaTimeInferred.getTypeName()); + assertEquals(NanosInstant.IDENTIFIER, javaTimeInferred.getLogicalType().getIdentifier()); + } + @Test public void testNestedPOJO() { Schema schema = diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java index 694db05b0918..d8ed86f2b552 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java @@ -19,10 +19,14 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import org.apache.beam.sdk.schemas.JavaBeanSchema; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; @@ -33,6 +37,8 @@ import org.apache.beam.sdk.schemas.annotations.SchemaFieldName; import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; import org.apache.beam.sdk.schemas.annotations.SchemaIgnore; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.CaseFormat; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.checkerframework.checker.nullness.qual.Nullable; @@ -1398,4 +1404,87 @@ public void setValue(@Nullable Float value) { Schema.Field.nullable("value", FieldType.FLOAT) .withDescription("This value is the value stored in the object as a float.")) .build(); + + /** A Bean containing JSR-310 date/time types and a UUID, all inferred as Beam logical types. */ + @DefaultSchema(JavaBeanSchema.class) + public static class JavaTimeBean { + private LocalDate localDate; + private LocalTime localTime; + private LocalDateTime localDateTime; + private java.time.Instant instant; + private UUID uuid; + + public JavaTimeBean() {} + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public java.time.Instant getInstant() { + return instant; + } + + public void setInstant(java.time.Instant instant) { + this.instant = instant; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof JavaTimeBean)) { + return false; + } + JavaTimeBean that = (JavaTimeBean) o; + return Objects.equals(localDate, that.localDate) + && Objects.equals(localTime, that.localTime) + && Objects.equals(localDateTime, that.localDateTime) + && Objects.equals(instant, that.instant) + && Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(localDate, localTime, localDateTime, instant, uuid); + } + } + + /** The schema for {@link JavaTimeBean}. */ + public static final Schema JAVA_TIME_BEAN_SCHEMA = + Schema.builder() + .addLogicalTypeField("localDate", SqlTypes.DATE) + .addLogicalTypeField("localTime", SqlTypes.TIME) + .addLogicalTypeField("localDateTime", SqlTypes.DATETIME) + .addLogicalTypeField("instant", new NanosInstant()) + .addLogicalTypeField("uuid", SqlTypes.UUID) + .build(); } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java index b82c4dc0e7e6..38ec507480f7 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java @@ -19,10 +19,14 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import org.apache.beam.sdk.schemas.JavaFieldSchema; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; @@ -34,6 +38,8 @@ import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; import org.apache.beam.sdk.schemas.annotations.SchemaIgnore; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.CaseFormat; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.checkerframework.checker.nullness.qual.Nullable; @@ -1281,4 +1287,116 @@ public int hashCode() { Schema.Field.nullable("str", FieldType.STRING) .withDescription("a simple string that is part of this field")) .build(); + + /** A POJO containing JSR-310 date/time types and a UUID, all inferred as Beam logical types. */ + @DefaultSchema(JavaFieldSchema.class) + public static class JavaTimePOJO { + public LocalDate localDate; + public LocalTime localTime; + public LocalDateTime localDateTime; + public java.time.Instant instant; + public UUID uuid; + + public JavaTimePOJO() {} + + public JavaTimePOJO( + LocalDate localDate, + LocalTime localTime, + LocalDateTime localDateTime, + java.time.Instant instant, + UUID uuid) { + this.localDate = localDate; + this.localTime = localTime; + this.localDateTime = localDateTime; + this.instant = instant; + this.uuid = uuid; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof JavaTimePOJO)) { + return false; + } + JavaTimePOJO that = (JavaTimePOJO) o; + return Objects.equals(localDate, that.localDate) + && Objects.equals(localTime, that.localTime) + && Objects.equals(localDateTime, that.localDateTime) + && Objects.equals(instant, that.instant) + && Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(localDate, localTime, localDateTime, instant, uuid); + } + } + + /** The schema for {@link JavaTimePOJO}. */ + public static final Schema JAVA_TIME_POJO_SCHEMA = + Schema.builder() + .addLogicalTypeField("localDate", SqlTypes.DATE) + .addLogicalTypeField("localTime", SqlTypes.TIME) + .addLogicalTypeField("localDateTime", SqlTypes.DATETIME) + .addLogicalTypeField("instant", new NanosInstant()) + .addLogicalTypeField("uuid", SqlTypes.UUID) + .build(); + + /** A POJO with nullable JSR-310 and UUID fields. */ + @DefaultSchema(JavaFieldSchema.class) + public static class NullableJavaTimePOJO { + public @Nullable LocalDate localDate; + public @Nullable LocalTime localTime; + public @Nullable LocalDateTime localDateTime; + public java.time.@Nullable Instant instant; + public @Nullable UUID uuid; + + public NullableJavaTimePOJO() {} + + public NullableJavaTimePOJO( + LocalDate localDate, + LocalTime localTime, + LocalDateTime localDateTime, + java.time.Instant instant, + UUID uuid) { + this.localDate = localDate; + this.localTime = localTime; + this.localDateTime = localDateTime; + this.instant = instant; + this.uuid = uuid; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NullableJavaTimePOJO)) { + return false; + } + NullableJavaTimePOJO that = (NullableJavaTimePOJO) o; + return Objects.equals(localDate, that.localDate) + && Objects.equals(localTime, that.localTime) + && Objects.equals(localDateTime, that.localDateTime) + && Objects.equals(instant, that.instant) + && Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(localDate, localTime, localDateTime, instant, uuid); + } + } + + /** The schema for {@link NullableJavaTimePOJO}. */ + public static final Schema NULLABLE_JAVA_TIME_POJO_SCHEMA = + Schema.builder() + .addNullableLogicalTypeField("localDate", SqlTypes.DATE) + .addNullableLogicalTypeField("localTime", SqlTypes.TIME) + .addNullableLogicalTypeField("localDateTime", SqlTypes.DATETIME) + .addNullableLogicalTypeField("instant", new NanosInstant()) + .addNullableLogicalTypeField("uuid", SqlTypes.UUID) + .build(); } diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java index 99eb7f961901..854b0d3c8bb0 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java @@ -335,15 +335,20 @@ public AvroConvertType(boolean returnRawType) { } @Override - protected java.lang.reflect.Type convertDefault(TypeDescriptor type) { + protected java.lang.reflect.Type convertLogicalType(TypeDescriptor type) { if (type.isSubtypeOf(TypeDescriptor.of(java.time.Instant.class)) || type.isSubtypeOf(TypeDescriptor.of(java.time.LocalDate.class))) { return convertDateTime(type); - } else if (type.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { + } + return super.convertLogicalType(type); + } + + @Override + protected java.lang.reflect.Type convertDefault(TypeDescriptor type) { + if (type.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { return byte[].class; - } else { - return super.convertDefault(type); } + return super.convertDefault(type); } } @@ -358,18 +363,8 @@ protected TypeConversionsFactory getFactory() { } @Override - protected StackManipulation convertDefault(TypeDescriptor type) { - if (type.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { - // Generate the following code: - // return value.bytes(); - return new Compound( - readValue, - MethodInvocation.invoke( - new ForLoadedType(GenericFixed.class) - .getDeclaredMethods() - .filter(ElementMatchers.named("bytes").and(ElementMatchers.returns(BYTES))) - .getOnly())); - } else if (java.time.Instant.class.isAssignableFrom(type.getRawType())) { + protected StackManipulation convertLogicalType(TypeDescriptor type) { + if (java.time.Instant.class.isAssignableFrom(type.getRawType())) { // Generates the following code: // return Instant.ofEpochMilli(value.toEpochMilli()) StackManipulation onNotNull = @@ -408,6 +403,22 @@ protected StackManipulation convertDefault(TypeDescriptor type) { .getOnly())); return shortCircuitReturnNull(readValue, onNotNull); } + return super.convertLogicalType(type); + } + + @Override + protected StackManipulation convertDefault(TypeDescriptor type) { + if (type.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { + // Generate the following code: + // return value.bytes(); + return new Compound( + readValue, + MethodInvocation.invoke( + new ForLoadedType(GenericFixed.class) + .getDeclaredMethods() + .filter(ElementMatchers.named("bytes").and(ElementMatchers.returns(BYTES))) + .getOnly())); + } return super.convertDefault(type); } } @@ -423,25 +434,8 @@ protected TypeConversionsFactory getFactory() { } @Override - protected StackManipulation convertDefault(TypeDescriptor type) { - if (type.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { - // Generate the following code: - // return new T((byte[]) value); - ForLoadedType loadedType = new ForLoadedType(type.getRawType()); - return new Compound( - TypeCreation.of(loadedType), - Duplication.SINGLE, - // Load the parameter and cast it to a byte[]. - readValue, - TypeCasting.to(BYTES), - // Create a new instance that wraps this byte[]. - MethodInvocation.invoke( - loadedType - .getDeclaredMethods() - .filter( - ElementMatchers.isConstructor().and(ElementMatchers.takesArguments(BYTES))) - .getOnly())); - } else if (java.time.Instant.class.isAssignableFrom(type.getRawType())) { + protected StackManipulation convertLogicalType(TypeDescriptor type) { + if (java.time.Instant.class.isAssignableFrom(type.getRawType())) { // Generates the following code: // return java.time.Instant.ofEpochMilli(value.getMillis()) StackManipulation onNotNull = @@ -479,6 +473,29 @@ protected StackManipulation convertDefault(TypeDescriptor type) { .getOnly())); return shortCircuitReturnNull(readValue, onNotNull); } + return super.convertLogicalType(type); + } + + @Override + protected StackManipulation convertDefault(TypeDescriptor type) { + if (type.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { + // Generate the following code: + // return new T((byte[]) value); + ForLoadedType loadedType = new ForLoadedType(type.getRawType()); + return new Compound( + TypeCreation.of(loadedType), + Duplication.SINGLE, + // Load the parameter and cast it to a byte[]. + readValue, + TypeCasting.to(BYTES), + // Create a new instance that wraps this byte[]. + MethodInvocation.invoke( + loadedType + .getDeclaredMethods() + .filter( + ElementMatchers.isConstructor().and(ElementMatchers.takesArguments(BYTES))) + .getOnly())); + } return super.convertDefault(type); } } From e1362e02190d5379fba4f73508ad4cd7c1f76657 Mon Sep 17 00:00:00 2001 From: aaaZayne <1138069338@qq.com> Date: Thu, 21 May 2026 03:48:43 +1000 Subject: [PATCH 195/490] introduce private method to remove clones (#38245) --- .../gcp/bigquery/BigQueryResourceManager.java | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java index 45c22b1f0d55..5ca6a8c4655c 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java +++ b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java @@ -156,6 +156,20 @@ private synchronized Table getTableIfExists(String tableId) throws IllegalStateE return table; } + private void validateTableCreationInputs(String tableName, Schema schema) { + BigQueryResourceManagerUtils.checkValidTableId(tableName); + if (schema == null) { + throw new IllegalArgumentException("A valid schema must be provided to create a table."); + } + } + + private void ensureDatasetExists() { + if (dataset == null) { + createDataset(DEFAULT_DATASET_REGION); + } + checkHasDataset(); + } + /** * Helper method for logging individual errors thrown by inserting rows to a table. This method is * used to log errors thrown by inserting certain rows when other rows were successful. @@ -235,18 +249,8 @@ public synchronized TableId createTable(String tableName, Schema schema) public synchronized TableId createTable( String tableName, Schema schema, Long expirationTimeMillis) throws BigQueryResourceManagerException { - // Check table ID - BigQueryResourceManagerUtils.checkValidTableId(tableName); - - // Check schema - if (schema == null) { - throw new IllegalArgumentException("A valid schema must be provided to create a table."); - } - // Create a default dataset if this resource manager has not already created one - if (dataset == null) { - createDataset(DEFAULT_DATASET_REGION); - } - checkHasDataset(); + validateTableCreationInputs(tableName, schema); + ensureDatasetExists(); LOG.info("Creating table using tableName '{}'.", tableName); // Create the table if it does not already exist in the dataset @@ -313,13 +317,7 @@ public synchronized TableId createTimePartitionedTable( public synchronized TableId createTimePartitionedTable( String tableName, Schema schema, TimePartitioning timePartitioning, Long expirationTimeMillis) throws BigQueryResourceManagerException { - // Check table ID - BigQueryResourceManagerUtils.checkValidTableId(tableName); - - // Check schema - if (schema == null) { - throw new IllegalArgumentException("A valid schema must be provided to create a table."); - } + validateTableCreationInputs(tableName, schema); // Check time partition details if (timePartitioning == null) { @@ -327,11 +325,7 @@ public synchronized TableId createTimePartitionedTable( "A valid TimePartition object must be provided to create a time paritioned table. Use createTable instead to create non-partitioned tables."); } - // Create a default dataset if this resource manager has not already created one - if (dataset == null) { - createDataset(DEFAULT_DATASET_REGION); - } - checkHasDataset(); + ensureDatasetExists(); LOG.info( "Creating time partitioned table using tableName '{}' on field '{}'.", From 503f482d04bc386d19211a128052b562d15321f2 Mon Sep 17 00:00:00 2001 From: Atharv Date: Wed, 20 May 2026 23:36:26 +0530 Subject: [PATCH 196/490] Refresh Iceberg partition specs periodically (#38408) --- .../AssignDestinationsAndPartitions.java | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java index 475786d3a4f6..e5d70d85d875 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java @@ -39,6 +39,7 @@ import org.apache.iceberg.exceptions.NoSuchTableException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; import org.joda.time.Instant; /** @@ -51,8 +52,10 @@ class AssignDestinationsAndPartitions private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; + static final String DESTINATION = "destination"; static final String PARTITION = "partition"; + static final org.apache.beam.sdk.schemas.Schema OUTPUT_SCHEMA = org.apache.beam.sdk.schemas.Schema.builder() .addStringField(DESTINATION) @@ -75,8 +78,13 @@ public PCollection> expand(PCollection input) { } static class AssignDoFn extends DoFn> { + + private static final Duration REFRESH_INTERVAL = Duration.standardMinutes(5); + private transient @MonotonicNonNull Map partitionKeys; private transient @MonotonicNonNull Map wrappers; + private transient @MonotonicNonNull Map lastRefreshTimes; + private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; @@ -89,6 +97,7 @@ static class AssignDoFn extends DoFn> { public void setup() { this.wrappers = new HashMap<>(); this.partitionKeys = new HashMap<>(); + this.lastRefreshTimes = new HashMap<>(); } @ProcessElement @@ -98,42 +107,74 @@ public void processElement( PaneInfo paneInfo, @Timestamp Instant timestamp, OutputReceiver> out) { + String tableIdentifier = dynamicDestinations.getTableStringIdentifier( ValueInSingleWindow.of(element, timestamp, window, paneInfo)); + Row data = dynamicDestinations.getData(element); @Nullable PartitionKey partitionKey = checkStateNotNull(partitionKeys).get(tableIdentifier); + @Nullable BeamRowWrapper wrapper = checkStateNotNull(wrappers).get(tableIdentifier); - if (partitionKey == null || wrapper == null) { + + @Nullable Instant lastRefresh = checkStateNotNull(lastRefreshTimes).get(tableIdentifier); + + Instant now = Instant.now(); + + boolean shouldRefresh = + partitionKey == null + || wrapper == null + || lastRefresh == null + || now.isAfter(lastRefresh.plus(REFRESH_INTERVAL)); + + if (shouldRefresh) { + PartitionSpec spec = PartitionSpec.unpartitioned(); + Schema schema = IcebergUtils.beamSchemaToIcebergSchema(data.getSchema()); + @Nullable IcebergTableCreateConfig createConfig = dynamicDestinations.instantiateDestination(tableIdentifier).getTableCreateConfig(); + if (createConfig != null && createConfig.getPartitionFields() != null) { + spec = PartitionUtils.toPartitionSpec(createConfig.getPartitionFields(), data.getSchema()); + } else { + try { // see if table already exists with a spec - // TODO(https://github.com/apache/beam/issues/38337): improve this by periodically - // refreshing the table to fetch updated specs spec = catalogConfig.catalog().loadTable(TableIdentifier.parse(tableIdentifier)).spec(); + } catch (NoSuchTableException ignored) { // no partition to apply } } + partitionKey = new PartitionKey(spec, schema); + wrapper = new BeamRowWrapper(data.getSchema(), schema.asStruct()); + checkStateNotNull(partitionKeys).put(tableIdentifier, partitionKey); + checkStateNotNull(wrappers).put(tableIdentifier, wrapper); + + checkStateNotNull(lastRefreshTimes).put(tableIdentifier, now); } + + partitionKey = checkStateNotNull(partitionKey); + wrapper = checkStateNotNull(wrapper); + partitionKey.partition(wrapper.wrap(data)); + String partitionPath = partitionKey.toPath(); Row destAndPartition = Row.withSchema(OUTPUT_SCHEMA).addValues(tableIdentifier, partitionPath).build(); + out.output(KV.of(destAndPartition, data)); } } From 3eced19a54d72316130a094866cc1f6c2c2ac2b7 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 20 May 2026 16:31:00 -0400 Subject: [PATCH 197/490] WriteToJson - force num_shards (#38484) * force number of shards * update logic based on gemini * address another gemini comment * address one more round --- sdks/python/apache_beam/dataframe/io.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sdks/python/apache_beam/dataframe/io.py b/sdks/python/apache_beam/dataframe/io.py index 02423f517eea..cabf6ccd5c88 100644 --- a/sdks/python/apache_beam/dataframe/io.py +++ b/sdks/python/apache_beam/dataframe/io.py @@ -680,17 +680,27 @@ def __init__( self.binary = binary def expand(self, pcoll): - if 'file_naming' in self.kwargs: + kwargs = dict(self.kwargs) + if 'file_naming' in kwargs: dir, name = self.path, '' else: dir, name = io.filesystems.FileSystems.split(self.path) + num_shards = kwargs.pop('num_shards', None) + max_writers_per_bundle = kwargs.pop('max_writers_per_bundle', None) + write_to_files_kwargs = {} + if num_shards is not None: + write_to_files_kwargs['shards'] = num_shards + write_to_files_kwargs['max_writers_per_bundle'] = 0 + elif max_writers_per_bundle is not None: + write_to_files_kwargs['max_writers_per_bundle'] = max_writers_per_bundle + + file_naming = kwargs.pop('file_naming', fileio.default_file_naming(name)) return pcoll | fileio.WriteToFiles( path=dir, - shards=self.kwargs.pop('num_shards', None), - file_naming=self.kwargs.pop( - 'file_naming', fileio.default_file_naming(name)), + file_naming=file_naming, sink=lambda _: _WriteToPandasFileSink( - self.writer, self.args, self.kwargs, self.incremental, self.binary)) + self.writer, self.args, kwargs, self.incremental, self.binary), + **write_to_files_kwargs) class _WriteToPandasFileSink(fileio.FileSink): From ffc4b14e0f0deb2684e0a4a95119a4f9172489c9 Mon Sep 17 00:00:00 2001 From: scwhittle Date: Thu, 21 May 2026 13:48:53 +0200 Subject: [PATCH 198/490] =?UTF-8?q?Revert=20"[Java=20Portable=20SDK]=20Con?= =?UTF-8?q?figure=20JVM=20so=20that=20it=20exits=20upon=20OutOfMemoryEr?= =?UTF-8?q?=E2=80=A6"=20(#38567)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 37ae1ffc176e56bc4e21bf989ffc7389bfbbdfa5. --- sdks/java/container/boot.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/sdks/java/container/boot.go b/sdks/java/container/boot.go index 3ce79e4927eb..ad29f8d940ac 100644 --- a/sdks/java/container/boot.go +++ b/sdks/java/container/boot.go @@ -195,9 +195,6 @@ func main() { "-XX:+UseParallelGC", "-XX:+AlwaysActAsServerClassMachine", "-XX:-OmitStackTraceInFastThrow", - // Crash and restart instead of throwing OutOfMemoryError which may be caught by user or - // framework code and leave things in a degraded state. - "-XX:+ExitOnOutOfMemoryError", } enableGoogleCloudProfiler := strings.Contains(options, enableGoogleCloudProfilerOption) @@ -227,18 +224,16 @@ func main() { args = append(args, jammAgentArgs) } - enableHeapDumpsOnOom := false // If heap dumping is enabled, configure the JVM to dump it on oom events. if pipelineOptions, ok := info.GetPipelineOptions().GetFields()["options"]; ok { if heapDumpOption, ok := pipelineOptions.GetStructValue().GetFields()["enableHeapDumps"]; ok { - enableHeapDumpsOnOom = heapDumpOption.GetBoolValue() + if heapDumpOption.GetBoolValue() { + args = append(args, "-XX:+HeapDumpOnOutOfMemoryError", + "-Dbeam.fn.heap_dump_dir="+filepath.Join(dir, "heapdumps"), + "-XX:HeapDumpPath="+filepath.Join(dir, "heapdumps", "heap_dump.hprof")) + } } } - if enableHeapDumpsOnOom { - args = append(args, "-XX:+HeapDumpOnOutOfMemoryError", - "-Dbeam.fn.heap_dump_dir="+filepath.Join(dir, "heapdumps"), - "-XX:HeapDumpPath="+filepath.Join(dir, "heapdumps", "heap_dump.hprof")) - } // Apply meta options const metaDir = "/opt/apache/beam/options" From 8631ee03f081c9bc61fd336d0ffe8ecd8c2c9bca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 08:07:22 -0400 Subject: [PATCH 199/490] Bump com.gradle.common-custom-user-data-gradle-plugin (#38574) Bumps com.gradle.common-custom-user-data-gradle-plugin from 2.4.0 to 2.6.0. --- updated-dependencies: - dependency-name: com.gradle.common-custom-user-data-gradle-plugin dependency-version: 2.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index fc5f40c23d17..9c6bd2b30594 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,7 +25,7 @@ pluginManagement { plugins { id("com.gradle.develocity") version "3.19" - id("com.gradle.common-custom-user-data-gradle-plugin") version "2.4.0" + id("com.gradle.common-custom-user-data-gradle-plugin") version "2.6.0" } From 6f26581ba776caff2ac2ea9f72dde605bc0e1e25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 08:58:22 -0400 Subject: [PATCH 200/490] Bump github.com/nats-io/nats-server/v2 from 2.14.0 to 2.14.1 in /sdks (#38576) Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.14.0 to 2.14.1. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md) - [Commits](https://github.com/nats-io/nats-server/compare/v2.14.0...v2.14.1) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 dependency-version: 2.14.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index fd3c1fa8e75e..48a4c753100f 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -46,7 +46,7 @@ require ( github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 github.com/lib/pq v1.12.3 github.com/linkedin/goavro/v2 v2.15.0 - github.com/nats-io/nats-server/v2 v2.14.0 + github.com/nats-io/nats-server/v2 v2.14.1 github.com/nats-io/nats.go v1.52.0 github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 @@ -182,7 +182,7 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/patternmatcher v0.6.1 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 863ea382f54c..23a2cb5ea07c 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -638,8 +638,8 @@ github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= @@ -724,8 +724,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.14.0 h1:+8q0HrDFotwLLcGH/legOEOnowunhK+aZ4GYBIWpQlM= -github.com/nats-io/nats-server/v2 v2.14.0/go.mod h1:ImVUUDvfClJbb6cuJQRc1VmgDCXKM5ds0OoiG9MVOKo= +github.com/nats-io/nats-server/v2 v2.14.1 h1:wXs/a5fw9Hzm3CvuzLxGeIwpjPulSa7gMT3eSuhGkcg= +github.com/nats-io/nats-server/v2 v2.14.1/go.mod h1:4N17zLpuS7WMbG8T9gsE2B7z9hC9PraPyulVBfpK6nU= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= From b3673319050fee5365d949490916fa984c0d9ef5 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 21 May 2026 09:11:01 -0400 Subject: [PATCH 201/490] Fix test hang in subprocess expansion service on port bind failure (#38572) * Fix silent test hang in subprocess expansion service on port bind failure * Formatting * Add retry when starting subprocess server. * Add sleep before retrying. --- .../portability/expansion_service_main.py | 14 ++- .../apache_beam/utils/subprocess_server.py | 86 ++++++++++--------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/sdks/python/apache_beam/runners/portability/expansion_service_main.py b/sdks/python/apache_beam/runners/portability/expansion_service_main.py index 269d02b3efbd..f2d03e0e898c 100644 --- a/sdks/python/apache_beam/runners/portability/expansion_service_main.py +++ b/sdks/python/apache_beam/runners/portability/expansion_service_main.py @@ -55,7 +55,9 @@ def main(argv): with fully_qualified_named_transform.FullyQualifiedNamedTransform.with_filter( known_args.fully_qualified_name_glob): - address = '0.0.0.0:{}'.format(known_args.port) + # Bind to localhost instead of 0.0.0.0 to ensure compatibility with loopback + # connections on dual-stack (IPv4/IPv6) systems. + address = 'localhost:{}'.format(known_args.port) server = grpc.server(thread_pool_executor.shared_unbounded_instance()) if known_args.serve_loopback_worker: beam_fn_api_pb2_grpc.add_BeamFnExternalWorkerPoolServicer_to_server( @@ -71,9 +73,15 @@ def main(argv): artifact_service.ArtifactRetrievalService( artifact_service.BeamFilesystemHandler(None).file_reader), server) - server.add_insecure_port(address) + # Ensure gRPC server successfully binds. If this fails (e.g., due to port collision), + # add_insecure_port returns 0. We raise an error to crash the subprocess immediately, + # allowing the parent process to detect it and fail fast rather than hanging. + bound_port = server.add_insecure_port(address) + if not bound_port: + raise RuntimeError( + "Failed to bind expansion service to {}".format(address)) server.start() - _LOGGER.info('Listening for expansion requests at %d', known_args.port) + _LOGGER.info('Listening for expansion requests at %d', bound_port) def cleanup(unused_signum, unused_frame): _LOGGER.info('Shutting down expansion service.') diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index d21cb486b8f4..b22e6badb5e7 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -186,45 +186,53 @@ def __exit__(self, *unused_args): self.stop() def start(self): - try: - process, endpoint = self.start_process() - wait_secs = .1 - channel_options = [ - ("grpc.max_receive_message_length", -1), - ("grpc.max_send_message_length", -1), - # Default: 20000ms (20s), increased to 10 minutes for stability - ("grpc.keepalive_timeout_ms", 600_000), - # Default: 2, set to 0 to allow unlimited pings without data - ("grpc.http2.max_pings_without_data", 0), - # Default: False, set to True to allow keepalive pings when no calls - ("grpc.keepalive_permit_without_calls", True), - # Default: 2, set to 0 to allow unlimited ping strikes - ("grpc.http2.max_ping_strikes", 0), - # Default: 0 (disabled), enable socket reuse for better handling - ("grpc.so_reuseport", 1), - ] - self._grpc_channel = grpc.insecure_channel( - endpoint, options=channel_options) - channel_ready = grpc.channel_ready_future(self._grpc_channel) - while True: - if process is not None and process.poll() is not None: - _LOGGER.error("Started job service with %s", process.args) - raise RuntimeError( - 'Service failed to start up with error %s' % process.poll()) - try: - channel_ready.result(timeout=wait_secs) - break - except (grpc.FutureTimeoutError, grpc.RpcError): - wait_secs *= 1.2 - logging.log( - logging.WARNING if wait_secs > 1 else logging.DEBUG, - 'Waiting for grpc channel to be ready at %s.', - endpoint) - return self._stub_class(self._grpc_channel) - except: # pylint: disable=bare-except - _LOGGER.exception("Error bringing up service") - self.stop() - raise + max_attempts = 3 + for attempt in range(max_attempts): + try: + process, endpoint = self.start_process() + wait_secs = .1 + channel_options = [ + ("grpc.max_receive_message_length", -1), + ("grpc.max_send_message_length", -1), + # Default: 20000ms (20s), increased to 10 minutes for stability + ("grpc.keepalive_timeout_ms", 600_000), + # Default: 2, set to 0 to allow unlimited pings without data + ("grpc.http2.max_pings_without_data", 0), + # Default: False, set to True to allow keepalive pings when no calls + ("grpc.keepalive_permit_without_calls", True), + # Default: 2, set to 0 to allow unlimited ping strikes + ("grpc.http2.max_ping_strikes", 0), + # Default: 0 (disabled), enable socket reuse for better handling + ("grpc.so_reuseport", 1), + ] + self._grpc_channel = grpc.insecure_channel( + endpoint, options=channel_options) + channel_ready = grpc.channel_ready_future(self._grpc_channel) + while True: + if process is not None and process.poll() is not None: + _LOGGER.error("Started job service with %s", process.args) + raise RuntimeError( + 'Service failed to start up with error %s' % process.poll()) + try: + channel_ready.result(timeout=wait_secs) + break + except (grpc.FutureTimeoutError, grpc.RpcError): + wait_secs *= 1.2 + logging.log( + logging.WARNING if wait_secs > 1 else logging.DEBUG, + 'Waiting for grpc channel to be ready at %s.', + endpoint) + return self._stub_class(self._grpc_channel) + except Exception as e: + _LOGGER.warning( + "Error bringing up service on attempt %d: %s", + attempt + 1, + e, + exc_info=True) + self.stop() + if attempt == max_attempts - 1: + raise + time.sleep(1) def start_process(self): if self._owner_id is not None: From 90e2f43e14cd9b91553a8dc2b88cf7c29ed4ff8d Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Thu, 21 May 2026 17:58:15 +0400 Subject: [PATCH 202/490] Install wget and use repo token (#38340) --- .github/workflows/finalize_release.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 5979d91517f0..295b09f10837 100644 --- a/.github/workflows/finalize_release.yml +++ b/.github/workflows/finalize_release.yml @@ -24,6 +24,9 @@ on: description: Whether to publish the python artifacts into PyPi. Should be yes unless you've already completed this step. required: true default: 'no' + REPO_TOKEN: + description: Github Personal Access Token with repo permissions. + required: true TAG_RELEASE: description: Whether to tag the release on GitHub. Should be yes unless you've already completed this step. required: true @@ -45,6 +48,10 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Install dependencies + run: | + sudo apt-get update --yes + sudo apt-get install -y wget - name: Publish to Docker env: RELEASE: "${{ github.event.inputs.RELEASE }}" @@ -114,6 +121,8 @@ jobs: disable-cache: true - name: Install dependencies run: | + sudo apt-get update --yes + sudo apt-get install -y wget pip install python-dateutil pip install requests pip install twine @@ -133,6 +142,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + token: ${{ github.event.inputs.REPO_TOKEN }} + repository: apache/beam + persist-credentials: true - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -156,14 +169,14 @@ jobs: # Tag for Go SDK git tag "sdks/$VERSION_TAG" "$RC_TAG"^{} -m "Tagging release" --local-user="${{steps.import_gpg.outputs.name}}" - git push https://github.com/apache/beam "sdks/$VERSION_TAG" + git push origin "sdks/$VERSION_TAG" # Tag for repo root. git tag "$VERSION_TAG" "$RC_TAG"^{} -m "Tagging release" --local-user="${{steps.import_gpg.outputs.name}}" - git push https://github.com/apache/beam "$VERSION_TAG" + git push origin "$VERSION_TAG" git checkout -b "$POST_RELEASE_BRANCH" "$VERSION_TAG" - git push https://github.com/apache/beam "$POST_RELEASE_BRANCH" + git push origin "$POST_RELEASE_BRANCH" update_master: needs: push_git_tags @@ -173,6 +186,9 @@ jobs: steps: - name: Check out code uses: actions/checkout@v6 + with: + token: ${{ github.event.inputs.REPO_TOKEN }} + ref: master - name: Set git config run: | git config user.name $GITHUB_ACTOR From 7cf21ab0409ea9c9258e8093a8464b24e4d5aab6 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Thu, 21 May 2026 10:33:42 -0400 Subject: [PATCH 203/490] huggingface model handler for yaml - retry (#38451) * huggingface model handler for yaml * add yaml huggingface test file * update dependency logic --- .../beam_PostCommit_Yaml_Xlang_Direct.json | 2 +- .../beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- .../beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- .../yaml/tests/runinference_huggingface.yaml | 62 +++++++++++++++++++ ...erence.yaml => runinference_vertexai.yaml} | 0 sdks/python/apache_beam/yaml/yaml_ml.py | 49 +++++++++++++++ sdks/python/build.gradle | 19 +++++- 7 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml rename sdks/python/apache_beam/yaml/tests/{runinference.yaml => runinference_vertexai.yaml} (100%) diff --git a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json index 541dc4ea8e87..8ed972c9f579 100644 --- a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 2 + "revision": 3 } diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index ea1c255f7cc9..afa437b64de1 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -80,7 +80,7 @@ jobs: - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 7d17fd2140c9..0b8f4cd63939 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -91,7 +91,7 @@ jobs: - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:yamlIntegrationTests - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml new file mode 100644 index 000000000000..8728a6f544ad --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +pipelines: + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - text: "I love Apache Beam!" + - text: "I hate this error." + - type: RunInference + config: + model_handler: + type: "HuggingFacePipeline" + config: + task: "text-classification" + inference_fn: + callable: | + def real_inference(batch, pipeline, inference_args): + predictions = pipeline(batch, **inference_args) + + # If it's a single dictionary (batch size of 1), wrap it in a list + if isinstance(predictions, dict): + predictions = [predictions] + + return { + 'label': [p['label'] for p in predictions], + 'score': [p['score'] for p in predictions] + } + preprocess: + callable: 'lambda x: x.text' + - type: MapToFields + config: + language: python + fields: + text: text + sentiment: + callable: 'lambda x: x.inference.inference["label"]' + - type: AssertEqual + config: + elements: + - text: "I love Apache Beam!" + sentiment: "POSITIVE" + - text: "I hate this error." + sentiment: "NEGATIVE" + + options: + yaml_experimental_features: ['ML'] diff --git a/sdks/python/apache_beam/yaml/tests/runinference.yaml b/sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml similarity index 100% rename from sdks/python/apache_beam/yaml/tests/runinference.yaml rename to sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml diff --git a/sdks/python/apache_beam/yaml/yaml_ml.py b/sdks/python/apache_beam/yaml/yaml_ml.py index 51f18c733046..05cbed3bd456 100644 --- a/sdks/python/apache_beam/yaml/yaml_ml.py +++ b/sdks/python/apache_beam/yaml/yaml_ml.py @@ -282,6 +282,55 @@ def inference_output_type(self): ('model_id', Optional[str])]) +@ModelHandlerProvider.register_handler_type('HuggingFacePipeline') +class HuggingFacePipelineProvider(ModelHandlerProvider): + def __init__( + self, + task: Optional[str] = None, + model: Optional[str] = None, + preprocess: Optional[dict[str, str]] = None, + postprocess: Optional[dict[str, str]] = None, + device: Optional[Any] = None, + inference_fn: Optional[dict[str, str]] = None, + load_pipeline_args: Optional[dict[str, Any]] = None, + **kwargs): + try: + from apache_beam.ml.inference.huggingface_inference import HuggingFacePipelineModelHandler + except ImportError: + raise ValueError( + 'Unable to import HuggingFacePipelineModelHandler. Please ' + 'install transformers dependencies.') + + kwargs = {k: v for k, v in kwargs.items() if not k.startswith('_')} + + inference_fn_obj = self.parse_processing_transform( + inference_fn, 'inference_fn') if inference_fn else None + + handler_kwargs = {} + if inference_fn_obj: + handler_kwargs['inference_fn'] = inference_fn_obj + + _handler = HuggingFacePipelineModelHandler( + task=task, + model=model, + device=device, + load_pipeline_args=load_pipeline_args, + **handler_kwargs, + **kwargs) + + super().__init__(_handler, preprocess, postprocess) + + @staticmethod + def validate(config): + if not config.get('task') and not config.get('model'): + raise ValueError( + "HuggingFacePipeline requires either 'task' or " + "'model' to be specified.") + + def inference_output_type(self): + return Any + + @beam.ptransform.ptransform_fn def run_inference( pcoll, diff --git a/sdks/python/build.gradle b/sdks/python/build.gradle index 5f09dff57e8f..837631868b8a 100644 --- a/sdks/python/build.gradle +++ b/sdks/python/build.gradle @@ -124,10 +124,25 @@ tasks.register("generateYamlDocs") { outputs.file "${buildDir}/yaml-examples.html" } +tasks.register("installYamlIntegrationTestDeps") { + dependsOn installGcpTest + doLast { + exec { + executable 'sh' + args '-c', ". ${envdir}/bin/activate && " + + "py_ver=\$(python -c 'import sys; print(f\"{sys.version_info.major}{sys.version_info.minor}\")') && " + + "ml_extra=\"ml_test\" && " + + "if [ \"\$py_ver\" -ge 313 ]; then ml_extra=\"p\${py_ver}_ml_test\"; fi && " + + "echo \"Installing dependencies...\" && " + + "pip install --pre --retries 10 ${buildDir}/apache-beam.tar.gz[\$ml_extra,yaml,transformers]" + } + } +} + tasks.register("yamlIntegrationTests") { description "Runs precommit integration tests for yaml pipelines." - dependsOn installGcpTest + dependsOn installYamlIntegrationTestDeps // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" @@ -146,7 +161,7 @@ tasks.register("yamlIntegrationTests") { tasks.register("postCommitYamlIntegrationTests") { description "Runs postcommit integration tests for yaml pipelines - parameterized by yamlTestSet." - dependsOn installGcpTest + dependsOn installYamlIntegrationTestDeps // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" From 38600b892b6ba7dfea818f3b5435287b5b3db1fb Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 21 May 2026 16:35:17 +0200 Subject: [PATCH 204/490] Fix table row inference benchmark using wrong model path (#38569) * Fix table row inference benchmark model_path * refactor * fix formatter --- .../table_row_inference_benchmark.py | 44 ++++++++++--------- .../apache_beam/testing/test_pipeline.py | 4 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py b/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py index b8591a0fea83..e3de24574391 100644 --- a/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py +++ b/sdks/python/apache_beam/testing/benchmarks/inference/table_row_inference_benchmark.py @@ -72,32 +72,36 @@ def __init__(self): metrics_namespace=self.metrics_namespace, is_streaming=False, pcollection='RunInference/BeamML_RunInference_Postprocess-0.out0') - self.is_streaming = ((self.pipeline.get_option('mode') or - 'batch') == 'streaming') + self.opts = self.pipeline.get_pipeline_options().view_as( + TableRowInferenceOptions) + mode = self.opts.mode or 'batch' + self.is_streaming = mode == 'streaming' if self.is_streaming: - self.subscription = self.pipeline.get_option('input_subscription') + self.subscription = self.opts.input_subscription def test(self): """Execute the table row inference pipeline for benchmarking.""" - extra_opts = {} - - mode = self.pipeline.get_option('mode') or 'batch' - extra_opts['mode'] = mode + mode = self.opts.mode or 'batch' + extra_opts = {'mode': mode} if mode == 'streaming': - extra_opts['input_subscription'] = self.pipeline.get_option( - 'input_subscription') - extra_opts['window_size_sec'] = int( - self.pipeline.get_option('window_size_sec') or 60) - extra_opts['trigger_interval_sec'] = int( - self.pipeline.get_option('trigger_interval_sec') or 30) - else: - extra_opts['input_file'] = self.pipeline.get_option('input_file') - - for opt in ['output_table', 'model_path', 'feature_columns']: - val = self.pipeline.get_option(opt) - if val: - extra_opts[opt] = val + if self.opts.input_subscription: + extra_opts['input_subscription'] = self.opts.input_subscription + extra_opts['window_size_sec'] = ( + self.opts.window_size_sec + if self.opts.window_size_sec is not None else 60) + extra_opts['trigger_interval_sec'] = ( + self.opts.trigger_interval_sec + if self.opts.trigger_interval_sec is not None else 30) + elif self.opts.input_file: + extra_opts['input_file'] = self.opts.input_file + + if self.opts.output_table: + extra_opts['output_table'] = self.opts.output_table + if self.opts.model_path: + extra_opts['model_path'] = self.opts.model_path + if self.opts.feature_columns: + extra_opts['feature_columns'] = self.opts.feature_columns self.result = table_row_inference.run( self.pipeline.get_full_options_as_args(**extra_opts), diff --git a/sdks/python/apache_beam/testing/test_pipeline.py b/sdks/python/apache_beam/testing/test_pipeline.py index 712da8636234..1fe2d86e35b3 100644 --- a/sdks/python/apache_beam/testing/test_pipeline.py +++ b/sdks/python/apache_beam/testing/test_pipeline.py @@ -209,7 +209,9 @@ def get_option(self, opt_name, bool_option=False): None if option is not found in existing option list which is generated by parsing value of argument `test-pipeline-options`. """ - parser = argparse.ArgumentParser() + # Parse one flag at a time; disable prefix matching so e.g. --mode does + # not satisfy --model_path when both appear in options_list. + parser = argparse.ArgumentParser(allow_abbrev=False) opt_name = opt_name[:2] if opt_name[:2] == '--' else opt_name # Option name should start with '--' when it's used for parsing. if bool_option: From bdfc616cef32e5a7cbf0686d9b5973beb9d01db1 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 21 May 2026 11:28:01 -0400 Subject: [PATCH 205/490] Fix flaky MatrixPowerTest.test_basics by reading all generated shards (#38585) * Fix flaky MatrixPowerTest.test_basics by reading all generated shards * Address comments. --- .../apache_beam/examples/matrix_power_test.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sdks/python/apache_beam/examples/matrix_power_test.py b/sdks/python/apache_beam/examples/matrix_power_test.py index ff17ca5ff123..b824ba7e24c2 100644 --- a/sdks/python/apache_beam/examples/matrix_power_test.py +++ b/sdks/python/apache_beam/examples/matrix_power_test.py @@ -17,6 +17,7 @@ """Test for the matrix power integration test.""" +import glob import logging import tempfile import unittest @@ -51,9 +52,15 @@ def test_basics(self): '--input_matrix=%s --input_vector=%s --exponent=%d --output=%s.result' % (matrix_path, vector_path, self.EXPONENT, vector_path)).split()) # Parse result file and compare. - with open(vector_path + '.result-00000-of-00001') as result_file: - results = result_file.read() - self.assertEqual(sorted(self.EXPECTED_OUTPUT), sorted(results)) + shard_paths = glob.glob(vector_path + '.result*') + self.assertTrue( + shard_paths, + 'No output shards found matching prefix: %s.result' % vector_path) + results = [] + for path in shard_paths: + with open(path) as result_file: + results.append(result_file.read()) + self.assertEqual(sorted(self.EXPECTED_OUTPUT), sorted(''.join(results))) if __name__ == '__main__': From 796548a9f81dfb7a192d726e8ed6f223b32fc6b6 Mon Sep 17 00:00:00 2001 From: Ganesh Sivakumar <281808907+ganesh-skumar@users.noreply.github.com> Date: Thu, 21 May 2026 22:19:44 +0530 Subject: [PATCH 206/490] Update transform catalogue docs (#38457) * update transform catalogue * spotless * fix for window bug * Update website/www/site/content/en/documentation/transforms/java/aggregation/batchelements.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * update website sidebar * update doc --------- Co-authored-by: Ganeshsivakumar Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../beam/examples/BatchElementsExample.java | 81 +++++++++++++++++++ .../beam/sdk/transforms/BatchElements.java | 9 ++- .../java/aggregation/batchelements.md | 31 +++++++ .../documentation/transforms/java/overview.md | 1 + .../section-menu/en/documentation.html | 1 + 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java create mode 100644 website/www/site/content/en/documentation/transforms/java/aggregation/batchelements.md diff --git a/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java new file mode 100644 index 000000000000..79130d7ac138 --- /dev/null +++ b/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.examples; + +import java.util.List; +import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.transforms.BatchElements; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// beam-playground: +// name: BatchElements +// description: Demonstration of BatchElements transform usage. +// multifile: false +// default_example: false +// context_line: 47 +// categories: +// - Core Transforms +// complexity: BASIC +// tags: +// - transforms +// - batch + +public class BatchElementsExample { + public static void main(String[] args) { + PipelineOptions options = PipelineOptionsFactory.create(); + + Pipeline pipeline = Pipeline.create(options); + + // [START main_section] + // Create input + + PCollection inputs = + pipeline.apply(Create.of("apple", "strawberry", "orange", "peach", "cherry", "pear")); + + // Create Batch Config + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder().withMinBatchSize(2).withMaxBatchSize(4).build(); + // Batch Elements + PCollection> result = inputs.apply(BatchElements.withConfig(config)); + // [END main_section] + result.apply(ParDo.of(new LogOutput())); + pipeline.run(); + } + + static class LogOutput extends DoFn, String> { + private static final Logger LOG = LoggerFactory.getLogger(LogOutput.class); + + @ProcessElement + public void processElement(ProcessContext c) throws Exception { + List batch = c.element(); + + LOG.info("Batch Contents: {}", batch); + + for (String element : batch) { + c.output(element); + } + } + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java index 35796d1b1385..d410c5be6007 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.transforms; +import static java.util.Collections.singleton; + import java.io.Serializable; import java.util.ArrayList; import java.util.Comparator; @@ -27,6 +29,7 @@ import javax.annotation.Nullable; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.WindowingStrategy; @@ -568,7 +571,11 @@ public void processElement( try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(targetBatch.size)) { - receiver.outputWithTimestamp(targetBatch.elements, targetWindow.maxTimestamp()); + receiver.outputWindowedValue( + targetBatch.elements, + targetWindow.maxTimestamp(), + singleton(targetWindow), + PaneInfo.NO_FIRING); } batches.remove(targetWindow); diff --git a/website/www/site/content/en/documentation/transforms/java/aggregation/batchelements.md b/website/www/site/content/en/documentation/transforms/java/aggregation/batchelements.md new file mode 100644 index 000000000000..f842d303c365 --- /dev/null +++ b/website/www/site/content/en/documentation/transforms/java/aggregation/batchelements.md @@ -0,0 +1,31 @@ +--- +title: "BatchElements" +--- + + +# BatchElements + +BatchElements transform groups individual elements into batches before processing them downstream. +It is designed for operations where each call has a fixed overhead regardless of how many elements are processed and the transform amortizes that cost across multiple elements at once. +The transform takes a `PCollection` as input and produces a `PCollection>`, where each output element is a batch containing multiple input elements. +Batch sizes are chosen dynamically between the configured minimum and maximum values by measuring the execution time of downstream operations. + +Batching is performed per window. Each emitted batch belongs to the same window as its input elements. + +## Examples + +{{< playground height="700px" >}} +{{< playground_snippet language="java" path="SDK_JAVA_BatchElements" show="main_section" >}} +{{< /playground >}} diff --git a/website/www/site/content/en/documentation/transforms/java/overview.md b/website/www/site/content/en/documentation/transforms/java/overview.md index 59aa93930fbe..adb4da51a836 100644 --- a/website/www/site/content/en/documentation/transforms/java/overview.md +++ b/website/www/site/content/en/documentation/transforms/java/overview.md @@ -55,6 +55,7 @@ limitations under the License. GroupByKeyTakes a keyed collection of elements and produces a collection where each element consists of a key and all values associated with that key. GroupIntoBatchesBatches values associated with keys into Iterable batches of some size. Each batch contains elements associated with a specific key. + BatchElementsGroups individual elements into batches to amortize fixed processing costs, using dynamically estimated batch sizes. HllCountEstimates the number of distinct elements and creates re-aggregatable sketches using the HyperLogLog++ algorithm. LatestSelects the latest element within each aggregation according to the implicit timestamp. MaxOutputs the maximum element within each aggregation. diff --git a/website/www/site/layouts/partials/section-menu/en/documentation.html b/website/www/site/layouts/partials/section-menu/en/documentation.html index 57514935825e..41100270d827 100755 --- a/website/www/site/layouts/partials/section-menu/en/documentation.html +++ b/website/www/site/layouts/partials/section-menu/en/documentation.html @@ -404,6 +404,7 @@

  • Distinct
  • GroupByKey
  • GroupIntoBatches
  • +
  • BatchElements
  • HllCount
  • Latest
  • Max
  • From f89c59a0fb957f53aa029d0993ef016a594300c2 Mon Sep 17 00:00:00 2001 From: Chamikara Jayalath Date: Thu, 21 May 2026 10:00:56 -0700 Subject: [PATCH 207/490] Initial skeleton for the Delta Lake source (#38571) --- .../beam/gradle/BeamModulePlugin.groovy | 3 + sdks/java/io/delta/build.gradle | 39 ++++++++ .../org/apache/beam/sdk/io/delta/DeltaIO.java | 91 +++++++++++++++++++ .../beam/sdk/io/delta/package-info.java | 24 +++++ .../apache/beam/sdk/io/delta/DeltaIOTest.java | 61 +++++++++++++ settings.gradle.kts | 1 + 6 files changed, 219 insertions(+) create mode 100644 sdks/java/io/delta/build.gradle create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java create mode 100644 sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 27b9cef9637a..5ca0de9de846 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -605,6 +605,7 @@ class BeamModulePlugin implements Plugin { def cdap_version = "6.5.1" def checkerframework_version = "3.42.0" def classgraph_version = "4.8.162" + def delta_lake_version = "4.2.0" def dbcp2_version = "2.9.0" def errorprone_version = "2.31.0" // [bomupgrader] determined by: com.google.api:gax, consistent with: google_cloud_platform_libraries_bom @@ -729,6 +730,8 @@ class BeamModulePlugin implements Plugin { commons_logging : "commons-logging:commons-logging:1.2", commons_math3 : "org.apache.commons:commons-math3:3.6.1", dbcp2 : "org.apache.commons:commons-dbcp2:$dbcp2_version", + delta_kernel_api : "io.delta:delta-kernel-api:$delta_lake_version", + delta_kernel_defaults : "io.delta:delta-kernel-defaults:$delta_lake_version", envoy_control_plane_api : "io.envoyproxy.controlplane:api:1.0.49", error_prone_annotations : "com.google.errorprone:error_prone_annotations:$errorprone_version", failsafe : "dev.failsafe:failsafe:3.3.0", diff --git a/sdks/java/io/delta/build.gradle b/sdks/java/io/delta/build.gradle new file mode 100644 index 000000000000..617965b3bc4e --- /dev/null +++ b/sdks/java/io/delta/build.gradle @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { id 'org.apache.beam.module' } +applyJavaNature( + automaticModuleName: 'org.apache.beam.sdk.io.delta', + // Latest version of the Delta Kernel API requires Java 17. + requireJavaVersion: JavaVersion.VERSION_17, +) + +description = "Apache Beam :: SDKs :: Java :: IO :: Delta Lake" +ext.summary = "Integration with Delta Lake." + + +dependencies { + implementation project(path: ":sdks:java:core", configuration: "shadow") + implementation library.java.delta_kernel_api + implementation library.java.delta_kernel_defaults + + permitUnusedDeclared library.java.delta_kernel_api + permitUnusedDeclared library.java.delta_kernel_defaults + + testImplementation library.java.junit +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java new file mode 100644 index 000000000000..6c5df4728b4e --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import com.google.auto.value.AutoValue; +import java.util.Map; +import org.apache.beam.sdk.annotations.Internal; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.values.PBegin; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A connector that reads from Delta Lake tables. + * + *

    This is work in progress. For more details and to track progress, please see Issue 21100. + */ +@Internal +public class DeltaIO { + + public static ReadRows readRows() { + return new AutoValue_DeltaIO_ReadRows.Builder().build(); + } + + @AutoValue + public abstract static class ReadRows extends PTransform> { + + public abstract @Nullable String getTablePath(); + + public abstract @Nullable Long getVersion(); + + public abstract @Nullable String getTimestamp(); + + public abstract @Nullable Map getHadoopConfig(); + + abstract Builder toBuilder(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setTablePath(String tablePath); + + abstract Builder setVersion(@Nullable Long version); + + abstract Builder setTimestamp(@Nullable String timestamp); + + abstract Builder setHadoopConfig(@Nullable Map hadoopConfig); + + abstract ReadRows build(); + } + + public ReadRows from(String tablePath) { + return toBuilder().setTablePath(tablePath).build(); + } + + public ReadRows withVersion(@Nullable Long version) { + return toBuilder().setVersion(version).build(); + } + + public ReadRows withTimestamp(@Nullable String timestamp) { + return toBuilder().setTimestamp(timestamp).build(); + } + + public ReadRows withConfig(Map config) { + return toBuilder().setHadoopConfig(config).build(); + } + + @Override + public PCollection expand(PBegin input) { + // TODO(https://github.com/apache/beam/issues/38551): Implement expansion for + // Delta Lake ReadRows + throw new UnsupportedOperationException("Not implemented yet."); + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java new file mode 100644 index 000000000000..c765a7fb7655 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Transforms for reading from Delta Lake. + * + * @see org.apache.beam.sdk.io.delta.DeltaIO + */ +package org.apache.beam.sdk.io.delta; diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java new file mode 100644 index 000000000000..b09fa8f69b3e --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import java.util.HashMap; +import java.util.Map; +import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for the {@link DeltaIO}. */ +@RunWith(JUnit4.class) +public class DeltaIOTest { + + @Test + public void testReadRowsBuilderAndGetters() { + String tablePath = "/path/to/table"; + long version = 5L; + String timestamp = "2026-05-20T15:43:26Z"; + Map hadoopConfig = new HashMap<>(); + hadoopConfig.put("fs.defaultFS", "file:///"); + + ReadRows readRows = DeltaIO.readRows() + .from(tablePath) + .withVersion(version) + .withTimestamp(timestamp) + .withConfig(hadoopConfig); + + Assert.assertEquals(tablePath, readRows.getTablePath()); + Assert.assertEquals(Long.valueOf(version), readRows.getVersion()); + Assert.assertEquals(timestamp, readRows.getTimestamp()); + Assert.assertEquals(hadoopConfig, readRows.getHadoopConfig()); + } + + @Test + public void testReadRowsNullDefaults() { + ReadRows readRows = DeltaIO.readRows(); + + Assert.assertNull(readRows.getTablePath()); + Assert.assertNull(readRows.getVersion()); + Assert.assertNull(readRows.getTimestamp()); + Assert.assertNull(readRows.getHadoopConfig()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c6bd2b30594..1ead92a9cfc1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -226,6 +226,7 @@ include(":sdks:java:io:file-based-io-tests") include(":sdks:java:io:bigquery-io-perf-tests") include(":sdks:java:io:cdap") include(":sdks:java:io:csv") +include(":sdks:java:io:delta") include(":sdks:java:io:datadog") include(":sdks:java:io:file-schema-transform") include(":sdks:java:io:google-ads") From f854666b7b1b9577cc9de6efe932f098dc598c1b Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 21 May 2026 14:06:02 -0400 Subject: [PATCH 208/490] Enforce Google Maven Mirror on CI environments (#38586) * Implement Maven Central mirroring in CI environments * Fix subproject buildscript classpath resolution for beam-test-gha using CI mirror fallback * Remove duplicated definition. * Discard the new settings file in buildSrc first. * Fix compilation error. --- .github/build.gradle | 10 ++++++++- buildSrc/build.gradle.kts | 19 ++++++++++++++++ .../apache/beam/gradle/Repositories.groovy | 22 +++++++++++++++---- gradle.properties | 3 +++ settings.gradle.kts | 15 ++++++++++++- 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/.github/build.gradle b/.github/build.gradle index 6effa8742f60..d5a9a6c9dc75 100644 --- a/.github/build.gradle +++ b/.github/build.gradle @@ -18,7 +18,15 @@ buildscript { repositories { - mavenCentral() + def mirrorUrl = project.findProperty("mavenCentralMirrorUrl") + boolean isCi = System.getenv("GITHUB_ACTIONS") != null || System.getenv("JENKINS_HOME") != null + def useMirror = isCi && mirrorUrl + + if (!useMirror) { + mavenCentral() + } else { + maven { url mirrorUrl } + } } dependencies { classpath group: 'org.yaml', name: 'snakeyaml', version: '2.2' diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 9ad1a6a5bf3b..96b1cb12dd4c 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,6 +15,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import java.util.Properties + +val parentProperties = Properties().apply { + val file = file("../gradle.properties") + if (file.exists()) { + file.inputStream().use { load(it) } + } +} + +val mavenCentralMirrorUrl = parentProperties.getProperty("mavenCentralMirrorUrl") +val isCi = System.getenv("GITHUB_ACTIONS") != null || System.getenv("JENKINS_HOME") != null +val useMirror = isCi && !mavenCentralMirrorUrl.isNullOrBlank() // Plugins for configuring _this build_ of the module plugins { @@ -25,6 +37,13 @@ plugins { // Define the set of repositories required to fetch and enable plugins. repositories { + if (useMirror) { + logger.lifecycle("Running in CI. Mirroring Maven Central repositories via Google Maven Mirror for buildSrc.") + } + + if (useMirror) { + maven { url = uri(mavenCentralMirrorUrl!!) } + } maven { url = uri("https://plugins.gradle.org/m2/") } maven { url = uri("https://repo.spring.io/plugins-release/") diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy index 12e48356898a..20c878c06a94 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy @@ -19,10 +19,16 @@ package org.apache.beam.gradle import org.gradle.api.Project - class Repositories { static void register(Project project) { + def mirrorUrl = project.findProperty("mavenCentralMirrorUrl") + boolean isCi = System.getenv("GITHUB_ACTIONS") != null || System.getenv("JENKINS_HOME") != null + def useMirror = isCi && mirrorUrl + + if (useMirror) { + project.logger.lifecycle("Running in CI. Mirroring Maven Central repositories via Google Maven Mirror.") + } project.repositories { maven { url project.offlineRepositoryRoot } @@ -36,7 +42,11 @@ class Repositories { return } - mavenCentral() + if (!useMirror) { + mavenCentral() + } else { + maven { url mirrorUrl } + } mavenLocal() // For Confluent Kafka dependencies @@ -60,7 +70,7 @@ class Repositories { // Apply a plugin which provides the 'updateOfflineRepository' task that creates an offline // repository. This offline repository satisfies all Gradle build dependencies and Java // project dependencies. The offline repository is placed within $rootDir/offline-repo - // but can be overridden by specifying '-PofflineRepositoryRoot=/path/to/repo'. + // but can be overridden by specifying '-Pareas/offline-repo'. // Note that parallel build must be disabled when executing 'updateOfflineRepository' // by specifying '--no-parallel', see // https://github.com/mdietrichstein/gradle-offline-dependencies-plugin/issues/3 @@ -68,7 +78,11 @@ class Repositories { project.offlineDependencies { repositories { mavenLocal() - mavenCentral() + if (!useMirror) { + mavenCentral() + } else { + maven { url mirrorUrl } + } maven { url "https://plugins.gradle.org/m2/" } maven { url "https://repo.spring.io/plugins-release" } maven { url "https://packages.confluent.io/maven/" } diff --git a/gradle.properties b/gradle.properties index 1d40c7ed4669..95e50105a494 100644 --- a/gradle.properties +++ b/gradle.properties @@ -44,3 +44,6 @@ flink_versions=1.17,1.18,1.19,1.20,2.0 spark_versions=3,4 # supported python versions python_versions=3.10,3.11,3.12,3.13,3.14 + +# Maven Central fallback mirror URL +mavenCentralMirrorUrl=https://maven-central.storage-download.googleapis.com/maven2/ diff --git a/settings.gradle.kts b/settings.gradle.kts index 1ead92a9cfc1..603832045f3e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,20 @@ import com.gradle.enterprise.gradleplugin.internal.extension.BuildScanExtensionWithHiddenFeatures pluginManagement { + val mavenCentralMirrorUrl = settings.providers.gradleProperty("mavenCentralMirrorUrl").orNull + val isCi = System.getenv("GITHUB_ACTIONS") != null || System.getenv("JENKINS_HOME") != null + val useMirror = isCi && !mavenCentralMirrorUrl.isNullOrBlank() + + if (useMirror) { + logger.lifecycle("Running in CI. Mirroring Maven Central repositories via Google Maven Mirror.") + } + + repositories { + if (useMirror) { + maven { url = uri(mavenCentralMirrorUrl!!) } + } + gradlePluginPortal() + } plugins { id("org.javacc.javacc") version "4.0.3" // enable the JavaCC parser generator } @@ -28,7 +42,6 @@ plugins { id("com.gradle.common-custom-user-data-gradle-plugin") version "2.6.0" } - // JENKINS_HOME and BUILD_ID set automatically during Jenkins execution val isJenkinsBuild = arrayOf("JENKINS_HOME", "BUILD_ID").all { System.getenv(it) != null } // GITHUB_REPOSITORY and GITHUB_RUN_ID set automatically during Github Actions run From b845db77169c7c99d8cf64ff8bb17d5da9b7632e Mon Sep 17 00:00:00 2001 From: Andrew Crites Date: Thu, 21 May 2026 13:40:16 -0700 Subject: [PATCH 209/490] Adds backlog reporting support for non-fnapi based SDF's. (#38346) --- ...datesRunner_Dataflow_Streaming_Engine.json | 2 +- ...oundedSplittableProcessElementInvoker.java | 247 ++++++++++-------- .../SplittableParDoViaKeyedWorkItems.java | 9 + .../core/SplittableProcessElementInvoker.java | 25 +- .../apache/beam/runners/core/StepContext.java | 6 + ...edSplittableProcessElementInvokerTest.java | 47 +++- .../core/SplittableParDoProcessFnTest.java | 124 ++++++++- .../worker/SplittableProcessFnFactory.java | 1 + .../worker/StreamingModeExecutionContext.java | 16 ++ .../StreamingModeExecutionContextTest.java | 25 ++ .../SpannerChangeStreamErrorTest.java | 41 ++- 11 files changed, 415 insertions(+), 128 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json index 50d17c108f2e..e623d3373a93 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 2, + "modification": 1, } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java index ebd88442b211..dbbcfe8ee317 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java @@ -130,114 +130,114 @@ public Result invokeProcessElement( final Map> sideInputMapping) { final ProcessContext processContext = new ProcessContext(element, tracker, watermarkEstimator); - DoFn.ProcessContinuation cont = - invoker.invokeProcessElement( - new DoFnInvoker.BaseArgumentProvider() { - @Override - public String getErrorContext() { - return OutputAndTimeBoundedSplittableProcessElementInvoker.class.getSimpleName(); - } - - @Override - public DoFn.ProcessContext processContext( - DoFn doFn) { - return processContext; - } - - @Override - public Object sideInput(String tagId) { - PCollectionView view = sideInputMapping.get(tagId); - if (view == null) { - throw new IllegalArgumentException("calling getSideInput() with unknown view"); - } - return processContext.sideInput(view); - } - - @Override - public Object restriction() { - return tracker.currentRestriction(); - } - - @Override - public InputT element(DoFn doFn) { - return processContext.element(); - } - - @Override - public Instant timestamp(DoFn doFn) { - return processContext.timestamp(); - } - - @Override - public String timerId(DoFn doFn) { - throw new UnsupportedOperationException( - "Cannot access timerId as parameter outside of @OnTimer method."); - } - - @Override - public TimeDomain timeDomain(DoFn doFn) { - throw new UnsupportedOperationException( - "Access to time domain not supported in ProcessElement"); - } - - @Override - public OutputReceiver outputReceiver(DoFn doFn) { - return DoFnOutputReceivers.windowedReceiver( - processContext, OutputBuilderSuppliers.supplierForElement(element), null); - } - - @Override - public OutputReceiver outputRowReceiver(DoFn doFn) { - throw new UnsupportedOperationException("Not supported in SplittableDoFn"); - } - - @Override - public MultiOutputReceiver taggedOutputReceiver(DoFn doFn) { - return DoFnOutputReceivers.windowedMultiReceiver( - processContext, OutputBuilderSuppliers.supplierForElement(element)); - } - - @Override - public CausedByDrain causedByDrain(DoFn doFn) { - return processContext.causedByDrain(); - } - - @Override - public RestrictionTracker restrictionTracker() { - return processContext.tracker; - } - - @Override - public WatermarkEstimator watermarkEstimator() { - return processContext.watermarkEstimator; - } - - @Override - public PipelineOptions pipelineOptions() { - return pipelineOptions; - } - - @Override - public BundleFinalizer bundleFinalizer() { - return bundleFinalizer.get(); - } - - // Unsupported methods below. - - @Override - public StartBundleContext startBundleContext(DoFn doFn) { - throw new IllegalStateException( - "Should not access startBundleContext() from @" - + DoFn.ProcessElement.class.getSimpleName()); - } - - @Override - public FinishBundleContext finishBundleContext(DoFn doFn) { - throw new IllegalStateException( - "Should not access finishBundleContext() from @" - + DoFn.ProcessElement.class.getSimpleName()); - } - }); + DoFnInvoker.BaseArgumentProvider invokerArgumentProvider = + new DoFnInvoker.BaseArgumentProvider() { + @Override + public String getErrorContext() { + return OutputAndTimeBoundedSplittableProcessElementInvoker.class.getSimpleName(); + } + + @Override + public DoFn.ProcessContext processContext(DoFn doFn) { + return processContext; + } + + @Override + public Object sideInput(String tagId) { + PCollectionView view = sideInputMapping.get(tagId); + if (view == null) { + throw new IllegalArgumentException("calling getSideInput() with unknown view"); + } + return processContext.sideInput(view); + } + + @Override + public Object restriction() { + return tracker.currentRestriction(); + } + + @Override + public InputT element(DoFn doFn) { + return processContext.element(); + } + + @Override + public Instant timestamp(DoFn doFn) { + return processContext.timestamp(); + } + + @Override + public String timerId(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access timerId as parameter outside of @OnTimer method."); + } + + @Override + public TimeDomain timeDomain(DoFn doFn) { + throw new UnsupportedOperationException( + "Access to time domain not supported in ProcessElement"); + } + + @Override + public OutputReceiver outputReceiver(DoFn doFn) { + return DoFnOutputReceivers.windowedReceiver( + processContext, OutputBuilderSuppliers.supplierForElement(element), null); + } + + @Override + public OutputReceiver outputRowReceiver(DoFn doFn) { + throw new UnsupportedOperationException("Not supported in SplittableDoFn"); + } + + @Override + public MultiOutputReceiver taggedOutputReceiver(DoFn doFn) { + return DoFnOutputReceivers.windowedMultiReceiver( + processContext, OutputBuilderSuppliers.supplierForElement(element)); + } + + @Override + public CausedByDrain causedByDrain(DoFn doFn) { + return processContext.causedByDrain(); + } + + @Override + public RestrictionTracker restrictionTracker() { + return processContext.tracker; + } + + @Override + public WatermarkEstimator watermarkEstimator() { + return processContext.watermarkEstimator; + } + + @Override + public PipelineOptions pipelineOptions() { + return pipelineOptions; + } + + @Override + public BundleFinalizer bundleFinalizer() { + return bundleFinalizer.get(); + } + + // Unsupported methods below. + + @Override + public StartBundleContext startBundleContext(DoFn doFn) { + throw new IllegalStateException( + "Should not access startBundleContext() from @" + + DoFn.ProcessElement.class.getSimpleName()); + } + + @Override + public FinishBundleContext finishBundleContext(DoFn doFn) { + throw new IllegalStateException( + "Should not access finishBundleContext() from @" + + DoFn.ProcessElement.class.getSimpleName()); + } + }; + + DoFn.ProcessContinuation cont = invoker.invokeProcessElement(invokerArgumentProvider); processContext.cancelScheduledCheckpoint(); @Nullable KV> residual = @@ -278,8 +278,37 @@ public FinishBundleContext finishBundleContext(DoFn doFn) { if (residual == null) { return new Result(null, cont, null, null); } + final KV> residualForGetSize = residual; + // For a list of all DoFnInvoker arguments, see DoFn.java. + double backlogBytes = + invoker.invokeGetSize( + new DoFnInvoker.DelegatingArgumentProvider( + invokerArgumentProvider, invokerArgumentProvider.getErrorContext() + "/GetSize") { + @Override + public Object restriction() { + return residualForGetSize.getKey(); + } + + @Override + public RestrictionTracker restrictionTracker() { + return invoker.invokeNewTracker( + new DoFnInvoker.DelegatingArgumentProvider( + invokerArgumentProvider, + invokerArgumentProvider.getErrorContext() + "/NewTracker") { + + @Override + public Object restriction() { + return residualForGetSize.getKey(); + } + }); + } + }); return new Result( - residual.getKey(), cont, residual.getValue().getKey(), residual.getValue().getValue()); + residual.getKey(), + cont, + residual.getValue().getKey(), + residual.getValue().getValue(), + backlogBytes); } private class ProcessContext extends DoFn.ProcessContext diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java index 424ea567115f..a750b01963f6 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java @@ -22,6 +22,7 @@ import com.google.auto.service.AutoService; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.sdk.coders.ByteArrayCoder; import org.apache.beam.sdk.coders.Coder; @@ -281,6 +282,7 @@ public static class ProcessFn invoker; + private transient @Nullable Consumer backlogBytesCallback; public ProcessFn( DoFn fn, @@ -323,6 +325,10 @@ public void setProcessElementInvoker( this.processElementInvoker = invoker; } + public void setBacklogBytesCallback(Consumer backlogBytesCallback) { + this.backlogBytesCallback = backlogBytesCallback; + } + public DoFn getFn() { return fn; } @@ -624,6 +630,9 @@ public String getErrorContext() { } else { holdState.clear(); } + if (backlogBytesCallback != null && result.getBacklogBytes() >= 0) { + backlogBytesCallback.accept(result.getBacklogBytes()); + } } private DoFnInvoker.ArgumentProvider wrapOptionsAsSetup( diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java index 1ff66d6e517c..d311806e0a2a 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java @@ -42,6 +42,10 @@ public class Result { private final DoFn.ProcessContinuation continuation; private final @Nullable Instant futureOutputWatermark; private final @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState; + private final double backlogBytes; + + /* Constant representing an unknown amount of backlog. */ + public static final double BACKLOG_UNKNOWN = -1.0; @SuppressFBWarnings( value = "NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE", @@ -50,12 +54,27 @@ public Result( @Nullable RestrictionT residualRestriction, DoFn.ProcessContinuation continuation, @Nullable Instant futureOutputWatermark, - @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState) { + @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState, + double backlogBytes) { checkArgument(continuation != null, "continuation must not be null"); this.continuation = continuation; this.residualRestriction = residualRestriction; this.futureOutputWatermark = futureOutputWatermark; this.futureWatermarkEstimatorState = futureWatermarkEstimatorState; + this.backlogBytes = backlogBytes; + } + + public Result( + @Nullable RestrictionT residualRestriction, + DoFn.ProcessContinuation continuation, + @Nullable Instant futureOutputWatermark, + @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState) { + this( + residualRestriction, + continuation, + futureOutputWatermark, + futureWatermarkEstimatorState, + BACKLOG_UNKNOWN); } /** @@ -76,6 +95,10 @@ public DoFn.ProcessContinuation getContinuation() { public @Nullable WatermarkEstimatorStateT getFutureWatermarkEstimatorState() { return futureWatermarkEstimatorState; } + + public double getBacklogBytes() { + return backlogBytes; + } } /** diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/StepContext.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/StepContext.java index d2a03ff6ab39..e07d1a105862 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/StepContext.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/StepContext.java @@ -36,4 +36,10 @@ public interface StepContext { default BundleFinalizer bundleFinalizer() { throw new UnsupportedOperationException("BundleFinalizer is unsupported."); } + + /** + * Set the current backlog bytes for this step. This is mainly used by splittable DoFn to report + * the size of the residual restriction. + */ + default void setBacklogBytes(double backlogBytes) {} } diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvokerTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvokerTest.java index 1750cceffa0e..52ac6b1a8199 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvokerTest.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvokerTest.java @@ -95,6 +95,30 @@ public OffsetRange getInitialRestriction(@SuppressWarnings("unused") @Element Vo } } + private static class GetSizeFn extends DoFn { + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(String.valueOf(i)); + if (i == 2) { + return resume(); + } + } + return stop(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction() { + return new OffsetRange(0, 10); + } + + @GetSize + public double getSize(@Restriction OffsetRange range) { + return range.getTo() - range.getFrom(); + } + } + private SplittableProcessElementInvoker.Result runTest( int totalNumOutputs, Duration sleepBeforeFirstClaim, @@ -103,11 +127,12 @@ private SplittableProcessElementInvoker.R throws Exception { SomeFn fn = new SomeFn(sleepBeforeFirstClaim, numOutputsPerProcessCall, sleepBeforeEachOutput); OffsetRange initialRestriction = new OffsetRange(0, totalNumOutputs); - return runTest(fn, initialRestriction); + return runTest(fn, initialRestriction, Duration.standardSeconds(3)); } private SplittableProcessElementInvoker.Result runTest( - DoFn fn, OffsetRange initialRestriction) throws Exception { + DoFn fn, OffsetRange initialRestriction, Duration checkpointDuration) + throws Exception { SplittableProcessElementInvoker invoker = new OutputAndTimeBoundedSplittableProcessElementInvoker<>( fn, @@ -122,7 +147,7 @@ public void output(TupleTag tag, WindowedValue outpu NullSideInputReader.empty(), Executors.newSingleThreadScheduledExecutor(), 1000, - Duration.standardSeconds(3), + checkpointDuration, () -> { throw new UnsupportedOperationException("BundleFinalizer not configured for test."); }); @@ -215,7 +240,7 @@ public OffsetRange getInitialRestriction( } }; e.expectMessage("Output is not allowed before tryClaim()"); - runTest(brokenFn, new OffsetRange(0, 5)); + runTest(brokenFn, new OffsetRange(0, 5), Duration.standardSeconds(3)); } @Test @@ -235,6 +260,18 @@ public OffsetRange getInitialRestriction( } }; e.expectMessage("Output is not allowed after a failed tryClaim()"); - runTest(brokenFn, new OffsetRange(0, 5)); + runTest(brokenFn, new OffsetRange(0, 5), Duration.standardSeconds(3)); + } + + @Test + public void testBacklogBytes() throws Exception { + GetSizeFn fn = new GetSizeFn(); + OffsetRange initialRestriction = new OffsetRange(0, 10); + // Set a high checkpoint duration to prevent flakiness caused by early checkpointing. + SplittableProcessElementInvoker.Result res = + runTest(fn, initialRestriction, Duration.standardMinutes(3)); + // GetSizeFn claims 3 elements and then takes a checkpoint. + assertEquals(7.0, res.getBacklogBytes(), 0.001); + assertEquals(new OffsetRange(3, 10), res.getResidualRestriction()); } } diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/SplittableParDoProcessFnTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/SplittableParDoProcessFnTest.java index ef1f201ca1ee..381e41c98705 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/SplittableParDoProcessFnTest.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/SplittableParDoProcessFnTest.java @@ -140,6 +140,8 @@ private static class ProcessFnTester< private InMemoryTimerInternals timerInternals; private TestInMemoryStateInternals stateInternals; private InMemoryBundleFinalizer bundleFinalizer; + private final ProcessFn + processFn; ProcessFnTester( Instant currentProcessingTime, @@ -154,15 +156,14 @@ private static class ProcessFnTester< // encode IntervalWindow's because that's what all tests here use. WindowingStrategy windowingStrategy = (WindowingStrategy) WindowingStrategy.of(FixedWindows.of(Duration.standardSeconds(1))); - final ProcessFn - processFn = - new ProcessFn<>( - fn, - inputCoder, - restrictionCoder, - watermarkEstimatorStateCoder, - windowingStrategy, - Collections.emptyMap()); + this.processFn = + new ProcessFn<>( + fn, + inputCoder, + restrictionCoder, + watermarkEstimatorStateCoder, + windowingStrategy, + Collections.emptyMap()); this.tester = DoFnTester.of(processFn); this.timerInternals = new InMemoryTimerInternals(); this.stateInternals = new TestInMemoryStateInternals<>("dummy"); @@ -386,6 +387,61 @@ public WatermarkEstimators.Manual newWatermarkEstimator( } } + private static class GetSizeFn extends DoFn { + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(String.valueOf(i)); + if (i == 2) { + return resume(); + } + } + return stop(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction() { + return new OffsetRange(0, 10); + } + + @NewTracker + public OffsetRangeTracker newTracker(@Restriction OffsetRange range) { + return new OffsetRangeTracker(range); + } + + @GetSize + public double getSize(@Restriction OffsetRange range) { + return range.getTo() - range.getFrom(); + } + } + + // Used to check that backlog can be computed from the restriction tracker if GetSize is not + // defined. + private static class SdfWithoutGetSize extends DoFn { + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(String.valueOf(i)); + if (i == 2) { + return resume(); + } + } + return stop(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction() { + return new OffsetRange(0, 10); + } + + @NewTracker + public OffsetRangeTracker newTracker(@Restriction OffsetRange range) { + return new OffsetRangeTracker(range); + } + } + @Test public void testDrains() throws Exception { DoFn fn = new WatermarkUpdateFn(); @@ -684,4 +740,54 @@ public void testInvokesLifecycleMethods() throws Exception { tester.startElement(42, new SomeRestriction()); } } + + @Test + public void testReportsBacklog() throws Exception { + DoFn fn = new GetSizeFn(); + Instant base = Instant.now(); + final List backlogs = new ArrayList<>(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.processFn.setBacklogBytesCallback(backlogs::add); + + tester.startElement(42, new OffsetRange(0, 10)); + // First call outputs 0, 1, and 2, and then resumes. + // The residual range should be [3, 10), so size is 7. + assertEquals(1, backlogs.size()); + assertEquals(7.0, backlogs.get(0), 0.001); + } + } + + @Test + public void testReportsBacklogWithoutGetSize() throws Exception { + DoFn fn = new SdfWithoutGetSize(); + Instant base = Instant.now(); + final List backlogs = new ArrayList<>(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.processFn.setBacklogBytesCallback(backlogs::add); + + tester.startElement(42, new OffsetRange(0, 10)); + // First call outputs 0, 1, and 2, and then resumes. + // The residual range should be [3, 10), so size is 7. + assertEquals(1, backlogs.size()); + assertEquals(7.0, backlogs.get(0), 0.001); + } + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java index 93c288fea9ea..3ad443ee2a2b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java @@ -157,6 +157,7 @@ public DoFnRunner>, OutputT> crea 10000, Duration.standardSeconds(10), stepContext::bundleFinalizer)); + processFn.setBacklogBytesCallback(userStepContext::setBacklogBytes); DoFnRunner>, OutputT> simpleRunner = new SimpleDoFnRunner<>( options, diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java index f75d452b211b..e1f1b21e135b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java @@ -254,6 +254,7 @@ public void start( : WindmillTagEncodingV1.instance(); this.outputBuilder = outputBuilder; this.sideInputCache.clear(); + this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN; clearSinkFullHint(); Instant processingTime = computeProcessingTime(work.getWorkItem().getTimers().getTimersList()); @@ -528,6 +529,11 @@ public Map> flushState() { getWorkItem().getWorkToken(), activeReader); activeReader = null; + } else if (backlogBytes != UnboundedReader.BACKLOG_UNKNOWN && backlogBytes != 1L) { + // If activeReader is null, we might still have backlogBytes from an SDF. We ignore a reported + // backlogBytes of 1 since older versions of the Java SDK use this value as a default when + // RestrictionTracker.getProgress() or GetSize() are not defined. + outputBuilder.setSourceBacklogBytes(backlogBytes); } return callbacks; } @@ -726,6 +732,11 @@ public DataflowStepContext namespacedToUser() { public BundleFinalizer bundleFinalizer() { return wrapped.bundleFinalizer(); } + + @Override + public void setBacklogBytes(double backlogBytes) { + wrapped.setBacklogBytes(backlogBytes); + } } /** A {@link SideInputReader} that fetches side inputs from the streaming worker's cache. */ @@ -856,6 +867,11 @@ public void flushState() { userTimerInternals.persistTo(outputBuilder); } + @Override + public void setBacklogBytes(double backlogBytes) { + StreamingModeExecutionContext.this.backlogBytes = (long) backlogBytes; + } + @Override public TimerData getNextFiredTimer(Coder windowCoder) { if (cachedFiredSystemTimers == null) { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java index 4bfa6efc8880..a1c7609e5af1 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java @@ -431,4 +431,29 @@ public void testStateTagEncodingBasedOnConfig() { assertEquals(expectedEncoding, executionContext.getWindmillTagEncoding().getClass()); } } + + @Test + public void testSetBacklogBytes() { + Windmill.WorkItemCommitRequest.Builder outputBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + NameContext nameContext = NameContextsForTests.nameContextForTest(); + DataflowOperationContext operationContext = + executionContext.createOperationContext(nameContext); + StreamingModeExecutionContext.StepContext stepContext = + executionContext.getStepContext(operationContext); + + executionContext.start( + "key", + createMockWork( + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), + stateReader, + sideInputStateFetcher, + outputBuilder); + + stepContext.setBacklogBytes(1234.0); + executionContext.flushState(); + + assertEquals(1234, outputBuilder.getSourceBacklogBytes()); + } } diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/SpannerChangeStreamErrorTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/SpannerChangeStreamErrorTest.java index f7f2eca60bbc..9f09ab18c62e 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/SpannerChangeStreamErrorTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/SpannerChangeStreamErrorTest.java @@ -334,9 +334,10 @@ public void testInvalidRecordReceivedWithDefaultSettings() { mockChangeStreamOptions(); mockTableExists(); mockGetWatermark(startTimestamp); + mockGetPartitionSize(endTimestamp, 0); ResultSet getPartitionResultSet = mockGetParentPartition(startTimestamp, endTimestamp); - mockchangePartitionState(startTimestamp, endTimestamp, "CREATED"); - mockchangePartitionState(startTimestamp, endTimestamp, "SCHEDULED"); + mockChangePartitionState(startTimestamp, endTimestamp, "CREATED"); + mockChangePartitionState(startTimestamp, endTimestamp, "SCHEDULED"); mockGetPartitionsAfter( Timestamp.ofTimeSecondsAndNanos(startTimestamp.getSeconds(), startTimestamp.getNanos() - 1), getPartitionResultSet); @@ -497,6 +498,40 @@ private void mockGetPartitionsAfter(Timestamp timestamp, ResultSet getPartitionR StatementResult.query(getPartitionsAfterStatement, getPartitionResultSet)); } + private void mockGetPartitionSize(Timestamp timestamp, long partitionSize) { + Statement getPartitionsAfterStatement = + Statement.newBuilder( + "SELECT COUNT(*) as count FROM my-metadata-table WHERE CreatedAt > @timestamp") + .bind("timestamp") + .to(Timestamp.ofTimeSecondsAndNanos(timestamp.getSeconds(), timestamp.getNanos())) + .build(); + ResultSetMetadata metadata = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("count") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + ResultSet countResultSet = + ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues( + Value.newBuilder().setStringValue(String.valueOf(partitionSize)).build()) + .build()) + .setMetadata(metadata) + .build(); + mockSpannerService.putStatementResult( + StatementResult.query(getPartitionsAfterStatement, countResultSet)); + } + private void mockGetWatermark(Timestamp watermark) { final String minWatermark = "min_watermark"; // The query needs to sync with getUnfinishedMinWatermark() in PartitionMetadataDao file. @@ -591,7 +626,7 @@ private void mockTableExists() { StatementResult.query(tableExistsStatement, tableExistsResultSet)); } - private ResultSet mockchangePartitionState( + private ResultSet mockChangePartitionState( Timestamp startTimestamp, Timestamp after3Seconds, String state) { List composedPartitionTokens = new ArrayList<>(); composedPartitionTokens.add("Parent0"); From 285bc8cc92fada3fc05818aa93474f0c1ae5d27a Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 21 May 2026 17:24:28 -0400 Subject: [PATCH 210/490] [Prism] Fix DEADLINE_EXCEEDED errors caused by worker failures (#38523) * Set b.BundleErr correctly when dynamic split happens. * Fix deadlock on worker failures by introducing a bundle-level Done signal The cancellation signal `b.Done` is to abort bundle data streaming immediately if the worker fails or completes the bundle early. Without this signal, a deadlock scenario can occur. - Runner sends elements through data channel - SDK worker reads and processes the first element, raises an exception and sends InstructionResponse through control channel. However, at the same time, Runner keeps sending data and is blocked until SDK worker reads more data. - The existed two signals are not sufficient to do an immediate break - ctx.Done() is closed when the pipeline is done or timeout. Given the bundle failure, timeout is the only option but it is too late. - wk.StoppedChan() is closed when the worker is stopped by the worker pool, which is also not happening when the runner is waiting to send data. * Fix a problem of prism log not relaying correctly. * Trigger more python tests. * Add a mutex and safe getter/setter methods for bundleErr * Rename b.Done to b.DataAbort. * Set bundleErr before closing DataAbort to ensure error will be propagated correctly. * Refactor Respond to prevent race condition on BundleErr setting * Remove debug msg * Formatting. * Address reviewer comments. --- .../beam_PostCommit_Python_Versions.json | 2 +- .../runners/prism/internal/execute_test.go | 21 ++++++ .../pkg/beam/runners/prism/internal/stage.go | 17 +++-- .../runners/prism/internal/testdofns_test.go | 34 ++++++++++ .../runners/prism/internal/worker/bundle.go | 58 +++++++++++++++- .../prism/internal/worker/worker_test.go | 67 +++++++++++++++++++ .../runners/portability/job_server.py | 5 ++ .../runners/portability/prism_runner.py | 10 ++- .../runners/portability/prism_runner_test.py | 11 +++ 9 files changed, 213 insertions(+), 12 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json index a975cd1cd104..541dc4ea8e87 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Versions.json +++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 1 + "revision": 2 } diff --git a/sdks/go/pkg/beam/runners/prism/internal/execute_test.go b/sdks/go/pkg/beam/runners/prism/internal/execute_test.go index 2bb73f20e200..96c90678be30 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/execute_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/execute_test.go @@ -541,6 +541,27 @@ func TestFailureHang(t *testing.T) { } } +func TestFailure_SplitError(t *testing.T) { + initRunner(t) + + p, s := beam.NewPipelineWithRoot() + configs := beam.Create(s, SourceConfig{NumElements: 100, InitialSplits: 1}) + col := beam.ParDo(s, &slowFailSDF{}, configs) + beam.ParDo(s, &int64Check{Name: "sdf_fail", Want: []int{}}, col) + + // Set a short timeout so the test fails quickly if the pipeline hangs due to split error handling bug + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := executeWithT(ctx, t, p) + if err == nil { + t.Fatalf("expected pipeline failure, but got a success") + } + if want := "intentional split error from tracker"; !strings.Contains(err.Error(), want) { + t.Fatalf("expected pipeline failure with %q, but was %v", want, err) + } +} + func TestRunner_Passert(t *testing.T) { initRunner(t) tests := []struct { diff --git a/sdks/go/pkg/beam/runners/prism/internal/stage.go b/sdks/go/pkg/beam/runners/prism/internal/stage.go index c4758984af83..b46c9c2fd5b1 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/stage.go +++ b/sdks/go/pkg/beam/runners/prism/internal/stage.go @@ -207,8 +207,9 @@ progress: return context.Cause(ctx) case resp = <-b.Resp: bundleFinished = true - if b.BundleErr != nil { - return b.BundleErr + if err := b.GetErr(); err != nil { + slog.Error("stage.Execute aborting due to bundle error", "stage", s.ID, "bundle", rb.BundleID) + return err } if dataFinished && bundleFinished { break progress // exit progress loop on close. @@ -240,10 +241,16 @@ progress: sr, err := b.Split(ctx, wk, 0.5 /* fraction of remainder */, nil /* allowed splits */) if err != nil { slog.Warn("SDK Error from split, aborting splits and failing bundle", "bundle", rb, "error", err.Error()) - if b.BundleErr != nil { - b.BundleErr = err + // Safely set the split error if no primary bundle error has been set yet. + // Both SetErr and GetErr are synchronized under the same mutex, guaranteeing + // no memory races occur. The write-read inconsistency is explicitly guarded + // by the nil/null check inside SetErr(): if a concurrent primary error (e.g., + // worker crash) was set first, it will not be overwritten and b.GetErr() will + // correctly preserve and return it instead of the secondary split error. + if !b.SetErr(err) { + slog.Debug("Error for bundle already set, logging dropped split error", "bundle", rb, "error", err) } - return b.BundleErr + return b.GetErr() } if sr.GetChannelSplits() == nil { slog.Debug("SDK returned no splits", "bundle", rb) diff --git a/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go b/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go index d21ccd53afd0..903805cf659b 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go @@ -64,6 +64,7 @@ func init() { register.Function2x1(combineIntSum) register.DoFn3x1[*sdf.LockRTracker, SourceConfig, func(int64), error]((*intRangeFn)(nil)) + register.DoFn4x1[context.Context, *sdf.LockRTracker, SourceConfig, func(int64), error]((*slowFailSDF)(nil)) register.Emitter1[int64]() register.Emitter2[int64, int64]() } @@ -404,3 +405,36 @@ func (fn *selfCheckpointingDoFn) ProcessElement(rt *sdf.LockRTracker, _ []byte, } } } + +type errorSplitTracker struct { + *offsetrange.Tracker +} + +func (t *errorSplitTracker) TrySplit(fraction float64) (any, any, error) { + return nil, nil, fmt.Errorf("intentional split error from tracker") +} + +type slowFailSDF struct{} + +func (fn *slowFailSDF) CreateInitialRestriction(config SourceConfig) offsetrange.Restriction { + return offsetrange.Restriction{Start: 0, End: config.NumElements} +} + +func (fn *slowFailSDF) SplitRestriction(config SourceConfig, rest offsetrange.Restriction) []offsetrange.Restriction { + return rest.EvenSplits(config.InitialSplits) +} + +func (fn *slowFailSDF) RestrictionSize(_ SourceConfig, rest offsetrange.Restriction) float64 { + return rest.Size() +} + +func (fn *slowFailSDF) CreateTracker(rest offsetrange.Restriction) *sdf.LockRTracker { + return sdf.NewLockRTracker(&errorSplitTracker{Tracker: offsetrange.NewTracker(rest)}) +} + +func (fn *slowFailSDF) ProcessElement(ctx context.Context, rt *sdf.LockRTracker, config SourceConfig, emit func(int64)) error { + for i := rt.GetRestriction().(offsetrange.Restriction).Start; rt.TryClaim(i); i++ { + <-ctx.Done() + } + return nil +} diff --git a/sdks/go/pkg/beam/runners/prism/internal/worker/bundle.go b/sdks/go/pkg/beam/runners/prism/internal/worker/bundle.go index 15023a1b0bdc..11d59d3c8d0b 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/worker/bundle.go +++ b/sdks/go/pkg/beam/runners/prism/internal/worker/bundle.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "log/slog" + "sync" "sync/atomic" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex" @@ -61,8 +62,19 @@ type B struct { dataSema atomic.Int32 OutputData engine.TentativeData - Resp chan *fnpb.ProcessBundleResponse - BundleErr error + Resp chan *fnpb.ProcessBundleResponse + // DataAbort is closed when the worker responds to the bundle instruction + // (with success or failure), signaling ProcessOn to stop streaming data. + // + // This prevents a deadlock where a worker fails mid-bundle and stops reading + // from the data channel while the runner blocks indefinitely attempting to + // write remaining elements. Other signals are insufficient to abort immediately: + // - ctx.Done() only triggers on global timeouts/cancellations, which is too late. + // - wk.StoppedChan is only closed when tearing down the worker pool, which does + // not happen while the runner is waiting on the current bundle to finish. + DataAbort chan struct{} + mu sync.Mutex + bundleErr error responded bool SinkToPCollection map[string]string @@ -80,6 +92,7 @@ func (b *B) Init() { close(b.DataWait) // Can happen if there are no outputs for the bundle. } b.Resp = make(chan *fnpb.ProcessBundleResponse, 1) + b.DataAbort = make(chan struct{}) } // DataOrTimerDone indicates a final element has been received from a Data or Timer output. @@ -96,14 +109,40 @@ func (b *B) LogValue() slog.Value { slog.String("stage", b.PBDID)) } +// SetErr sets the bundle error if it is not already set, returning true if it was set, and false otherwise. +func (b *B) SetErr(err error) bool { + b.mu.Lock() + defer b.mu.Unlock() + if b.bundleErr == nil { + b.bundleErr = err + return true + } + return false +} + +// GetErr gets the current bundle error. +func (b *B) GetErr() error { + b.mu.Lock() + defer b.mu.Unlock() + return b.bundleErr +} + func (b *B) Respond(resp *fnpb.InstructionResponse) { if b.responded { slog.Warn("additional bundle response", "bundle", b, "resp", resp) return } b.responded = true + if b.DataAbort != nil { + // Defer closing DataAbort to guarantee that the abort signal is sent + // when this function returns. This ensures it is always executed after + // any error has been safely written and synchronized via b.SetErr() or, + // in the happy path, after the successful response is sent to b.Resp. + defer close(b.DataAbort) + } if resp.GetError() != "" { - b.BundleErr = fmt.Errorf("bundle %v %v failed:%v", resp.GetInstructionId(), b.PBDID, resp.GetError()) + slog.Error("Prism received bundle error from worker response", "bundle", resp.GetInstructionId()) + b.SetErr(fmt.Errorf("bundle %v %v failed:%v", resp.GetInstructionId(), b.PBDID, resp.GetError())) close(b.Resp) return } @@ -143,6 +182,13 @@ func (b *B) ProcessOn(ctx context.Context, wk *W) <-chan struct{} { b.DataOrTimerDone() } return b.DataWait + case <-b.DataAbort: + // The bundle completed/failed before req was sent. + outCap := b.OutputCount + len(b.HasTimers) + for i := 0; i < outCap; i++ { + b.DataOrTimerDone() + } + return b.DataWait case wk.InstReqs <- req: // desired outcome } @@ -181,6 +227,9 @@ func (b *B) ProcessOn(ctx context.Context, wk *W) <-chan struct{} { case <-ctx.Done(): b.DataOrTimerDone() return b.DataWait + case <-b.DataAbort: + b.DataOrTimerDone() + return b.DataWait case wk.DataReqs <- elms: } } @@ -202,6 +251,9 @@ func (b *B) ProcessOn(ctx context.Context, wk *W) <-chan struct{} { case <-ctx.Done(): b.DataOrTimerDone() return b.DataWait + case <-b.DataAbort: + b.DataOrTimerDone() + return b.DataWait case wk.DataReqs <- &fnpb.Elements{ Timers: timers, Data: []*fnpb.Elements_Data{ diff --git a/sdks/go/pkg/beam/runners/prism/internal/worker/worker_test.go b/sdks/go/pkg/beam/runners/prism/internal/worker/worker_test.go index 76a05563ec38..055a003e2765 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/worker/worker_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/worker/worker_test.go @@ -522,6 +522,73 @@ func TestWorker_State_MultimapSideInput(t *testing.T) { } } +// TestBundle_ProcessOn_WorkerFailure verifies that the runner does not deadlock when +// a worker fails mid-bundle and stops reading elements from the Data plane stream. +func TestBundle_ProcessOn_WorkerFailure(t *testing.T) { + ctx, wk, clientConn := serveTestWorker(t) + + dataCli := fnpb.NewBeamFnDataClient(clientConn) + dataStream, err := dataCli.Data(ctx) + if err != nil { + t.Fatal("couldn't create data client:", err) + } + + instID := wk.NextInst() + + // Create 15 large input blocks (512 KB each) to saturate the 10-slot channel buffer + // and the gRPC flow control window, forcing the Data sender inside worker.go to block. + largeBytes := make([]byte, 512*1024) + var inputBlocks []*engine.Block + for i := 0; i < 15; i++ { + inputBlocks = append(inputBlocks, &engine.Block{ + Kind: engine.BlockData, + Bytes: [][]byte{largeBytes}, + }) + } + + b := &B{ + InstID: instID, + PBDID: "teststageID", + Input: inputBlocks, + OutputCount: 1, + } + b.Init() + wk.activeInstructions[instID] = b + + processOnDone := make(chan struct{}) + go func() { + b.ProcessOn(ctx, wk) + close(processOnDone) + }() + + // Send the initial process bundle request trigger. + wk.InstReqs <- &fnpb.InstructionRequest{ + InstructionId: instID, + } + + // Read only the first block to simulate worker processing start. + _, err = dataStream.Recv() + if err != nil { + t.Fatal("couldn't receive first data element:", err) + } + + // Simulate worker failure by responding with an error on the Control channel. + // Without the fix, ProcessOn's background goroutine deadlocks at `wk.DataReqs <- elms` + // because the client stopped reading and the buffer/flow-control is saturated. + wk.activeInstructions[instID].Respond(&fnpb.InstructionResponse{ + InstructionId: instID, + Error: "Intentional worker failure", + }) + + // Verify that ProcessOn exits cleanly and does not deadlock/hang. + select { + case <-processOnDone: + // Test passed: ProcessOn exited successfully! + case <-time.After(10 * time.Second): + t.Fatal("ProcessOn deadlocked / hung after worker failure!") + } +} + func newWorker() *W { mw := &MultiplexW{ pool: map[string]*W{}, diff --git a/sdks/python/apache_beam/runners/portability/job_server.py b/sdks/python/apache_beam/runners/portability/job_server.py index 53688e0be950..8bdad6b70fea 100644 --- a/sdks/python/apache_beam/runners/portability/job_server.py +++ b/sdks/python/apache_beam/runners/portability/job_server.py @@ -121,6 +121,11 @@ def start(self): logger = logging.getLogger(f"{self.__class__.__name__}") if self._log_filter is not None: logger.addFilter(self._log_filter) + # Explicitly set logger level to INFO so logger.info(...) calls in + # SubprocessServer._really_start_process's log_stdout pass the initial + # isEnabledFor check, allowing the filter to dynamically elevate log + # levels to WARNING or ERROR. + logger.setLevel(logging.INFO) self._server = subprocess_server.SubprocessServer( beam_job_api_pb2_grpc.JobServiceStub, cmd, port=port, logger=logger) return self._server.start() diff --git a/sdks/python/apache_beam/runners/portability/prism_runner.py b/sdks/python/apache_beam/runners/portability/prism_runner.py index 0458898b5e96..105c27fcb8c9 100644 --- a/sdks/python/apache_beam/runners/portability/prism_runner.py +++ b/sdks/python/apache_beam/runners/portability/prism_runner.py @@ -154,9 +154,13 @@ def filter(self, record): record.msg = json_record["sdk"]["msg"] else: record.name = "PrismRunner" - record.msg = ( - f"{json_record['msg']} " - f"({', '.join(f'{k}={v!r}' for k, v in extras.items())})") + formatted_extras = [] + for k, v in extras.items(): + if isinstance(v, str) and '\n' in v: + formatted_extras.append(f"\n{k}:\n{v}") + else: + formatted_extras.append(f"{k}={v!r}") + record.msg = f"{json_record['msg']} ({', '.join(formatted_extras)})" except (json.JSONDecodeError, KeyError, ValueError, diff --git a/sdks/python/apache_beam/runners/portability/prism_runner_test.py b/sdks/python/apache_beam/runners/portability/prism_runner_test.py index 9c1620603fd3..bb6b7bb04da6 100644 --- a/sdks/python/apache_beam/runners/portability/prism_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/prism_runner_test.py @@ -300,6 +300,17 @@ def test_after_count_trigger_streaming(self): ('B-3', {10, 15, 16}), ]))) + def test_failing_dofn(self): + # Prism interprets all bundle failures as RuntimeError + with self.assertRaisesRegex( + RuntimeError, "Intentional DoFn failure for prism logging test"): + with self.create_pipeline() as p: + + def failing_fn(x): + raise ValueError("Intentional DoFn failure for prism logging test") + + _ = p | beam.Create([1, 2, 3]) | beam.Map(failing_fn) + class PrismJobServerTest(unittest.TestCase): def setUp(self) -> None: From 90934e8299b252c84d5742b1228b68467f435c30 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 21 May 2026 21:15:41 -0400 Subject: [PATCH 211/490] Suppress log spams in gcsio 3.0 (#38588) --- .../apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java index 97778ac4e1df..1ad08f0ba1a2 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java @@ -71,6 +71,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; @@ -184,6 +185,7 @@ public boolean shouldRetry(IOException e) { return RetryDeterminer.SOCKET_ERRORS.shouldRetry(e); } }; + private static final AtomicBoolean overwriteLog = new AtomicBoolean(false); ///////////////////////////////////////////////////////////////////////////// @@ -726,9 +728,16 @@ public WritableByteChannel create(GcsPath path, CreateOptions options) throws IO } } + @SuppressFBWarnings("LG_LOST_LOGGER_DUE_TO_WEAK_REFERENCE") GoogleCloudStorage createGoogleCloudStorage( GoogleCloudStorageOptions options, Storage storage, Credentials credentials) throws IOException { + // Suppress log spams in gcsio 3.0 + if (overwriteLog.compareAndSet(false, true)) { + java.util.logging.Logger.getLogger("com.google.cloud.hadoop.gcsio.GoogleCloudStorageImpl") + .setLevel(java.util.logging.Level.SEVERE); + } + return GoogleCloudStorageImpl.builder() .setOptions(options) .setHttpTransport(storage.getRequestFactory().getTransport()) From 2d0b3d67c75b49ba05eaf3b7eafd1767203e2884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 07:46:11 -0400 Subject: [PATCH 212/490] Bump golang.org/x/net from 0.54.0 to 0.55.0 in /sdks (#38595) --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 48a4c753100f..fc79db727abf 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -55,10 +55,10 @@ require ( github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 - golang.org/x/net v0.54.0 + golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 google.golang.org/api v0.280.0 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 diff --git a/sdks/go.sum b/sdks/go.sum index 23a2cb5ea07c..4060068e9e75 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1052,8 +1052,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1176,8 +1176,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= From b1f7e25d419590f96727f7109dac76806fd6919f Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Fri, 22 May 2026 09:38:28 -0400 Subject: [PATCH 213/490] Revert "huggingface model handler for yaml - retry (#38451)" (#38601) This reverts commit 65e8b655195bf9886c402c5ca27c28fe783d5991. --- .../beam_PostCommit_Yaml_Xlang_Direct.json | 2 +- .../beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- .../beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- ...erence_vertexai.yaml => runinference.yaml} | 0 .../yaml/tests/runinference_huggingface.yaml | 62 ------------------- sdks/python/apache_beam/yaml/yaml_ml.py | 49 --------------- sdks/python/build.gradle | 19 +----- 7 files changed, 5 insertions(+), 131 deletions(-) rename sdks/python/apache_beam/yaml/tests/{runinference_vertexai.yaml => runinference.yaml} (100%) delete mode 100644 sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml diff --git a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json index 8ed972c9f579..541dc4ea8e87 100644 --- a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 3 + "revision": 2 } diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index afa437b64de1..ea1c255f7cc9 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -80,7 +80,7 @@ jobs: - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} + gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 0b8f4cd63939..7d17fd2140c9 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -91,7 +91,7 @@ jobs: - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:yamlIntegrationTests + gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml b/sdks/python/apache_beam/yaml/tests/runinference.yaml similarity index 100% rename from sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml rename to sdks/python/apache_beam/yaml/tests/runinference.yaml diff --git a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml deleted file mode 100644 index 8728a6f544ad..000000000000 --- a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml +++ /dev/null @@ -1,62 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -pipelines: - - pipeline: - type: chain - transforms: - - type: Create - config: - elements: - - text: "I love Apache Beam!" - - text: "I hate this error." - - type: RunInference - config: - model_handler: - type: "HuggingFacePipeline" - config: - task: "text-classification" - inference_fn: - callable: | - def real_inference(batch, pipeline, inference_args): - predictions = pipeline(batch, **inference_args) - - # If it's a single dictionary (batch size of 1), wrap it in a list - if isinstance(predictions, dict): - predictions = [predictions] - - return { - 'label': [p['label'] for p in predictions], - 'score': [p['score'] for p in predictions] - } - preprocess: - callable: 'lambda x: x.text' - - type: MapToFields - config: - language: python - fields: - text: text - sentiment: - callable: 'lambda x: x.inference.inference["label"]' - - type: AssertEqual - config: - elements: - - text: "I love Apache Beam!" - sentiment: "POSITIVE" - - text: "I hate this error." - sentiment: "NEGATIVE" - - options: - yaml_experimental_features: ['ML'] diff --git a/sdks/python/apache_beam/yaml/yaml_ml.py b/sdks/python/apache_beam/yaml/yaml_ml.py index 05cbed3bd456..51f18c733046 100644 --- a/sdks/python/apache_beam/yaml/yaml_ml.py +++ b/sdks/python/apache_beam/yaml/yaml_ml.py @@ -282,55 +282,6 @@ def inference_output_type(self): ('model_id', Optional[str])]) -@ModelHandlerProvider.register_handler_type('HuggingFacePipeline') -class HuggingFacePipelineProvider(ModelHandlerProvider): - def __init__( - self, - task: Optional[str] = None, - model: Optional[str] = None, - preprocess: Optional[dict[str, str]] = None, - postprocess: Optional[dict[str, str]] = None, - device: Optional[Any] = None, - inference_fn: Optional[dict[str, str]] = None, - load_pipeline_args: Optional[dict[str, Any]] = None, - **kwargs): - try: - from apache_beam.ml.inference.huggingface_inference import HuggingFacePipelineModelHandler - except ImportError: - raise ValueError( - 'Unable to import HuggingFacePipelineModelHandler. Please ' - 'install transformers dependencies.') - - kwargs = {k: v for k, v in kwargs.items() if not k.startswith('_')} - - inference_fn_obj = self.parse_processing_transform( - inference_fn, 'inference_fn') if inference_fn else None - - handler_kwargs = {} - if inference_fn_obj: - handler_kwargs['inference_fn'] = inference_fn_obj - - _handler = HuggingFacePipelineModelHandler( - task=task, - model=model, - device=device, - load_pipeline_args=load_pipeline_args, - **handler_kwargs, - **kwargs) - - super().__init__(_handler, preprocess, postprocess) - - @staticmethod - def validate(config): - if not config.get('task') and not config.get('model'): - raise ValueError( - "HuggingFacePipeline requires either 'task' or " - "'model' to be specified.") - - def inference_output_type(self): - return Any - - @beam.ptransform.ptransform_fn def run_inference( pcoll, diff --git a/sdks/python/build.gradle b/sdks/python/build.gradle index 837631868b8a..5f09dff57e8f 100644 --- a/sdks/python/build.gradle +++ b/sdks/python/build.gradle @@ -124,25 +124,10 @@ tasks.register("generateYamlDocs") { outputs.file "${buildDir}/yaml-examples.html" } -tasks.register("installYamlIntegrationTestDeps") { - dependsOn installGcpTest - doLast { - exec { - executable 'sh' - args '-c', ". ${envdir}/bin/activate && " + - "py_ver=\$(python -c 'import sys; print(f\"{sys.version_info.major}{sys.version_info.minor}\")') && " + - "ml_extra=\"ml_test\" && " + - "if [ \"\$py_ver\" -ge 313 ]; then ml_extra=\"p\${py_ver}_ml_test\"; fi && " + - "echo \"Installing dependencies...\" && " + - "pip install --pre --retries 10 ${buildDir}/apache-beam.tar.gz[\$ml_extra,yaml,transformers]" - } - } -} - tasks.register("yamlIntegrationTests") { description "Runs precommit integration tests for yaml pipelines." - dependsOn installYamlIntegrationTestDeps + dependsOn installGcpTest // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" @@ -161,7 +146,7 @@ tasks.register("yamlIntegrationTests") { tasks.register("postCommitYamlIntegrationTests") { description "Runs postcommit integration tests for yaml pipelines - parameterized by yamlTestSet." - dependsOn installYamlIntegrationTestDeps + dependsOn installGcpTest // Need to build all expansion services referenced in apache_beam/yaml/*.* // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" From e090598847ba2ae54f7f64e3875f10d69745190a Mon Sep 17 00:00:00 2001 From: parveensania Date: Fri, 22 May 2026 07:22:33 -0700 Subject: [PATCH 214/490] Fix data_race condition in WindmillStreamSenderTest (#38589) * Instead of memoizing Backoff constructed instance, memoize the builder config instead * fix indendation * indendation fix --- .../windmill/client/grpc/GrpcWindmillStreamFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java index 244d2ad3fa14..0184b88d53cd 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java @@ -117,13 +117,13 @@ private GrpcWindmillStreamFactory( this.streamingRpcBatchLimit = streamingRpcBatchLimit; this.windmillMessagesBetweenIsReadyChecks = windmillMessagesBetweenIsReadyChecks; // Configure backoff to retry calls forever, with a maximum sane retry interval. - this.grpcBackOff = + Supplier backoffConfig = Suppliers.memoize( () -> FluentBackoff.DEFAULT .withInitialBackoff(MIN_BACKOFF) - .withMaxBackoff(maxBackOffSupplier.get()) - .backoff()); + .withMaxBackoff(maxBackOffSupplier.get())); + this.grpcBackOff = () -> backoffConfig.get().backoff(); this.streamRegistry = ConcurrentHashMap.newKeySet(); this.sendKeyedGetDataRequests = sendKeyedGetDataRequests; this.requestBatchedGetWorkResponse = requestBatchedGetWorkResponse; From e2f9470683be7112ab701d517f5b715a3c4ca7e2 Mon Sep 17 00:00:00 2001 From: Deji Ibrahim <31637316+dejii@users.noreply.github.com> Date: Fri, 22 May 2026 15:58:57 +0100 Subject: [PATCH 215/490] [Iceberg] Fix manifest bounds being padded with trailing 0x00 bytes (#38580) * [Iceberg] Fix manifest bounds being padded with trailing 0x00 bytes * update changes.md * test: encode bound ByteBuffers via Conversions.toByteBuffer * trigger build --- CHANGES.md | 1 + .../sdk/io/iceberg/SerializableDataFile.java | 14 +++- .../io/iceberg/SerializableDataFileTest.java | 65 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 52475a99d8e1..fa4fdb096601 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -98,6 +98,7 @@ * Fixed BigQueryEnrichmentHandler batch mode dropping earlier requests when multiple requests share the same enrichment key (Python) ([#38035](https://github.com/apache/beam/issues/38035)). * Added `max_batch_duration_secs` passthrough support in Python Enrichment BigQuery and CloudSQL handlers so batching duration can be forwarded to `BatchElements` ([#38243](https://github.com/apache/beam/issues/38243)). +* Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). ## Security Fixes diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java index 1f717b82c21e..9e75be0a1987 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java @@ -205,11 +205,23 @@ DataFile createDataFile(Map partitionSpecs) { } Map output = new HashMap<>(input.size()); for (Map.Entry e : input.entrySet()) { - output.put(e.getKey(), e.getValue().array()); + output.put(e.getKey(), toByteArray(e.getValue())); } return output; } + // Copy only [position, limit). ByteBuffer.array() returns the full backing + // array, which is sometimes larger than the buffer's content (e.g. trailing + // 0x00 bytes). Leaking those into manifest bounds shifts the lower bound + // above the real min and breaks equality predicate pushdown in some query + // engines. + private static byte[] toByteArray(ByteBuffer buf) { + ByteBuffer view = buf.duplicate(); + byte[] bytes = new byte[view.remaining()]; + view.get(bytes); + return bytes; + } + private static @Nullable Map toByteBufferMap( @Nullable Map input) { if (input == null) { diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java index 983f021fd7ce..d4e7793718d8 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java @@ -17,13 +17,27 @@ */ package org.apache.beam.sdk.io.iceberg; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.types.Conversions; +import org.apache.iceberg.types.Types; import org.junit.Test; /** @@ -73,4 +87,55 @@ public void testFieldsInEqualsMethodInSyncWithGetterFields() { + "to this test class's FIELDS_SET."); } } + + /** + * Bounds with {@code capacity > limit} must be copied by {@code [position, limit)}, not by {@link + * ByteBuffer#array()}. Otherwise trailing 0x00 bytes leak into the manifest bounds and break + * equality predicate pushdown in some query engines. + */ + @Test + public void testBoundByteBufferIsCopiedByLimitNotBackingArrayLength() { + // Encode bounds the same way iceberg-parquet does in the wild — via + // Conversions.toByteBuffer(STRING, value). For UTF-8 strings of 10+ + // characters the underlying JDK CharsetEncoder over-allocates by ~10% + // and flips, producing a ByteBuffer with capacity > limit. + int columnId = 3; + String lowerValue = "lower_bound_str"; + String upperValue = "upper_bound_str"; + byte[] expectedLower = lowerValue.getBytes(StandardCharsets.UTF_8); + byte[] expectedUpper = upperValue.getBytes(StandardCharsets.UTF_8); + + ByteBuffer lower = Conversions.toByteBuffer(Types.StringType.get(), lowerValue); + ByteBuffer upper = Conversions.toByteBuffer(Types.StringType.get(), upperValue); + + Map lowerBounds = new HashMap<>(); + lowerBounds.put(columnId, lower); + Map upperBounds = new HashMap<>(); + upperBounds.put(columnId, upper); + + Metrics metrics = new Metrics(1L, null, null, null, null, lowerBounds, upperBounds); + + DataFile dataFile = + DataFiles.builder(PartitionSpec.unpartitioned()) + .withFormat(FileFormat.PARQUET) + .withPath("gs://test-bucket/data/test-file.parquet") + .withFileSizeInBytes(1024L) + .withMetrics(metrics) + .build(); + + SerializableDataFile serialized = SerializableDataFile.from(dataFile, ""); + + byte[] serializedLower = serialized.getLowerBounds().get(columnId); + byte[] serializedUpper = serialized.getUpperBounds().get(columnId); + assertEquals( + "lower bound length must match content, not backing array", + expectedLower.length, + serializedLower.length); + assertEquals( + "upper bound length must match content, not backing array", + expectedUpper.length, + serializedUpper.length); + assertArrayEquals(expectedLower, serializedLower); + assertArrayEquals(expectedUpper, serializedUpper); + } } From 54c24c4411c3fca0db62bb285bee62886c317804 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 11:19:56 -0400 Subject: [PATCH 216/490] Bump docker/build-push-action from 7.1.0 to 7.2.0 (#38597) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.1.0 to 7.2.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/bcafcacb16a39f128d818304e6c9c0c18556b85f...f9f3042f7e2789586610d6e8b85c8f03e5195baf) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_runner_image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index 7990ce0a4859..e866ecf96933 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - name: Build and Load to docker - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: context: ${{ env.working-directory }} load: true @@ -57,7 +57,7 @@ jobs: - name: Push Docker image if: github.ref == 'refs/heads/master' id: docker_build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: context: ${{ env.working-directory }} push: true From e48d07008baa8f68b2f97d4e40f080e8d947807a Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 22 May 2026 19:06:10 +0200 Subject: [PATCH 217/490] Fix cancelled run handling in flaky test prefetcher (#38579) Co-authored-by: Cursor --- .../metrics/sync/github/github_runs_prefetcher/code/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py index 044866813fa7..76d8e488bce5 100644 --- a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py +++ b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py @@ -182,7 +182,7 @@ def filter_workflow_runs(run, issue): success_rate = 1.0 if len(workflow_runs): - failed_runs = list(filter(lambda r: r.status == "failure" | r.status == "cancelled", workflow_runs)) + failed_runs = list(filter(lambda r: r.status == "failure" or r.status == "cancelled", workflow_runs)) print(f"Number of failed workflow runs: {len(failed_runs)}") success_rate -= len(failed_runs) / len(workflow_runs) From 43cca9ae7d40cc468b7d172ec971bc186d05d179 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Fri, 22 May 2026 13:07:35 -0400 Subject: [PATCH 218/490] increase timeout (#38604) --- .github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 7d17fd2140c9..34a1af741f74 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -70,7 +70,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Yaml_Xlang_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 + timeout-minutes: 180 name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) strategy: matrix: From f6f4c5ae9609812a02fdc60ae1fb725b031c0ea6 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Fri, 22 May 2026 13:37:51 -0400 Subject: [PATCH 219/490] Revert "Bump golangci/golangci-lint-action from 3 to 8 (#36291)" (#38606) This reverts commit f869272095d55281d44e287ba5000c6bcbd573c4. --- .github/workflows/tour_of_beam_backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml index 4271020ad403..3632a728d4a2 100644 --- a/.github/workflows/tour_of_beam_backend.yml +++ b/.github/workflows/tour_of_beam_backend.yml @@ -58,7 +58,7 @@ jobs: run: go test -v ./... - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v3 with: version: v1.49.0 working-directory: learning/tour-of-beam/backend From e79f5ea1f823ad358805cf45e3ff7378ef73d2df Mon Sep 17 00:00:00 2001 From: Andrew Kabas Date: Fri, 22 May 2026 14:04:00 -0400 Subject: [PATCH 220/490] Report source lineage from HadoopFormatIO (#37265) * Report source lineage from HadoopFormatIO * Extract shared code into FileSystems public API * A little refactoring * Fix import in HadoopFormatIO * check style * Implement lineage in HDFS and add tests * address review comments * Address review comments --- .../beam/sdk/io/hdfs/HadoopFileSystem.java | 17 +++++++++++++ .../sdk/io/hdfs/HadoopFileSystemTest.java | 20 +++++++++++++++ .../sdk/io/hadoop/format/HadoopFormatIO.java | 25 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java b/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java index 4de1d539e76a..00a77aa84476 100644 --- a/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java +++ b/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java @@ -38,6 +38,7 @@ import org.apache.beam.sdk.io.fs.MatchResult.Metadata; import org.apache.beam.sdk.io.fs.MatchResult.Status; import org.apache.beam.sdk.io.fs.MoveOptions; +import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.hadoop.conf.Configuration; @@ -336,6 +337,22 @@ protected String getScheme() { return scheme; } + @Override + protected void reportLineage(HadoopResourceId resourceId, Lineage lineage, LineageLevel level) { + URI uri = resourceId.toPath().toUri(); + ImmutableList.Builder segments = ImmutableList.builder(); + if (uri.getAuthority() != null && !uri.getAuthority().isEmpty()) { + segments.add(uri.getAuthority()); + } + if (level != LineageLevel.TOP_LEVEL + && uri.getPath() != null + && !uri.getPath().isEmpty() + && !uri.getPath().equals("/")) { + segments.add(uri.getPath()); + } + lineage.add(scheme, segments.build(), "/"); + } + /** An adapter around {@link FSDataInputStream} that implements {@link SeekableByteChannel}. */ private static class HadoopSeekableByteChannel implements SeekableByteChannel { private final FileStatus fileStatus; diff --git a/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java b/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java index c5918a8c9aa6..e0b299401c76 100644 --- a/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java +++ b/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java @@ -24,6 +24,9 @@ import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.io.FileNotFoundException; import java.io.InputStream; @@ -37,12 +40,14 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import org.apache.beam.sdk.io.FileSystem; import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.io.TextIO; import org.apache.beam.sdk.io.fs.CreateOptions.StandardCreateOptions; import org.apache.beam.sdk.io.fs.MatchResult; import org.apache.beam.sdk.io.fs.MatchResult.Metadata; import org.apache.beam.sdk.io.fs.MatchResult.Status; +import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.sdk.testing.ExpectedLogs; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; @@ -481,6 +486,21 @@ public void testReadPipeline() throws Exception { p.run(); } + @Test + public void testReportLineage() { + verifyLineage( + "hdfs://namenode/path/to/file.txt", ImmutableList.of("namenode", "/path/to/file.txt")); + verifyLineage("hdfs://namenode/", ImmutableList.of("namenode")); + verifyLineage("hdfs://namenode", ImmutableList.of("namenode")); + } + + private void verifyLineage(String uri, List expected) { + HadoopResourceId resourceId = new HadoopResourceId(URI.create(uri)); + Lineage mockLineage = mock(Lineage.class); + fileSystem.reportLineage(resourceId, mockLineage, FileSystem.LineageLevel.FILE); + verify(mockLineage, times(1)).add("hdfs", expected, "/"); + } + private void create(String relativePath, byte[] contents) throws Exception { try (WritableByteChannel channel = fileSystem.create( diff --git a/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java b/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java index 0412e4286bb8..501040928e41 100644 --- a/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java +++ b/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java @@ -34,6 +34,7 @@ import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -54,6 +55,7 @@ import org.apache.beam.sdk.io.BoundedSource; import org.apache.beam.sdk.io.hadoop.SerializableConfiguration; import org.apache.beam.sdk.io.hadoop.WritableCoder; +import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.Create; @@ -97,6 +99,7 @@ import org.apache.hadoop.mapreduce.TaskAttemptID; import org.apache.hadoop.mapreduce.TaskID; import org.apache.hadoop.mapreduce.lib.db.DBConfiguration; +import org.apache.hadoop.mapreduce.lib.input.FileSplit; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.mapreduce.task.JobContextImpl; import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl; @@ -725,6 +728,7 @@ public List>> split(long desiredBundleSizeBytes, Pipeline return ImmutableList.of(this); } computeSplitsIfNecessary(); + reportSourceLineage(inputSplits); LOG.info( "Generated {} splits. Size of first split is {} ", inputSplits.size(), @@ -744,6 +748,27 @@ public List>> split(long desiredBundleSizeBytes, Pipeline .collect(Collectors.toList()); } + private void reportSourceLineage(final List inputSplits) { + for (SerializableSplit serializableSplit : inputSplits) { + InputSplit split = serializableSplit.getSplit(); + if (split instanceof FileSplit) { + URI uri = ((FileSplit) split).getPath().toUri(); + String scheme = uri.getScheme(); + if (scheme == null) { + continue; + } + ImmutableList.Builder segments = ImmutableList.builder(); + if (uri.getAuthority() != null && !uri.getAuthority().isEmpty()) { + segments.add(uri.getAuthority()); + } + if (uri.getPath() != null && !uri.getPath().isEmpty() && !uri.getPath().equals("/")) { + segments.add(uri.getPath()); + } + Lineage.getSources().add(scheme, segments.build(), "/"); + } + } + } + @Override public long getEstimatedSizeBytes(PipelineOptions po) throws Exception { if (inputSplit == null) { From 360d99572ae00d69c62fe9369d25967462869d41 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Fri, 22 May 2026 15:26:31 -0400 Subject: [PATCH 221/490] Pipe Valuekind through DoFn and output builders (#38490) * add valuekind * add tests and DirectRunner support * add some doc to model * add some doc to model * add missing noteOutput * add to ParamWindowedValueCoder * add to windmill keyed work item * add missing pieces to Dataflow runner, fnapi, spark * remove premature dataflow enabliung * address comments * remove duplicate method * remove extra methods from DoFn * cleanup * cleanup * create a valuekind helper in df worker * cleanup --- ...oundedSplittableProcessElementInvoker.java | 11 + .../beam/runners/core/SimpleDoFnRunner.java | 21 + .../runners/dataflow/BatchViewOverrides.java | 10 +- .../RedistributeByKeyOverrideFactory.java | 1 + .../runners/dataflow/DataflowRunnerTest.java | 143 ++++ .../worker/UngroupedWindmillReader.java | 3 +- .../worker/WindmillKeyedWorkItem.java | 3 +- .../runners/dataflow/worker/WindmillSink.java | 1 + .../worker/WindmillValueKindHelper.java | 60 ++ .../StreamingGroupAlsoByWindowFnsTest.java | 6 + .../org/apache/beam/sdk/transforms/DoFn.java | 18 + .../beam/sdk/transforms/DoFnTester.java | 25 +- .../beam/sdk/transforms/Redistribute.java | 1 + .../org/apache/beam/sdk/transforms/Reify.java | 5 +- .../apache/beam/sdk/transforms/Reshuffle.java | 1 + .../reflect/ByteBuddyDoFnInvokerFactory.java | 11 + .../sdk/transforms/reflect/DoFnInvoker.java | 15 + .../sdk/transforms/reflect/DoFnSignature.java | 26 + .../transforms/reflect/DoFnSignatures.java | 8 + .../SplittableParDoNaiveBounded.java | 11 + .../beam/sdk/values/WindowedValues.java | 3 +- .../beam/sdk/transforms/ValueKindTest.java | 623 ++++++++++++++++++ .../beam/sdk/util/WindowedValueTest.java | 33 + .../beam/fn/harness/FnApiDoFnRunner.java | 31 +- .../apache/beam/sdk/io/delta/DeltaIOTest.java | 11 +- 25 files changed, 1059 insertions(+), 22 deletions(-) create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillValueKindHelper.java create mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ValueKindTest.java diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java index dbbcfe8ee317..9e9524957f50 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java @@ -52,6 +52,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; @@ -200,6 +201,11 @@ public CausedByDrain causedByDrain(DoFn doFn) { return processContext.causedByDrain(); } + @Override + public ValueKind valueKind(DoFn doFn) { + return processContext.valueKind(); + } + @Override public RestrictionTracker restrictionTracker() { return processContext.tracker; @@ -436,6 +442,11 @@ public CausedByDrain causedByDrain() { return element.causedByDrain(); } + @Override + public ValueKind valueKind() { + return element.getValueKind(); + } + @Override public PipelineOptions getPipelineOptions() { return pipelineOptions; diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java index d553a7be2d44..9859d672b3e8 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java @@ -61,6 +61,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowingStrategy; @@ -506,6 +507,11 @@ public Instant timestamp() { return elem.getRecordOffset(); } + @Override + public ValueKind valueKind() { + return elem.getValueKind(); + } + public Collection windows() { return elem.getWindows(); } @@ -597,6 +603,11 @@ public CausedByDrain causedByDrain(DoFn doFn) { return elem.causedByDrain(); } + @Override + public ValueKind valueKind(DoFn doFn) { + return elem.getValueKind(); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException( @@ -895,6 +906,11 @@ public CausedByDrain causedByDrain(DoFn doFn) { return causedByDrain; } + @Override + public ValueKind valueKind(DoFn doFn) { + throw new UnsupportedOperationException("ValueKind parameters are not supported."); + } + @Override public String timerId(DoFn doFn) { return timerId; @@ -1228,6 +1244,11 @@ public TimeDomain timeDomain(DoFn doFn) { "Cannot access time domain outside of @ProcessTimer method."); } + @Override + public ValueKind valueKind(DoFn doFn) { + throw new UnsupportedOperationException("ValueKind parameters are not supported."); + } + @Override public KeyT key() { return key; diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java index b15b3f3834d2..1fec6d368980 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/BatchViewOverrides.java @@ -1381,6 +1381,11 @@ public T getValue() { return value; } + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; @@ -1416,11 +1421,6 @@ public PaneInfo getPaneInfo() { return null; } - @Override - public ValueKind getValueKind() { - return ValueKind.INSERT; - } - @Override public Iterable> explodeWindows() { return Collections.emptyList(); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/RedistributeByKeyOverrideFactory.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/RedistributeByKeyOverrideFactory.java index 47ff5b764910..15a26530f3de 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/RedistributeByKeyOverrideFactory.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/RedistributeByKeyOverrideFactory.java @@ -141,6 +141,7 @@ public void processElement( .setTimestamp(kv.getValue().getTimestamp()) .setWindow(kv.getValue().getWindow()) .setPaneInfo(kv.getValue().getPaneInfo()) + .setValueKind(kv.getValue().getValueKind()) .output(); } })); diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java index ab3b62a0aa1b..f2f8b44a1626 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java @@ -154,6 +154,7 @@ import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Redistribute; +import org.apache.beam.sdk.transforms.Reshuffle; import org.apache.beam.sdk.transforms.SerializableFunctions; import org.apache.beam.sdk.transforms.SimpleFunction; import org.apache.beam.sdk.transforms.resourcehints.ResourceHints; @@ -171,11 +172,17 @@ import org.apache.beam.sdk.util.construction.PipelineTranslation; import org.apache.beam.sdk.util.construction.SdkComponents; import org.apache.beam.sdk.util.construction.TransformPayloadTranslatorRegistrar; +import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; import org.apache.beam.sdk.values.PValues; import org.apache.beam.sdk.values.TimestampedValue; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; import org.apache.beam.sdk.values.TypeDescriptors; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowingStrategy; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.InvalidProtocolBufferException; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -191,6 +198,7 @@ import org.joda.time.Duration; import org.joda.time.Instant; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -2777,4 +2785,139 @@ public void process() {} })); p.run(); } + + @Test + @Category({ValidatesRunner.class}) + public void testValueKindParameterAndOutputWithKind() { + boolean isRunnerV2 = false; + @Nullable + List experiments = + pipeline.getOptions().as(DataflowPipelineOptions.class).getExperiments(); + if (experiments != null + && (experiments.contains("use_unified_worker") || experiments.contains("use_runner_v2"))) { + isRunnerV2 = true; + } + // Skipp runner v2 because its Create uses a splittable DoFn, which contains a shuffle. + // ValueKind is not supported in Dataflow shuffle yet + assumeFalse(isRunnerV2); + + PCollection input = pipeline.apply(Create.of("a", "b", "c", "d")); + TupleTag mainTag = new TupleTag() {}; + TupleTag sideTag = new TupleTag() {}; + + PCollectionTuple tuple = + input.apply( + "SetKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, + @Timestamp org.joda.time.Instant timestamp, + BoundedWindow window, + PaneInfo paneInfo, + ProcessContext c, + MultiOutputReceiver outputReceiver) { + switch (element) { + case "a": + c.output(element); // default: INSERT + return; + case "b": + c.outputWindowedValue( + WindowedValues.of( + element, + timestamp, + Collections.singleton(window), + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.UPDATE_BEFORE)); + return; + case "c": + outputReceiver + .get(mainTag) + .builder(element) + .setValueKind(ValueKind.UPDATE_AFTER) + .output(); + return; + case "d": + outputReceiver + .get(sideTag) + .builder(element) + .setValueKind(ValueKind.DELETE) + .output(); + } + } + }) + .withOutputTags(mainTag, TupleTagList.of(sideTag))); + + PCollection main = + tuple + .get(mainTag) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PCollection side = + tuple + .get(sideTag) + .apply( + "ReadKind-SideTag", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(main).containsInAnyOrder("a:INSERT", "b:UPDATE_BEFORE", "c:UPDATE_AFTER"); + PAssert.that(side).containsInAnyOrder("d:DELETE"); + pipeline.run(); + } + + @Test + @Ignore("enable once when element metadata is supported in shuffle") + @Category({ValidatesRunner.class}) + public void testValueKindPreservedAcrossShuffle() { + PCollection> input = pipeline.apply(Create.of(KV.of("key", "value"))); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn, KV>() { + @ProcessElement + public void processElement( + @Element KV element, + OutputReceiver> out) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } + })) + .apply(Reshuffle.of()) + .apply( + "ReadKind", + ParDo.of( + new DoFn, String>() { + @ProcessElement + public void processElement( + @Element KV element, ProcessContext c, ValueKind kind) { + c.output(element.getValue() + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("value:UPDATE_BEFORE"); + pipeline.run(); + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java index 0b883c048462..1bcf8cf179cd 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java @@ -39,7 +39,6 @@ import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.ValueKind; -import org.apache.beam.sdk.values.ValueKindUtil; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; @@ -149,7 +148,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; - valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); + valueKind = WindmillValueKindHelper.fromProto(elementMetadata.getValueKind()); } if (valueCoder instanceof KvCoder) { KvCoder kvCoder = (KvCoder) valueCoder; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java index 033e4186158d..c4c0b6ed92d3 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java @@ -43,7 +43,6 @@ import org.apache.beam.sdk.util.common.ElementByteSizeObserver; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.ValueKind; -import org.apache.beam.sdk.values.ValueKindUtil; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicate; @@ -158,7 +157,7 @@ public Iterable timersIterable() { elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; - valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); + valueKind = WindmillValueKindHelper.fromProto(elementMetadata.getValueKind()); } InputStream inputStream = message.getData().newInput(); ElemT value = valueCoder.decode(inputStream, Coder.Context.OUTER); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java index 3ab46f0ddb42..ae5941f2e82d 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java @@ -226,6 +226,7 @@ public long add(WindowedValue data) throws IOException { data.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN ? BeamFnApi.Elements.DrainMode.Enum.DRAINING : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .setValueKind(WindmillValueKindHelper.toProto(data.getValueKind())) .build(); ByteString metadata = encodeMetadata( diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillValueKindHelper.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillValueKindHelper.java new file mode 100644 index 000000000000..2d255462d6b3 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillValueKindHelper.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; +import org.apache.beam.sdk.values.ValueKind; + +/** + * Utility class for converting between {@link ValueKind} and {@link Elements.ValueKind.Enum} inside + * the Dataflow Worker. This is a duplicate of the utility inside core sdk because core utility is + * compiled against unrepackaged BeamFnApi, whereas Dataflow Worker relocates BeamFnApi to dataflow + * repackaged version causing NoSuchMethodErrors at runtime. + */ +final class WindmillValueKindHelper { + public static Elements.ValueKind.Enum toProto(ValueKind valueKind) { + switch (valueKind) { + case INSERT: + return Elements.ValueKind.Enum.INSERT; + case UPDATE_BEFORE: + return Elements.ValueKind.Enum.UPDATE_BEFORE; + case UPDATE_AFTER: + return Elements.ValueKind.Enum.UPDATE_AFTER; + case DELETE: + return Elements.ValueKind.Enum.DELETE; + default: + throw new IllegalArgumentException("Unknown ValueKind: " + valueKind); + } + } + + public static ValueKind fromProto(Elements.ValueKind.Enum proto) { + switch (proto) { + case VALUE_KIND_UNSPECIFIED: + case INSERT: + return ValueKind.INSERT; + case UPDATE_BEFORE: + return ValueKind.UPDATE_BEFORE; + case UPDATE_AFTER: + return ValueKind.UPDATE_AFTER; + case DELETE: + return ValueKind.DELETE; + default: + throw new IllegalArgumentException("Unknown ValueKind: " + proto); + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java index 7136538753db..c31c39de9ccb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java @@ -81,6 +81,7 @@ import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; import org.joda.time.Duration; import org.joda.time.Instant; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -109,6 +110,11 @@ public void setUp() { stepContext = new TestStepContext(STEP_NAME); } + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + @Test public void testReshufle() throws Exception { GroupAlsoByWindowFn fn = diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java index 864b903b25f9..a366ded4fe2d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java @@ -51,6 +51,7 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.TypeDescriptor; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowingStrategy; import org.checkerframework.checker.nullness.qual.Nullable; @@ -338,6 +339,9 @@ public abstract class ProcessContext extends WindowedContext { @Pure public abstract org.apache.beam.sdk.values.CausedByDrain causedByDrain(); + + @Pure + public abstract ValueKind valueKind(); } /** Information accessible when running a {@link DoFn.OnTimer} method. */ @@ -426,6 +430,20 @@ default void outputWindowedValue( PaneInfo paneInfo) { builder(value).setTimestamp(timestamp).setWindows(windows).setPaneInfo(paneInfo).output(); } + + default void outputWindowedValue( + T value, + Instant timestamp, + Collection windows, + PaneInfo paneInfo, + ValueKind valueKind) { + builder(value) + .setTimestamp(timestamp) + .setWindows(windows) + .setPaneInfo(paneInfo) + .setValueKind(valueKind) + .output(); + } } /** Receives tagged output for a multi-output function. */ diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java index 05d7b7b0d920..1ca02ec3f19f 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java @@ -56,6 +56,7 @@ import org.apache.beam.sdk.values.TimestampedValue; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; @@ -503,7 +504,8 @@ public void output(TupleTag tag, T output, Instant timestamp, BoundedWind null, null, CausedByDrain.NORMAL, - null)); + null, + ValueKind.INSERT)); } }; } @@ -607,6 +609,11 @@ public CausedByDrain causedByDrain() { return element.getCausedByDrain(); } + @Override + public ValueKind valueKind() { + return element.getValueKind(); + } + @Override public PipelineOptions getPipelineOptions() { return options; @@ -648,7 +655,8 @@ public void outputWithTimestamp(TupleTag tag, T output, Instant timestamp null, null, CausedByDrain.NORMAL, - element.getOpenTelemetryContext())); + element.getOpenTelemetryContext(), + ValueKind.INSERT)); } @Override @@ -669,7 +677,8 @@ public void outputWindowedValue(TupleTag tag, WindowedValue windowedVa windowedValue.getRecordId(), windowedValue.getRecordOffset(), windowedValue.causedByDrain(), - windowedValue.getOpenTelemetryContext())); + windowedValue.getOpenTelemetryContext(), + windowedValue.getValueKind())); } } @@ -684,7 +693,15 @@ public void outputWindowedValue( getMutableOutput(tag) .add( ValueInSingleWindow.of( - output, timestamp, w, paneInfo, null, null, CausedByDrain.NORMAL, null)); + output, + timestamp, + w, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT)); } } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Redistribute.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Redistribute.java index 44f27824382d..5463365e4c6e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Redistribute.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Redistribute.java @@ -188,6 +188,7 @@ public void processElement( .setWindow(kv.getValue().getWindow()) .setPaneInfo(kv.getValue().getPaneInfo()) .setCausedByDrain(kv.getValue().getCausedByDrain()) + .setValueKind(kv.getValue().getValueKind()) .output(); } })); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java index 1e3203744d05..b1288c054142 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java @@ -30,6 +30,7 @@ import org.apache.beam.sdk.values.TimestampedValue; import org.apache.beam.sdk.values.TimestampedValue.TimestampedValueCoder; import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.beam.sdk.values.ValueKind; import org.joda.time.Duration; import org.joda.time.Instant; @@ -148,6 +149,7 @@ public void processElement( BoundedWindow window, PaneInfo paneInfo, CausedByDrain causedByDrain, + ValueKind valueKind, OutputReceiver>> r) { r.output( KV.of( @@ -160,7 +162,8 @@ public void processElement( pc.currentRecordId(), pc.currentRecordOffset(), causedByDrain, - null))); + null, + valueKind))); } })) .setCoder( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java index 0a8d058107b8..594bdfc85039 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java @@ -190,6 +190,7 @@ public void processElement( .setTimestamp(kv.getValue().getTimestamp()) .setWindow(kv.getValue().getWindow()) .setPaneInfo(kv.getValue().getPaneInfo()) + .setValueKind(kv.getValue().getValueKind()) .output(); } })); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java index 9adbe3a12cf4..3ebabb6e3c37 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java @@ -97,6 +97,7 @@ import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.TimerFamilyParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.TimerParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.TimestampParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.ValueKindParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.WatermarkEstimatorParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.WatermarkEstimatorStateParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.WindowParameter; @@ -134,6 +135,7 @@ class ByteBuddyDoFnInvokerFactory implements DoFnInvokerFactory { public static final String CURRENT_RECORD_ID_PARAMETER_METHOD = "currentRecordId"; public static final String CURRENT_RECORD_OFFSET_PARAMETER_METHOD = "currentRecordOffset"; public static final String FIRE_TIMESTAMP_PARAMETER_METHOD = "fireTimestamp"; + public static final String VALUE_KIND_PARAMETER_METHOD = "valueKind"; public static final String BUNDLE_FINALIZER_PARAMETER_METHOD = "bundleFinalizer"; public static final String OUTPUT_ROW_RECEIVER_METHOD = "outputRowReceiver"; public static final String TIME_DOMAIN_PARAMETER_METHOD = "timeDomain"; @@ -1117,6 +1119,15 @@ public StackManipulation dispatch(CausedByDrainParameter p) { CAUSED_BY_DRAIN_PARAMETER_METHOD, DoFn.class))); } + @Override + public StackManipulation dispatch(ValueKindParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + VALUE_KIND_PARAMETER_METHOD, DoFn.class))); + } + @Override public StackManipulation dispatch(BundleFinalizerParameter p) { return simpleExtraContextParameter(BUNDLE_FINALIZER_PARAMETER_METHOD); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java index 1f122f1bf661..eaabdff907c7 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java @@ -43,6 +43,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; @@ -232,6 +233,9 @@ interface ArgumentProvider { /** Provide a reference to the caused by drain. */ CausedByDrain causedByDrain(DoFn doFn); + /** Provide a reference to the {@link ValueKind}. */ + ValueKind valueKind(DoFn doFn); + /** Provide a reference to the time domain for a timer firing. */ TimeDomain timeDomain(DoFn doFn); @@ -364,6 +368,12 @@ public CausedByDrain causedByDrain(DoFn doFn) { String.format("CausedByDrain unsupported in %s", getErrorContext())); } + @Override + public ValueKind valueKind(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("ValueKind unsupported in %s", getErrorContext())); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException( @@ -573,6 +583,11 @@ public CausedByDrain causedByDrain(DoFn doFn) { return delegate.causedByDrain(doFn); } + @Override + public ValueKind valueKind(DoFn doFn) { + return delegate.valueKind(doFn); + } + @Override public TimeDomain timeDomain(DoFn doFn) { return delegate.timeDomain(doFn); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java index 33fccc2e1cde..51dadd178a6f 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java @@ -351,6 +351,8 @@ public ResultT match(Cases cases) { return cases.dispatch((CurrentRecordOffsetParameter) this); } else if (this instanceof FireTimestampParameter) { return cases.dispatch((FireTimestampParameter) this); + } else if (this instanceof ValueKindParameter) { + return cases.dispatch((ValueKindParameter) this); } else if (this instanceof KeyParameter) { return cases.dispatch((KeyParameter) this); } else { @@ -417,6 +419,8 @@ public interface Cases { ResultT dispatch(CausedByDrainParameter p); + ResultT dispatch(ValueKindParameter p); + ResultT dispatch(KeyParameter p); /** A base class for a visitor with a default method for cases it is not interested in. */ @@ -534,6 +538,11 @@ public ResultT dispatch(CausedByDrainParameter p) { return dispatchDefault(p); } + @Override + public ResultT dispatch(ValueKindParameter p) { + return dispatchDefault(p); + } + @Override public ResultT dispatch(StateParameter p) { return dispatchDefault(p); @@ -591,6 +600,8 @@ public ResultT dispatch(KeyParameter p) { new AutoValue_DoFnSignature_Parameter_BundleFinalizerParameter(); private static final CausedByDrainParameter CAUSED_BY_DRAIN_PARAMETER = new AutoValue_DoFnSignature_Parameter_CausedByDrainParameter(); + private static final ValueKindParameter VALUE_KIND_PARAMETER = + new AutoValue_DoFnSignature_Parameter_ValueKindParameter(); private static final OnWindowExpirationContextParameter ON_WINDOW_EXPIRATION_CONTEXT_PARAMETER = new AutoValue_DoFnSignature_Parameter_OnWindowExpirationContextParameter(); private static final CurrentRecordIdParameter CURRENT_RECORD_ID_PARAMETER = @@ -625,6 +636,11 @@ public static CausedByDrainParameter causedByDrainParameter() { return CAUSED_BY_DRAIN_PARAMETER; } + /** Returns a {@link ValueKindParameter}. */ + public static ValueKindParameter valueKindParameter() { + return VALUE_KIND_PARAMETER; + } + /** Returns a {@link CurrentRecordIdParameter}. */ public static CurrentRecordIdParameter currentRecordIdParameter() { return CURRENT_RECORD_ID_PARAMETER; @@ -802,6 +818,16 @@ public abstract static class CausedByDrainParameter extends Parameter { CausedByDrainParameter() {} } + /** + * Descriptor for a {@link Parameter} of type {@link org.apache.beam.sdk.values.ValueKind}. + * + *

    All such descriptors are equal. + */ + @AutoValue + public abstract static class ValueKindParameter extends Parameter { + ValueKindParameter() {} + } + /** * Descriptor for a {@link Parameter} of type {@link DoFn.RecordId}. * diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java index ec624696fc7c..0bd2c1c888f0 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java @@ -98,6 +98,7 @@ import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.TypeDescriptors; import org.apache.beam.sdk.values.TypeParameter; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; @@ -143,6 +144,7 @@ private DoFnSignatures() {} Parameter.CurrentRecordIdParameter.class, Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, + Parameter.ValueKindParameter.class, Parameter.BundleFinalizerParameter.class); private static final ImmutableList> @@ -162,6 +164,7 @@ private DoFnSignatures() {} Parameter.CurrentRecordIdParameter.class, Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, + Parameter.ValueKindParameter.class, Parameter.BundleFinalizerParameter.class); private static final ImmutableList> ALLOWED_SETUP_PARAMETERS = @@ -1386,6 +1389,11 @@ private static Parameter analyzeExtraParameter( rawType.equals(CausedByDrain.class), "CausedByDrain argument must have type org.apache.beam.sdk.values.CausedByDrain."); return Parameter.causedByDrainParameter(); + } else if (ValueKind.class.isAssignableFrom(rawType)) { + methodErrors.checkArgument( + rawType.equals(ValueKind.class), + "ValueKind argument must have type org.apache.beam.sdk.values.ValueKind."); + return Parameter.valueKindParameter(); } else if (hasAnnotation(DoFn.SideInput.class, param.getAnnotations())) { String sideInputId = getSideInputId(param.getAnnotations()); paramErrors.checkArgument( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java index 44212f97ad28..d1fb23e77c47 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java @@ -56,6 +56,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Uninterruptibles; @@ -520,6 +521,11 @@ public CausedByDrain causedByDrain() { return outerContext.causedByDrain(); } + @Override + public ValueKind valueKind() { + return outerContext.valueKind(); + } + @Override public Object sideInput(String tagId) { PCollectionView view = sideInputMapping.get(tagId); @@ -549,6 +555,11 @@ public CausedByDrain causedByDrain(DoFn doFn) { return outerContext.causedByDrain(); } + @Override + public ValueKind valueKind(DoFn doFn) { + return outerContext.valueKind(); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException(); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java index 33700a9dc0d2..43db94dffb61 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java @@ -397,7 +397,6 @@ public static WindowedValue of( ValueKind.INSERT); } - /** Returns a {@code WindowedValue} with the given value, timestamp, and window. */ public static WindowedValue of( T value, Instant timestamp, @@ -1045,7 +1044,7 @@ public static ParamWindowedValueCoder getParamWindowedValueCoder(Coder /** Abstract class for {@code WindowedValue} coder. */ public abstract static class WindowedValueCoder extends StructuredCoder> { final Coder valueCoder; - private static boolean metadataSupported = false; + private static volatile boolean metadataSupported = false; public static void setMetadataSupported() { metadataSupported = true; diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ValueKindTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ValueKindTest.java new file mode 100644 index 000000000000..b653d6366bfc --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ValueKindTest.java @@ -0,0 +1,623 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.transforms; + +import java.io.Serializable; +import java.util.Collections; +import org.apache.beam.sdk.coders.VarIntCoder; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.state.StateSpec; +import org.apache.beam.sdk.state.StateSpecs; +import org.apache.beam.sdk.state.TimeDomain; +import org.apache.beam.sdk.state.Timer; +import org.apache.beam.sdk.state.TimerSpec; +import org.apache.beam.sdk.state.TimerSpecs; +import org.apache.beam.sdk.state.ValueState; +import org.apache.beam.sdk.testing.NeedsRunner; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.DoFn.StateId; +import org.apache.beam.sdk.transforms.DoFn.TimerId; +import org.apache.beam.sdk.transforms.DoFn.Timestamp; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; +import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.beam.sdk.values.WindowedValues; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ValueKind} support in {@link DoFn}. */ +@RunWith(JUnit4.class) +public class ValueKindTest implements Serializable { + @Rule public final transient TestPipeline pipeline = TestPipeline.create(); + + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindParameterInDoFn() { + PCollection input = pipeline.apply(Create.of("a", "b")); + + PCollection output = + input.apply( + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:INSERT", "b:INSERT"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testOutputWithKind() { + PCollection input = pipeline.apply(Create.of("a", "b")); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, + @Timestamp Instant timestamp, + BoundedWindow window, + PaneInfo paneInfo, + OutputReceiver out, + ProcessContext c) { + if ("a".equals(element)) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } else { + c.outputWindowedValue( + WindowedValues.of( + element, + timestamp, + Collections.singleton(window), + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.UPDATE_AFTER)); + } + } + })) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE", "b:UPDATE_AFTER"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testDefaultValueKind() { + PCollection input = pipeline.apply(Create.of("a")); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, OutputReceiver out) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } + })) + .apply( + "StandardOutput", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement(@Element String element, ProcessContext c) { + c.output(element); // Should preserve input kind! + } + })) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedInOutputWindowedValue_MainOutput() { + PCollection input = pipeline.apply(Create.of("a")); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, OutputReceiver out) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } + })) + .apply( + "OutputWindowedValue", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, BoundedWindow window) { + // Should preserve input kind! + c.outputWindowedValue( + element, c.timestamp(), Collections.singleton(window), c.pane()); + } + })) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedInOutputWindowedValue_TaggedOutput() { + PCollection input = pipeline.apply(Create.of("a")); + TupleTag mainTag = new TupleTag() {}; + TupleTag sideTag = new TupleTag() {}; + + PCollectionTuple outputTuple = + input + .apply( + "SetKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, OutputReceiver out) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } + })) + .apply( + "OutputWindowedValueTagged", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, BoundedWindow window) { + c.outputWindowedValue( + sideTag, + element, + c.timestamp(), + Collections.singleton(window), + c.pane()); + } + }) + .withOutputTags(mainTag, TupleTagList.of(sideTag))); + + PCollection output = + outputTuple + .get(sideTag) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedInOutputWindowedValue_Object() { + PCollection input = pipeline.apply(Create.of("a")); + + PCollection output = + input + .apply( + "OutputWindowedValueObject", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, BoundedWindow window) { + c.outputWindowedValue( + WindowedValues.of( + element, + c.timestamp(), + Collections.singleton(window), + c.pane(), + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.UPDATE_BEFORE)); + } + })) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedInOutputWindowedValue_TaggedObject() { + PCollection input = pipeline.apply(Create.of("a")); + TupleTag mainTag = new TupleTag() {}; + TupleTag sideTag = new TupleTag() {}; + + PCollectionTuple outputTuple = + input.apply( + "OutputWindowedValueTaggedObject", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, BoundedWindow window) { + c.outputWindowedValue( + sideTag, + WindowedValues.of( + element, + c.timestamp(), + Collections.singleton(window), + c.pane(), + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.UPDATE_BEFORE)); + } + }) + .withOutputTags(mainTag, TupleTagList.of(sideTag))); + + PCollection output = + outputTuple + .get(sideTag) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedAcrossTags() { + PCollection input = pipeline.apply(Create.of("a")); + TupleTag mainTag = new TupleTag() {}; + TupleTag sideTag = new TupleTag() {}; + + PCollectionTuple outputTuple = + input.apply( + "OutputWithKindTagged", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement(@Element String element, MultiOutputReceiver out) { + out.get(sideTag) + .builder(element) + .setValueKind(ValueKind.UPDATE_BEFORE) + .output(); + } + }) + .withOutputTags(mainTag, TupleTagList.of(sideTag))); + + PCollection output = + outputTuple + .get(sideTag) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindInSplittableDoFn() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + PCollection input = pipeline.apply(Create.of("a", "b")); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, OutputReceiver out) { + if ("a".equalsIgnoreCase(element)) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } else { + out.output(element); + } + } + })) + .apply( + "SplittableDoFn", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, + RestrictionTracker tracker, + ProcessContext c, + ValueKind kind) { + if (tracker.tryClaim(tracker.currentRestriction().getFrom())) { + c.output(element + ":" + kind); + } + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction(@Element String element) { + return new OffsetRange(0, 1); + } + })); + + PAssert.that(output).containsInAnyOrder("a:UPDATE_BEFORE", "b:INSERT"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testOutputWithKindInWindowExpiration() { + PCollection> input = pipeline.apply(Create.of(KV.of("key", "a"))); + PCollection> windowedInput = + input.apply(Window.into(FixedWindows.of(Duration.standardSeconds(1)))); + + PCollection output = + windowedInput.apply( + "StatefulParDo", + ParDo.of( + new DoFn, String>() { + @StateId("dummy") + private final StateSpec> spec = + StateSpecs.value(VarIntCoder.of()); + + @ProcessElement + public void processElement( + @Element KV element, ProcessContext c) { + // Do nothing, just let state be created + } + + @OnWindowExpiration + public void onWindowExpiration( + OutputReceiver receiver, + BoundedWindow window, + @Timestamp Instant timestamp) { + receiver.outputWindowedValue( + "expired", + timestamp, + Collections.singleton(window), + PaneInfo.NO_FIRING, + ValueKind.UPDATE_BEFORE); + } + })); + + PAssert.that(output).containsInAnyOrder("expired"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testOutputWithKindInOnTimer() { + PCollection> input = pipeline.apply(Create.of(KV.of("key", "a"))); + + PCollection output = + input + .apply( + "TimerParDo", + ParDo.of( + new DoFn, String>() { + @TimerId("timer") + private final TimerSpec timerSpec = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void processElement( + @Element KV element, + @TimerId("timer") Timer timer, + ProcessContext c) { + timer.set(c.timestamp().plus(Duration.standardSeconds(1))); + } + + @OnTimer("timer") + public void onTimer(OnTimerContext c) { + c.outputWindowedValue( + WindowedValues.of( + "timed_out", + c.timestamp(), + c.window(), + PaneInfo.NO_FIRING, + null, + null, + c.causedByDrain(), + null, + ValueKind.DELETE)); + } + })) + .apply( + "ReadKind", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element String element, ProcessContext c, ValueKind kind) { + c.output(element + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("timed_out:DELETE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedInReshuffle() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + PCollection> input = pipeline.apply(Create.of(KV.of("key", "value"))); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn, KV>() { + @ProcessElement + public void processElement( + @Element KV element, + OutputReceiver> out) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } + })) + .apply(Reshuffle.of()) + .apply( + "ReadKind", + ParDo.of( + new DoFn, String>() { + @ProcessElement + public void processElement( + @Element KV element, ProcessContext c, ValueKind kind) { + c.output(element.getValue() + ":" + kind); + } + })); + + PAssert.that(output).containsInAnyOrder("value:UPDATE_BEFORE"); + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testValueKindPreservedInGroupByKeyWithReify() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + PCollection> input = pipeline.apply(Create.of(KV.of("key", "value"))); + + PCollection output = + input + .apply( + "SetKind", + ParDo.of( + new DoFn, KV>() { + @ProcessElement + public void processElement( + @Element KV element, + OutputReceiver> out) { + out.builder(element).setValueKind(ValueKind.UPDATE_BEFORE).output(); + } + })) + .apply(Reify.windowsInValue()) + .apply(GroupByKey.create()) + .apply( + "ReadKind", + ParDo.of( + new DoFn>>, String>() { + @ProcessElement + public void processElement( + @Element KV>> element, + ProcessContext c) { + for (ValueInSingleWindow value : element.getValue()) { + c.output(value.getValue() + ":" + value.getValueKind()); + } + } + })); + + PAssert.that(output).containsInAnyOrder("value:UPDATE_BEFORE"); + pipeline.run(); + } +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java index 3689b1be7dba..ce603f41516d 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java @@ -46,6 +46,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.joda.time.Duration; import org.joda.time.Instant; +import org.junit.After; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -57,6 +58,11 @@ @RunWith(JUnit4.class) public class WindowedValueTest { + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + @Rule public ExpectedException thrown = ExpectedException.none(); @Test @@ -130,6 +136,33 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { Assert.assertEquals(ValueKind.DELETE, value.getValueKind()); } + @Test + public void testWindowedValueWithValueKindCoder() throws CoderException { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + Instant timestamp = new Instant(1234); + WindowedValue value = + WindowedValues.builder() + .setValue("abc") + .setTimestamp(timestamp) + .setWindows( + Arrays.asList(new IntervalWindow(timestamp, timestamp.plus(Duration.millis(1000))))) + .setPaneInfo(PaneInfo.NO_FIRING) + .setValueKind(ValueKind.UPDATE_BEFORE) + .build(); + + Coder> windowedValueCoder = + WindowedValues.getFullCoder(StringUtf8Coder.of(), IntervalWindow.getCoder()); + + byte[] encodedValue = CoderUtils.encodeToByteArray(windowedValueCoder, value); + WindowedValue decodedValue = + CoderUtils.decodeFromByteArray(windowedValueCoder, encodedValue); + + Assert.assertEquals(value.getValue(), decodedValue.getValue()); + Assert.assertEquals(value.getTimestamp(), decodedValue.getTimestamp()); + Assert.assertArrayEquals(value.getWindows().toArray(), decodedValue.getWindows().toArray()); + Assert.assertEquals(value.getValueKind(), decodedValue.getValueKind()); + } + @Test public void testFullWindowedValueCoderIsSerializableWithWellKnownCoderType() { CoderProperties.coderSerializable( diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java index 0fbc92c1f7d8..d92a84ea9ff7 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java @@ -108,6 +108,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.WindowedValueCoder; @@ -1736,6 +1737,13 @@ private class WindowObservingProcessBundleContext public OutputBuilder builder(OutputT value) { return WindowedValues.builder() .setValue(value) + .setTimestamp(currentElement.getTimestamp()) + .setPaneInfo(currentElement.getPaneInfo()) + .setWindows(currentElement.getWindows()) + .setRecordOffset(currentElement.getRecordOffset()) + .setRecordId(currentElement.getRecordId()) + .setCausedByDrain(currentElement.causedByDrain()) + .setValueKind(currentElement.getValueKind()) .setReceiver(windowedValue -> outputTo(mainOutputConsumer, windowedValue)); } @@ -1820,7 +1828,18 @@ public void outputWindowedValue( if (consumer == null) { throw new IllegalArgumentException(String.format("Unknown output tag %s", tag)); } - outputTo(consumer, WindowedValues.of(output, timestamp, windows, paneInfo)); + outputTo( + consumer, + WindowedValues.of( + output, + timestamp, + windows, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT)); } @Override @@ -2286,10 +2305,20 @@ public CausedByDrain causedByDrain() { return currentElement.causedByDrain(); } + @Override + public ValueKind valueKind() { + return currentElement.getValueKind(); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return currentElement.causedByDrain(); } + + @Override + public ValueKind valueKind(DoFn doFn) { + return currentElement.getValueKind(); + } } /** diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java index b09fa8f69b3e..4ab932fc5d83 100644 --- a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java @@ -37,11 +37,12 @@ public void testReadRowsBuilderAndGetters() { Map hadoopConfig = new HashMap<>(); hadoopConfig.put("fs.defaultFS", "file:///"); - ReadRows readRows = DeltaIO.readRows() - .from(tablePath) - .withVersion(version) - .withTimestamp(timestamp) - .withConfig(hadoopConfig); + ReadRows readRows = + DeltaIO.readRows() + .from(tablePath) + .withVersion(version) + .withTimestamp(timestamp) + .withConfig(hadoopConfig); Assert.assertEquals(tablePath, readRows.getTablePath()); Assert.assertEquals(Long.valueOf(version), readRows.getVersion()); From 531eca84bda4885292a1382c9501b721ab00946a Mon Sep 17 00:00:00 2001 From: Enzo Maruffa Moreira Date: Fri, 22 May 2026 18:57:43 -0300 Subject: [PATCH 222/490] [Python] Add type_overrides parameter to BigQuery I/O for custom BigQuery-to-Python type mappings (#37253) * feat(bigquery): add type_overrides parameter for custom BigQuery to Python type mappings * docs(bigquery): fix Sphinx indentation in generate_user_type_from_bq_schema docstring --- CHANGES.md | 1 + sdks/python/apache_beam/io/gcp/bigquery.py | 25 ++- .../io/gcp/bigquery_schema_tools.py | 63 +++++-- .../io/gcp/bigquery_schema_tools_test.py | 109 ++++++++++++ .../apache_beam/io/gcp/bigquery_tools.py | 13 +- .../apache_beam/io/gcp/bigquery_tools_test.py | 161 ++++++++++++++++++ 6 files changed, 346 insertions(+), 26 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa4fdb096601..c87c1271280b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -82,6 +82,7 @@ * Added plugin mechanism to support different Lineage implementations (Java) ([#36790](https://github.com/apache/beam/issues/36790)). * (Python) Supported Python user type in Beam SQL. For example, SQL statements like `SELECT some_field from PCOLLECTION` can now operate a PCollection of Beam Row containing pickable Python user type ([#20738](https://github.com/apache/beam/issues/20738)). * (Python) Introduced `beam.coders.registry.register_row` as preferred API to register a named tuple or dataclass with a Beam Row. At pipelne runtime, the original type associated with the registered row are preserved across the serialization boundary ([#38108](https://github.com/apache/beam/issues/38108)). +* (Python) Added `type_overrides` parameter to `WriteToBigQuery` allowing users to specify custom BigQuery to Python type mappings when using Storage Write API. This enables support for types like DATE, DATETIME, and JSON (Python) ([#25946](https://github.com/apache/beam/issues/25946)). ## Breaking Changes diff --git a/sdks/python/apache_beam/io/gcp/bigquery.py b/sdks/python/apache_beam/io/gcp/bigquery.py index e1bb5583f38b..d751d60c905f 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery.py +++ b/sdks/python/apache_beam/io/gcp/bigquery.py @@ -2006,7 +2006,8 @@ def __init__( use_cdc_writes: bool = False, primary_key: list[str] = None, expansion_service=None, - big_lake_configuration=None): + big_lake_configuration=None, + type_overrides=None): """Initialize a WriteToBigQuery transform. Args: @@ -2183,6 +2184,11 @@ def __init__( CREATE_IF_NEEDED mode for the underlying tables a list of column names is required to be configured as the primary key. Used for STORAGE_WRITE_API, working on 'at least once' mode. + type_overrides (dict): Optional mapping of BigQuery type names (uppercase) + to Python types. These override the default type mappings when + converting BigQuery schemas to Python types for STORAGE_WRITE_API. + For example: ``{'DATE': datetime.date, 'JSON': dict}``. + Default mappings include STRING->str, INT64->np.int64, etc. """ self._table = table self._dataset = dataset @@ -2228,6 +2234,7 @@ def __init__( self._use_cdc_writes = use_cdc_writes self._primary_key = primary_key self._big_lake_configuration = big_lake_configuration + self._type_overrides = type_overrides # Dict/schema methods were moved to bigquery_tools, but keep references # here for backward compatibility. @@ -2392,7 +2399,8 @@ def find_in_nested_dict(schema): use_cdc_writes=self._use_cdc_writes, primary_key=self._primary_key, big_lake_configuration=self._big_lake_configuration, - expansion_service=self.expansion_service) + expansion_service=self.expansion_service, + type_overrides=self._type_overrides) else: raise ValueError(f"Unsupported method {method_to_use}") @@ -2641,7 +2649,8 @@ def __init__( use_cdc_writes: bool = False, primary_key: list[str] = None, big_lake_configuration=None, - expansion_service=None): + expansion_service=None, + type_overrides=None): self._table = table self._table_side_inputs = table_side_inputs self._schema = schema @@ -2655,6 +2664,7 @@ def __init__( self._use_cdc_writes = use_cdc_writes self._primary_key = primary_key self._big_lake_configuration = big_lake_configuration + self._type_overrides = type_overrides self._expansion_service = expansion_service or BeamJarExpansionService( 'sdks:java:io:google-cloud-platform:expansion-service:build') @@ -2688,7 +2698,7 @@ def expand(self, input): input_beam_rows = ( input | "Convert dict to Beam Row" >> self.ConvertToBeamRows( - schema, False).with_output_types()) + schema, False, self._type_overrides).with_output_types()) # For dynamic destinations, we first figure out where each row is going. # Then we send (destination, record) rows over to Java SchemaTransform. @@ -2720,7 +2730,7 @@ def expand(self, input): input_beam_rows = ( input_rows | "Convert dict to Beam Row" >> self.ConvertToBeamRows( - schema, True).with_output_types()) + schema, True, self._type_overrides).with_output_types()) # communicate to Java that this write should use dynamic destinations table = StorageWriteToBigQuery.DYNAMIC_DESTINATIONS @@ -2788,9 +2798,10 @@ def __exit__(self, *args): pass class ConvertToBeamRows(PTransform): - def __init__(self, schema, dynamic_destinations): + def __init__(self, schema, dynamic_destinations, type_overrides=None): self.schema = schema self.dynamic_destinations = dynamic_destinations + self.type_overrides = type_overrides def expand(self, input_dicts): if self.dynamic_destinations: @@ -2816,7 +2827,7 @@ def expand(self, input_dicts): def with_output_types(self): row_type_hints = bigquery_tools.get_beam_typehints_from_tableschema( - self.schema) + self.schema, self.type_overrides) if self.dynamic_destinations: type_hint = RowTypeConstraint.from_fields([ (StorageWriteToBigQuery.DESTINATION, str), diff --git a/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py index 54c7ca90f011..d3d608b1fc6f 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py @@ -55,15 +55,23 @@ def generate_user_type_from_bq_schema( - the_table_schema, selected_fields: 'bigquery.TableSchema' = None) -> type: + the_table_schema, + selected_fields: 'bigquery.TableSchema' = None, + type_overrides=None) -> type: """Convert a schema of type TableSchema into a pcollection element. - Args: - the_table_schema: A BQ schema of type TableSchema - selected_fields: if not None, the subset of fields to consider - Returns: - type: type that can be used to work with pCollections. - """ + Args: + the_table_schema: A BQ schema of type TableSchema + selected_fields: if not None, the subset of fields to consider + type_overrides: Optional mapping of BigQuery type names (uppercase) + to Python types. These override the default mappings in + BIG_QUERY_TO_PYTHON_TYPES. For example: + ``{'DATE': datetime.date, 'JSON': dict}`` + + Returns: + type: type that can be used to work with pCollections. + """ + effective_types = {**BIG_QUERY_TO_PYTHON_TYPES, **(type_overrides or {})} the_schema = beam.io.gcp.bigquery_tools.get_dict_table_schema( the_table_schema) if the_schema == {}: @@ -72,8 +80,8 @@ def generate_user_type_from_bq_schema( for field in the_schema['fields']: if selected_fields is not None and field['name'] not in selected_fields: continue - if field['type'] in BIG_QUERY_TO_PYTHON_TYPES: - typ = bq_field_to_type(field['type'], field['mode']) + if field['type'] in effective_types: + typ = bq_field_to_type(field['type'], field['mode'], type_overrides) else: raise ValueError( f"Encountered " @@ -85,19 +93,44 @@ def generate_user_type_from_bq_schema( return usertype -def bq_field_to_type(field, mode): +def bq_field_to_type(field, mode, type_overrides=None): + """Convert a BigQuery field type and mode to a Python type hint. + + Args: + field: The BigQuery type name (e.g., 'STRING', 'DATE'). + mode: The field mode ('NULLABLE', 'REPEATED', 'REQUIRED'). + type_overrides: Optional mapping of BigQuery type names (uppercase) + to Python types. These override the default mappings. + + Returns: + The corresponding Python type hint. + """ + effective_types = {**BIG_QUERY_TO_PYTHON_TYPES, **(type_overrides or {})} if mode == 'NULLABLE' or mode is None or mode == '': - return Optional[BIG_QUERY_TO_PYTHON_TYPES[field]] + return Optional[effective_types[field]] elif mode == 'REPEATED': - return Sequence[BIG_QUERY_TO_PYTHON_TYPES[field]] + return Sequence[effective_types[field]] elif mode == 'REQUIRED': - return BIG_QUERY_TO_PYTHON_TYPES[field] + return effective_types[field] else: raise ValueError(f"Encountered an unsupported mode: {mode!r}") -def convert_to_usertype(table_schema, selected_fields=None): - usertype = generate_user_type_from_bq_schema(table_schema, selected_fields) +def convert_to_usertype( + table_schema, selected_fields=None, type_overrides=None): + """Convert a BigQuery table schema to a user type. + + Args: + table_schema: A BQ schema of type TableSchema + selected_fields: if not None, the subset of fields to consider + type_overrides: Optional mapping of BigQuery type names (uppercase) + to Python types. + + Returns: + A ParDo transform that converts dictionaries to the user type. + """ + usertype = generate_user_type_from_bq_schema( + table_schema, selected_fields, type_overrides) return beam.ParDo(BeamSchemaConversionDoFn(usertype)) diff --git a/sdks/python/apache_beam/io/gcp/bigquery_schema_tools_test.py b/sdks/python/apache_beam/io/gcp/bigquery_schema_tools_test.py index 0eb3351ee84c..3cf641a2fb04 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_schema_tools_test.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_schema_tools_test.py @@ -337,6 +337,115 @@ def test_geography_with_complex_wkt(self): self.assertEqual(usertype.__annotations__, expected_annotations) +@unittest.skipIf(HttpError is None, 'GCP dependencies are not installed') +class TestTypeOverridesSchemaTools(unittest.TestCase): + """Tests for type_overrides parameter in bigquery_schema_tools.""" + def test_bq_field_to_type_with_overrides(self): + """Test bq_field_to_type function with type_overrides.""" + import datetime + + from apache_beam.io.gcp.bigquery_schema_tools import bq_field_to_type + + # Without overrides, DATE is not supported + with self.assertRaises(KeyError): + bq_field_to_type("DATE", "REQUIRED") + + # With overrides, DATE works + overrides = {"DATE": datetime.date} + self.assertEqual( + bq_field_to_type("DATE", "REQUIRED", overrides), datetime.date) + self.assertEqual( + bq_field_to_type("DATE", "NULLABLE", overrides), + typing.Optional[datetime.date]) + self.assertEqual( + bq_field_to_type("DATE", "REPEATED", overrides), + typing.Sequence[datetime.date]) + + def test_bq_field_to_type_overrides_can_use_str(self): + """Test that type_overrides can map DATE/DATETIME/JSON to str.""" + from apache_beam.io.gcp.bigquery_schema_tools import bq_field_to_type + + overrides = {"DATE": str, "DATETIME": str, "JSON": str} + self.assertEqual(bq_field_to_type("DATE", "REQUIRED", overrides), str) + self.assertEqual(bq_field_to_type("DATETIME", "REQUIRED", overrides), str) + self.assertEqual(bq_field_to_type("JSON", "REQUIRED", overrides), str) + + def test_generate_user_type_with_overrides(self): + """Test generate_user_type_from_bq_schema with type_overrides.""" + import datetime + + schema = bigquery.TableSchema( + fields=[ + bigquery.TableFieldSchema( + name='id', type='INTEGER', mode="REQUIRED"), + bigquery.TableFieldSchema( + name='event_date', type='DATE', mode="NULLABLE") + ]) + + # Without overrides, DATE is not supported + with self.assertRaises(ValueError): + bigquery_schema_tools.generate_user_type_from_bq_schema(schema) + + # With overrides, DATE works + overrides = {"DATE": datetime.date} + usertype = bigquery_schema_tools.generate_user_type_from_bq_schema( + schema, type_overrides=overrides) + self.assertEqual( + usertype.__annotations__, { + 'id': np.int64, 'event_date': typing.Optional[datetime.date] + }) + + def test_generate_user_type_overrides_with_str(self): + """Test that type_overrides can map DATE to str.""" + schema = bigquery.TableSchema( + fields=[ + bigquery.TableFieldSchema( + name='id', type='INTEGER', mode="REQUIRED"), + bigquery.TableFieldSchema( + name='event_date', type='DATE', mode="NULLABLE") + ]) + + overrides = {"DATE": str} + usertype = bigquery_schema_tools.generate_user_type_from_bq_schema( + schema, type_overrides=overrides) + self.assertEqual( + usertype.__annotations__, { + 'id': np.int64, 'event_date': typing.Optional[str] + }) + + def test_convert_to_usertype_with_overrides(self): + """Test convert_to_usertype function with type_overrides.""" + import datetime + + schema = bigquery.TableSchema( + fields=[ + bigquery.TableFieldSchema( + name='id', type='INTEGER', mode="REQUIRED"), + bigquery.TableFieldSchema( + name='event_date', type='DATE', mode="NULLABLE") + ]) + + overrides = {"DATE": datetime.date} + transform = bigquery_schema_tools.convert_to_usertype( + schema, type_overrides=overrides) + + # The transform should be created successfully + self.assertIsNotNone(transform) + self.assertIsInstance(transform, beam.ParDo) + + def test_type_overrides_can_override_default_types(self): + """Test that type_overrides can override default type mappings.""" + from apache_beam.io.gcp.bigquery_schema_tools import bq_field_to_type + + # GEOGRAPHY is in the default mapping as str + self.assertEqual(bq_field_to_type("GEOGRAPHY", "REQUIRED"), str) + + # We can override it + overrides = {"GEOGRAPHY": bytes} + self.assertEqual( + bq_field_to_type("GEOGRAPHY", "REQUIRED", overrides), bytes) + + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) unittest.main() diff --git a/sdks/python/apache_beam/io/gcp/bigquery_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_tools.py index a16d10003047..8dd58cd55a01 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_tools.py @@ -1781,18 +1781,23 @@ def get_avro_schema_from_table_schema(schema): "root", dict_table_schema) -def get_beam_typehints_from_tableschema(schema): +def get_beam_typehints_from_tableschema(schema, type_overrides=None): """Extracts Beam Python type hints from the schema. Args: schema (~apache_beam.io.gcp.internal.clients.bigquery.\ bigquery_v2_messages.TableSchema): The TableSchema to extract type hints from. + type_overrides (dict): Optional mapping of BigQuery type names (uppercase) + to Python types. These override the default mappings in + BIGQUERY_TYPE_TO_PYTHON_TYPE. For example: + ``{'DATE': datetime.date, 'JSON': dict}`` Returns: List[Tuple[str, Any]]: A list of type hints that describe the input schema. Nested and repeated fields are supported. """ + effective_types = {**BIGQUERY_TYPE_TO_PYTHON_TYPE, **(type_overrides or {})} if not isinstance(schema, (bigquery.TableSchema, bigquery.TableFieldSchema)): schema = get_bq_tableschema(schema) typehints = [] @@ -1802,9 +1807,9 @@ def get_beam_typehints_from_tableschema(schema): if field_type in ["STRUCT", "RECORD"]: # Structs can be represented as Beam Rows. typehint = RowTypeConstraint.from_fields( - get_beam_typehints_from_tableschema(field)) - elif field_type in BIGQUERY_TYPE_TO_PYTHON_TYPE: - typehint = BIGQUERY_TYPE_TO_PYTHON_TYPE[field_type] + get_beam_typehints_from_tableschema(field, type_overrides)) + elif field_type in effective_types: + typehint = effective_types[field_type] else: raise ValueError( f"Converting BigQuery type [{field_type}] to " diff --git a/sdks/python/apache_beam/io/gcp/bigquery_tools_test.py b/sdks/python/apache_beam/io/gcp/bigquery_tools_test.py index 2594e6728e0e..078c42160941 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_tools_test.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_tools_test.py @@ -1248,6 +1248,167 @@ def test_geography_with_special_characters(self): self.assertIsInstance(result, str) +@unittest.skipIf(HttpError is None, 'GCP dependencies are not installed') +class TestTypeOverrides(unittest.TestCase): + """Tests for type_overrides parameter in BigQuery type mappings.""" + def test_type_overrides_enables_unsupported_types(self): + """Test that type_overrides enables support for DATE/DATETIME/JSON.""" + import datetime + schema = { + "fields": [{ + "name": "date_field", "type": "DATE", "mode": "REQUIRED" + }, + { + "name": "datetime_field", + "type": "DATETIME", + "mode": "REQUIRED" + }, { + "name": "json_field", "type": "JSON", "mode": "REQUIRED" + }] + } + + # Without overrides, these types are not supported + with self.assertRaises(ValueError): + get_beam_typehints_from_tableschema(schema) + + # With overrides, they work + type_overrides = {"DATE": str, "DATETIME": str, "JSON": str} + typehints = get_beam_typehints_from_tableschema(schema, type_overrides) + self.assertEqual( + typehints, [("date_field", str), ("datetime_field", str), + ("json_field", str)]) + + def test_type_overrides_with_custom_types(self): + """Test type_overrides with custom Python types.""" + import datetime + schema = { + "fields": [{ + "name": "date_field", "type": "DATE", "mode": "REQUIRED" + }, + { + "name": "datetime_field", + "type": "DATETIME", + "mode": "REQUIRED" + }] + } + + type_overrides = {"DATE": datetime.date, "DATETIME": datetime.datetime} + typehints = get_beam_typehints_from_tableschema(schema, type_overrides) + self.assertEqual( + typehints, [("date_field", datetime.date), + ("datetime_field", datetime.datetime)]) + + def test_type_overrides_with_modes(self): + """Test that type_overrides works with NULLABLE and REPEATED modes.""" + import datetime + schema = { + "fields": [{ + "name": "required_date", "type": "DATE", "mode": "REQUIRED" + }, { + "name": "optional_date", "type": "DATE", "mode": "NULLABLE" + }, { + "name": "repeated_dates", "type": "DATE", "mode": "REPEATED" + }] + } + + type_overrides = {"DATE": datetime.date} + typehints = get_beam_typehints_from_tableschema(schema, type_overrides) + + expected = [("required_date", datetime.date), + ("optional_date", Optional[datetime.date]), + ("repeated_dates", Sequence[datetime.date])] + self.assertEqual(typehints, expected) + + def test_type_overrides_mixed_with_default_types(self): + """Test type_overrides alongside default type mappings.""" + import datetime + schema = { + "fields": [{ + "name": "date_field", "type": "DATE", "mode": "REQUIRED" + }, { + "name": "string_field", "type": "STRING", "mode": "REQUIRED" + }, { + "name": "int_field", "type": "INTEGER", "mode": "REQUIRED" + }] + } + + type_overrides = {"DATE": datetime.date} + typehints = get_beam_typehints_from_tableschema(schema, type_overrides) + + expected = [("date_field", datetime.date), ("string_field", str), + ("int_field", np.int64)] + self.assertEqual(typehints, expected) + + def test_type_overrides_with_nested_struct(self): + """Test that type_overrides is propagated to nested STRUCT fields.""" + import datetime + schema = bigquery.TableSchema() + + # Root field + date_field = bigquery.TableFieldSchema() + date_field.name = "date_field" + date_field.type = "DATE" + date_field.mode = "REQUIRED" + + # Nested struct with DATE field + struct_field = bigquery.TableFieldSchema() + struct_field.name = "nested" + struct_field.type = "RECORD" + struct_field.mode = "REQUIRED" + + nested_date = bigquery.TableFieldSchema() + nested_date.name = "nested_date" + nested_date.type = "DATE" + nested_date.mode = "REQUIRED" + struct_field.fields.append(nested_date) + + schema.fields.append(date_field) + schema.fields.append(struct_field) + + type_overrides = {"DATE": datetime.date} + typehints = get_beam_typehints_from_tableschema(schema, type_overrides) + + self.assertEqual(len(typehints), 2) + self.assertEqual(typehints[0], ("date_field", datetime.date)) + # The nested field's DATE should also be overridden + nested_constraint = typehints[1][1] + nested_fields = nested_constraint._fields + self.assertEqual(nested_fields[0], ("nested_date", datetime.date)) + + def test_type_overrides_can_override_default_types(self): + """Test that type_overrides can override default type mappings.""" + schema = { + "fields": [{ + "name": "geo_field", "type": "GEOGRAPHY", "mode": "REQUIRED" + }] + } + + # Without overrides, GEOGRAPHY maps to str (default) + typehints = get_beam_typehints_from_tableschema(schema, None) + self.assertEqual(typehints, [("geo_field", str)]) + + # With overrides, we can change it + typehints_override = get_beam_typehints_from_tableschema( + schema, {"GEOGRAPHY": bytes}) + self.assertEqual(typehints_override, [("geo_field", bytes)]) + + def test_type_overrides_json_to_dict(self): + """Test using type_overrides to map JSON to dict.""" + schema = {"fields": [{"name": "data", "type": "JSON", "mode": "NULLABLE"}]} + + # Without overrides, JSON is not supported + with self.assertRaises(ValueError): + get_beam_typehints_from_tableschema(schema) + + # With overrides, can map to str + typehints_str = get_beam_typehints_from_tableschema(schema, {"JSON": str}) + self.assertEqual(typehints_str, [("data", Optional[str])]) + + # Or map to dict + typehints_dict = get_beam_typehints_from_tableschema(schema, {"JSON": dict}) + self.assertEqual(typehints_dict, [("data", Optional[dict])]) + + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) unittest.main() From 813f2d5807331f587b53b3aea89a679da391a87e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 18:11:41 -0700 Subject: [PATCH 223/490] Update Python Dependencies - manual trigger (#38610) * Allow regeneration. * Update Python Dependencies * restore the config. --------- Co-authored-by: Valentyn Tymofieiev Co-authored-by: tvalentyn Co-authored-by: tvalentyn --- .../ml/py310/base_image_requirements.txt | 139 +++++----- .../ml/py310/gpu_image_requirements.txt | 243 ++++++++++-------- .../ml/py311/base_image_requirements.txt | 139 +++++----- .../ml/py311/gpu_image_requirements.txt | 241 +++++++++-------- .../ml/py312/base_image_requirements.txt | 139 +++++----- .../ml/py312/gpu_image_requirements.txt | 239 +++++++++-------- .../ml/py313/base_image_requirements.txt | 139 +++++----- .../py310/base_image_requirements.txt | 123 ++++----- .../py311/base_image_requirements.txt | 125 ++++----- .../py312/base_image_requirements.txt | 123 +++++---- .../py313/base_image_requirements.txt | 125 +++++---- .../py314/base_image_requirements.txt | 121 +++++---- 12 files changed, 972 insertions(+), 924 deletions(-) diff --git a/sdks/python/container/ml/py310/base_image_requirements.txt b/sdks/python/container/ml/py310/base_image_requirements.txt index b60f788ea396..1ef9fe59430d 100644 --- a/sdks/python/container/ml/py310/base_image_requirements.txt +++ b/sdks/python/container/ml/py310/base_image_requirements.txt @@ -23,8 +23,8 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -37,74 +37,74 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b7 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy-data-plane==0.2.6 exceptiongroup==1.3.1 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.3.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -112,12 +112,12 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 +importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -125,17 +125,17 @@ Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.12.2 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 multidict==6.7.1 namex==0.1.0 @@ -144,52 +144,53 @@ nltk==3.9.4 numpy==2.2.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pyjsparser==2.7.1 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 +rich==15.0.0 rpds-py==0.30.0 rsa==4.9.1 safetensors==0.7.0 @@ -202,11 +203,11 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 @@ -219,14 +220,14 @@ tqdm==4.67.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 tzlocal==5.3.1 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 +zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py310/gpu_image_requirements.txt b/sdks/python/container/ml/py310/gpu_image_requirements.txt index ac7f9add86af..70ef4e441661 100644 --- a/sdks/python/container/ml/py310/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py310/gpu_image_requirements.txt @@ -23,106 +23,108 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.12.1 +anyio==4.13.0 asn1crypto==1.5.1 astor==0.8.1 astunparse==1.6.3 async-timeout==5.0.1 -attrs==25.4.0 +attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b7 blake3==1.0.8 bs4==0.0.2 -build==1.4.0 +build==1.5.0 cachetools==6.2.6 -cbor2==5.8.0 -certifi==2026.2.25 +cbor2==6.1.1 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.5 -click==8.3.1 -cloud-sql-python-connector==1.20.0 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 cloudpickle==3.1.2 compressed-tensors==0.10.2 crcmod==1.7 -cryptography==46.0.5 -cuda-bindings==12.9.4 -cuda-pathfinder==1.4.2 +cryptography==47.0.0 +cuda-bindings==13.2.0 +cuda-pathfinder==1.5.4 +cuda-toolkit==13.0.2 cupy-cuda12x==14.0.1 Cython==3.2.4 depyf==0.19.0 +detect-installer==0.1.0 dill==0.3.1.1 diskcache==5.6.3 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 einops==0.8.2 email-validator==2.3.0 envoy-data-plane==0.2.6 exceptiongroup==1.3.1 execnet==2.1.2 -fastapi==0.135.1 +fastapi==0.136.1 fastapi-cli==0.0.24 -fastapi-cloud-cli==0.15.0 -fastar==0.8.0 -fastavro==1.12.1 +fastapi-cloud-cli==0.18.0 +fastar==0.11.0 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.2.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -gguf==0.18.0 -google-api-core==2.30.0 -google-api-python-client==2.192.0 +gguf==0.19.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.141.0 -google-cloud-bigquery==3.40.1 -google-cloud-bigquery-storage==2.36.2 -google-cloud-bigtable==2.35.0 -google-cloud-build==3.35.0 -google-cloud-core==2.5.0 -google-cloud-datastore==2.23.0 -google-cloud-dlp==3.34.0 -google-cloud-kms==3.11.0 -google-cloud-language==2.19.0 -google-cloud-monitoring==2.29.1 +google-cloud-aiplatform==1.153.1 +google-cloud-bigquery==3.41.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 +google-cloud-build==3.36.0 +google-cloud-core==2.6.0 +google-cloud-datastore==2.24.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 +google-cloud-language==2.20.0 +google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.16.0 -google-cloud-secret-manager==2.26.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 -google-cloud-videointelligence==2.18.0 -google-cloud-vision==3.12.1 +google-cloud-resource-manager==1.17.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 +google-cloud-videointelligence==2.19.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.67.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.0 -googleapis-common-protos==1.73.0 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.78.0 -grpcio-status==1.78.0 +grpcio==1.80.0 +grpcio-status==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.16.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -131,21 +133,21 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 +importlib_metadata==9.0.0 iniconfig==2.3.0 interegular==0.3.3 jaraco.classes==3.4.0 -jaraco.context==6.1.1 -jaraco.functools==4.4.0 +jaraco.context==6.1.2 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 -jiter==0.13.0 +jiter==0.15.0 joblib==1.5.3 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.12.2 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 @@ -154,104 +156,119 @@ llguidance==0.7.30 llvmlite==0.44.0 lm-format-enforcer==0.10.12 Markdown==3.10.2 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 -mistral_common==1.10.0 +mistral_common==1.11.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 msgpack==1.1.2 -msgspec==0.20.0 +msgspec==0.21.1 multidict==6.7.1 namex==0.1.0 networkx==3.4.2 ninja==1.13.0 -nltk==3.9.3 +nltk==3.9.4 numba==0.61.2 numpy==2.2.6 +nvidia-cublas==13.1.1.3 nvidia-cublas-cu12==12.6.4.1 +nvidia-cuda-cupti==13.0.85 nvidia-cuda-cupti-cu12==12.6.80 +nvidia-cuda-nvrtc==13.0.88 nvidia-cuda-nvrtc-cu12==12.6.77 +nvidia-cuda-runtime==13.0.96 nvidia-cuda-runtime-cu12==12.6.77 nvidia-cudnn-cu12==9.5.1.17 +nvidia-cudnn-cu13==9.20.0.48 +nvidia-cufft==12.0.0.61 nvidia-cufft-cu12==11.3.0.4 +nvidia-cufile==1.15.1.6 nvidia-cufile-cu12==1.11.1.6 +nvidia-curand==10.4.0.35 nvidia-curand-cu12==10.3.7.77 +nvidia-cusolver==12.0.4.66 nvidia-cusolver-cu12==11.7.1.2 +nvidia-cusparse==12.6.3.3 nvidia-cusparse-cu12==12.5.4.2 nvidia-cusparselt-cu12==0.6.3 +nvidia-cusparselt-cu13==0.8.1 nvidia-nccl-cu12==2.26.2 +nvidia-nccl-cu13==2.29.7 +nvidia-nvjitlink==13.0.88 nvidia-nvjitlink-cu12==12.6.85 -nvidia-nvshmem-cu12==3.4.5 +nvidia-nvshmem-cu13==3.4.5 +nvidia-nvtx==13.0.85 nvidia-nvtx-cu12==12.6.77 oauth2client==4.1.3 objsize==0.7.1 openai==1.107.1 openai-harmony==0.0.8 opencv-python-headless==4.13.0.92 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.7 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 outlines_core==0.2.10 -packaging==26.0 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 prometheus-fastapi-instrumentator==7.1.0 -prometheus_client==0.24.1 -propcache==0.4.1 -proto-plus==1.27.1 -protobuf==6.33.5 +prometheus_client==0.25.0 +propcache==0.5.2 +proto-plus==1.28.0 +protobuf==6.33.6 psutil==7.2.2 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 py-cpuinfo==9.0.0 pyarrow==23.0.1 pyarrow-hotfix==0.7 -pyasn1==0.6.2 +pyasn1==0.6.3 pyasn1_modules==0.4.2 pybase64==1.4.3 pycountry==26.2.16 pycparser==3.0 -pydantic==2.12.5 -pydantic-extra-types==2.11.0 -pydantic-settings==2.13.1 -pydantic_core==2.41.5 -Pygments==2.19.2 +pydantic==2.13.4 +pydantic-extra-types==2.11.1 +pydantic-settings==2.14.1 +pydantic_core==2.46.4 +pydot==1.4.2 +Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 -python-json-logger==4.0.0 -python-multipart==0.0.22 +python-json-logger==4.1.0 +python-multipart==0.0.29 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 pyzmq==27.1.0 -ray==2.54.0 +ray==2.55.1 referencing==0.37.0 -regex==2026.2.28 -requests==2.32.5 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 -rich-toolkit==0.19.7 +rich==15.0.0 +rich-toolkit==0.19.10 rignore==0.7.6 rpds-py==0.30.0 rsa==4.9.1 @@ -261,55 +278,55 @@ scipy==1.15.3 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.54.0 +sentry-sdk==2.60.0 setproctitle==1.3.7 -setuptools==82.0.1 +setuptools==81.0.0 shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soundfile==0.13.1 soupsieve==2.8.3 -soxr==1.0.0 -SQLAlchemy==2.0.48 +soxr==1.1.0 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 starlette==0.52.1 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorboard==2.20.0 tensorboard-data-server==0.7.2 tensorflow==2.20.0 tensorflow-cpu-aws==2.20.0;platform_machine=="aarch64" termcolor==3.3.0 -testcontainers==4.14.1 +testcontainers==4.14.2 threadpoolctl==3.6.0 -tiktoken==0.12.0 +tiktoken==0.13.0 tokenizers==0.21.4 -tomli==2.4.0 +tomli==2.4.1 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 tqdm==4.67.3 transformers==4.55.4 triton==3.3.1 -typer==0.24.1 +typer==0.25.1 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 -urllib3==2.6.3 -uvicorn==0.41.0 +urllib3==2.7.0 +uvicorn==0.47.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 -watchfiles==1.1.1 +watchfiles==1.2.0 websockets==16.0 -Werkzeug==3.1.6 -wheel==0.46.3 -wrapt==2.1.2 +Werkzeug==3.1.8 +wheel==0.47.0 +wrapt==2.2.1 xformers==0.0.31 xgrammar==0.1.21 -yarl==1.23.0 -zipp==3.23.0 +yarl==1.24.2 +zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py311/base_image_requirements.txt b/sdks/python/container/ml/py311/base_image_requirements.txt index 27025b1130e3..c7ffd60a9da3 100644 --- a/sdks/python/container/ml/py311/base_image_requirements.txt +++ b/sdks/python/container/ml/py311/base_image_requirements.txt @@ -23,8 +23,8 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -36,74 +36,74 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.3.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -111,12 +111,12 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 +importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -124,56 +124,57 @@ Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 multidict==6.7.1 namex==0.1.0 networkx==3.6.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pyjsparser==2.7.1 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -182,13 +183,13 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 +rich==15.0.0 rpds-py==0.30.0 rsa==4.9.1 safetensors==0.7.0 @@ -201,11 +202,11 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 @@ -217,14 +218,14 @@ tqdm==4.67.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 tzlocal==5.3.1 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 +zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py311/gpu_image_requirements.txt b/sdks/python/container/ml/py311/gpu_image_requirements.txt index 01caaf42565a..547cfd308fe0 100644 --- a/sdks/python/container/ml/py311/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py311/gpu_image_requirements.txt @@ -23,105 +23,107 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.12.1 +anyio==4.13.0 asn1crypto==1.5.1 astor==0.8.1 astunparse==1.6.3 -attrs==25.4.0 +attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 blake3==1.0.8 bs4==0.0.2 -build==1.4.0 +build==1.5.0 cachetools==6.2.6 -cbor2==5.8.0 -certifi==2026.2.25 +cbor2==6.1.1 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.5 -click==8.3.1 -cloud-sql-python-connector==1.20.0 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 cloudpickle==3.1.2 compressed-tensors==0.10.2 crcmod==1.7 -cryptography==46.0.5 -cuda-bindings==12.9.4 -cuda-pathfinder==1.4.2 +cryptography==47.0.0 +cuda-bindings==13.2.0 +cuda-pathfinder==1.5.4 +cuda-toolkit==13.0.2 cupy-cuda12x==14.0.1 Cython==3.2.4 depyf==0.19.0 +detect-installer==0.1.0 dill==0.3.1.1 diskcache==5.6.3 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 einops==0.8.2 email-validator==2.3.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastapi==0.135.1 +fastapi==0.136.1 fastapi-cli==0.0.24 -fastapi-cloud-cli==0.15.0 -fastar==0.8.0 -fastavro==1.12.1 +fastapi-cloud-cli==0.18.0 +fastar==0.11.0 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.2.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -gguf==0.18.0 -google-api-core==2.30.0 -google-api-python-client==2.192.0 +gguf==0.19.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.141.0 -google-cloud-bigquery==3.40.1 -google-cloud-bigquery-storage==2.36.2 -google-cloud-bigtable==2.35.0 -google-cloud-build==3.35.0 -google-cloud-core==2.5.0 -google-cloud-datastore==2.23.0 -google-cloud-dlp==3.34.0 -google-cloud-kms==3.11.0 -google-cloud-language==2.19.0 -google-cloud-monitoring==2.29.1 +google-cloud-aiplatform==1.153.1 +google-cloud-bigquery==3.41.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 +google-cloud-build==3.36.0 +google-cloud-core==2.6.0 +google-cloud-datastore==2.24.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 +google-cloud-language==2.20.0 +google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.16.0 -google-cloud-secret-manager==2.26.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 -google-cloud-videointelligence==2.18.0 -google-cloud-vision==3.12.1 +google-cloud-resource-manager==1.17.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 +google-cloud-videointelligence==2.19.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.67.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.0 -googleapis-common-protos==1.73.0 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.78.0 -grpcio-status==1.78.0 -grpcio-tools==1.78.0 +grpcio==1.80.0 +grpcio-status==1.80.0 +grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.16.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -130,21 +132,21 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 +importlib_metadata==9.0.0 iniconfig==2.3.0 interegular==0.3.3 jaraco.classes==3.4.0 -jaraco.context==6.1.1 -jaraco.functools==4.4.0 +jaraco.context==6.1.2 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 -jiter==0.13.0 +jiter==0.15.0 joblib==1.5.3 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 @@ -153,84 +155,99 @@ llguidance==0.7.30 llvmlite==0.44.0 lm-format-enforcer==0.10.12 Markdown==3.10.2 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 -mistral_common==1.10.0 +mistral_common==1.11.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 msgpack==1.1.2 -msgspec==0.20.0 +msgspec==0.21.1 multidict==6.7.1 namex==0.1.0 networkx==3.6.1 ninja==1.13.0 -nltk==3.9.3 +nltk==3.9.4 numba==0.61.2 numpy==2.2.6 +nvidia-cublas==13.1.1.3 nvidia-cublas-cu12==12.6.4.1 +nvidia-cuda-cupti==13.0.85 nvidia-cuda-cupti-cu12==12.6.80 +nvidia-cuda-nvrtc==13.0.88 nvidia-cuda-nvrtc-cu12==12.6.77 +nvidia-cuda-runtime==13.0.96 nvidia-cuda-runtime-cu12==12.6.77 nvidia-cudnn-cu12==9.5.1.17 +nvidia-cudnn-cu13==9.20.0.48 +nvidia-cufft==12.0.0.61 nvidia-cufft-cu12==11.3.0.4 +nvidia-cufile==1.15.1.6 nvidia-cufile-cu12==1.11.1.6 +nvidia-curand==10.4.0.35 nvidia-curand-cu12==10.3.7.77 +nvidia-cusolver==12.0.4.66 nvidia-cusolver-cu12==11.7.1.2 +nvidia-cusparse==12.6.3.3 nvidia-cusparse-cu12==12.5.4.2 nvidia-cusparselt-cu12==0.6.3 +nvidia-cusparselt-cu13==0.8.1 nvidia-nccl-cu12==2.26.2 +nvidia-nccl-cu13==2.29.7 +nvidia-nvjitlink==13.0.88 nvidia-nvjitlink-cu12==12.6.85 -nvidia-nvshmem-cu12==3.4.5 +nvidia-nvshmem-cu13==3.4.5 +nvidia-nvtx==13.0.85 nvidia-nvtx-cu12==12.6.77 oauth2client==4.1.3 objsize==0.7.1 openai==1.107.1 openai-harmony==0.0.8 opencv-python-headless==4.13.0.92 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.7 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 outlines_core==0.2.10 -packaging==26.0 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 prometheus-fastapi-instrumentator==7.1.0 -prometheus_client==0.24.1 -propcache==0.4.1 -proto-plus==1.27.1 -protobuf==6.33.5 +prometheus_client==0.25.0 +propcache==0.5.2 +proto-plus==1.28.0 +protobuf==6.33.6 psutil==7.2.2 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 py-cpuinfo==9.0.0 pyarrow==23.0.1 pyarrow-hotfix==0.7 -pyasn1==0.6.2 +pyasn1==0.6.3 pyasn1_modules==0.4.2 pybase64==1.4.3 pycountry==26.2.16 pycparser==3.0 -pydantic==2.12.5 -pydantic-extra-types==2.11.0 -pydantic-settings==2.13.1 -pydantic_core==2.41.5 -Pygments==2.19.2 +pydantic==2.13.4 +pydantic-extra-types==2.11.1 +pydantic-settings==2.14.1 +pydantic_core==2.46.4 +pydot==1.4.2 +Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -238,19 +255,19 @@ pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 -python-json-logger==4.0.0 -python-multipart==0.0.22 +python-json-logger==4.1.0 +python-multipart==0.0.29 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 pyzmq==27.1.0 -ray==2.54.0 +ray==2.55.1 referencing==0.37.0 -regex==2026.2.28 -requests==2.32.5 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 -rich-toolkit==0.19.7 +rich==15.0.0 +rich-toolkit==0.19.10 rignore==0.7.6 rpds-py==0.30.0 rsa==4.9.1 @@ -260,30 +277,30 @@ scipy==1.17.1 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.54.0 +sentry-sdk==2.60.0 setproctitle==1.3.7 -setuptools==82.0.1 +setuptools==81.0.0 shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soundfile==0.13.1 soupsieve==2.8.3 -soxr==1.0.0 -SQLAlchemy==2.0.48 +soxr==1.1.0 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 starlette==0.52.1 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorboard==2.20.0 tensorboard-data-server==0.7.2 tensorflow==2.20.0 tensorflow-cpu-aws==2.20.0;platform_machine=="aarch64" termcolor==3.3.0 -testcontainers==4.14.1 +testcontainers==4.14.2 threadpoolctl==3.6.0 -tiktoken==0.12.0 +tiktoken==0.13.0 tokenizers==0.21.4 torch==2.7.1 torchaudio==2.7.1 @@ -291,23 +308,23 @@ torchvision==0.22.1 tqdm==4.67.3 transformers==4.55.4 triton==3.3.1 -typer==0.24.1 +typer==0.25.1 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 -urllib3==2.6.3 -uvicorn==0.41.0 +urllib3==2.7.0 +uvicorn==0.47.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 -watchfiles==1.1.1 +watchfiles==1.2.0 websockets==16.0 -Werkzeug==3.1.6 -wheel==0.46.3 -wrapt==2.1.2 +Werkzeug==3.1.8 +wheel==0.47.0 +wrapt==2.2.1 xformers==0.0.31 xgrammar==0.1.21 -yarl==1.23.0 -zipp==3.23.0 +yarl==1.24.2 +zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index 761753b6801a..9ab5d88a0efa 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -23,8 +23,8 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -35,74 +35,74 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.3.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -110,67 +110,67 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 multidict==6.7.1 namex==0.1.0 networkx==3.6.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -179,13 +179,13 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.0 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 +rich==15.0.0 rpds-py==0.30.0 rsa==4.9.1 safetensors==0.7.0 @@ -198,29 +198,28 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 threadpoolctl==3.6.0 tokenizers==0.21.4 -torch==2.8.0 +torch==2.8.0+cpu tqdm==4.67.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index 2b947b22e23a..3c4d570820a5 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -23,104 +23,106 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.12.1 +anyio==4.13.0 asn1crypto==1.5.1 astor==0.8.1 astunparse==1.6.3 -attrs==25.4.0 +attrs==26.1.0 beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 blake3==1.0.8 bs4==0.0.2 -build==1.4.0 +build==1.5.0 cachetools==6.2.6 -cbor2==5.8.0 -certifi==2026.2.25 +cbor2==6.1.1 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.5 -click==8.3.1 -cloud-sql-python-connector==1.20.0 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 cloudpickle==3.1.2 compressed-tensors==0.10.2 crcmod==1.7 -cryptography==46.0.5 -cuda-bindings==12.9.4 -cuda-pathfinder==1.4.2 +cryptography==47.0.0 +cuda-bindings==13.2.0 +cuda-pathfinder==1.5.4 +cuda-toolkit==13.0.2 cupy-cuda12x==14.0.1 Cython==3.2.4 depyf==0.19.0 +detect-installer==0.1.0 dill==0.3.1.1 diskcache==5.6.3 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 einops==0.8.2 email-validator==2.3.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastapi==0.135.1 +fastapi==0.136.1 fastapi-cli==0.0.24 -fastapi-cloud-cli==0.15.0 -fastar==0.8.0 -fastavro==1.12.1 +fastapi-cloud-cli==0.18.0 +fastar==0.11.0 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.2.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -gguf==0.18.0 -google-api-core==2.30.0 -google-api-python-client==2.192.0 +gguf==0.19.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.141.0 -google-cloud-bigquery==3.40.1 -google-cloud-bigquery-storage==2.36.2 -google-cloud-bigtable==2.35.0 -google-cloud-build==3.35.0 -google-cloud-core==2.5.0 -google-cloud-datastore==2.23.0 -google-cloud-dlp==3.34.0 -google-cloud-kms==3.11.0 -google-cloud-language==2.19.0 -google-cloud-monitoring==2.29.1 +google-cloud-aiplatform==1.153.1 +google-cloud-bigquery==3.41.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 +google-cloud-build==3.36.0 +google-cloud-core==2.6.0 +google-cloud-datastore==2.24.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 +google-cloud-language==2.20.0 +google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.16.0 -google-cloud-secret-manager==2.26.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 -google-cloud-videointelligence==2.18.0 -google-cloud-vision==3.12.1 +google-cloud-resource-manager==1.17.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 +google-cloud-videointelligence==2.19.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.67.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.0 -googleapis-common-protos==1.73.0 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.78.0 -grpcio-status==1.78.0 -grpcio-tools==1.78.0 +grpcio==1.80.0 +grpcio-status==1.80.0 +grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.16.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -129,21 +131,20 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 iniconfig==2.3.0 interegular==0.3.3 jaraco.classes==3.4.0 -jaraco.context==6.1.1 -jaraco.functools==4.4.0 +jaraco.context==6.1.2 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 -jiter==0.13.0 +jiter==0.15.0 joblib==1.5.3 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 @@ -152,17 +153,17 @@ llguidance==0.7.30 llvmlite==0.44.0 lm-format-enforcer==0.10.12 Markdown==3.10.2 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 -mistral_common==1.10.0 +mistral_common==1.11.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 msgpack==1.1.2 -msgspec==0.20.0 +msgspec==0.21.1 multidict==6.7.1 namex==0.1.0 networkx==3.6.1 @@ -170,66 +171,81 @@ ninja==1.13.0 nltk==3.9.4 numba==0.61.2 numpy==2.2.6 +nvidia-cublas==13.1.1.3 nvidia-cublas-cu12==12.6.4.1 +nvidia-cuda-cupti==13.0.85 nvidia-cuda-cupti-cu12==12.6.80 +nvidia-cuda-nvrtc==13.0.88 nvidia-cuda-nvrtc-cu12==12.6.77 +nvidia-cuda-runtime==13.0.96 nvidia-cuda-runtime-cu12==12.6.77 nvidia-cudnn-cu12==9.5.1.17 +nvidia-cudnn-cu13==9.20.0.48 +nvidia-cufft==12.0.0.61 nvidia-cufft-cu12==11.3.0.4 +nvidia-cufile==1.15.1.6 nvidia-cufile-cu12==1.11.1.6 +nvidia-curand==10.4.0.35 nvidia-curand-cu12==10.3.7.77 +nvidia-cusolver==12.0.4.66 nvidia-cusolver-cu12==11.7.1.2 +nvidia-cusparse==12.6.3.3 nvidia-cusparse-cu12==12.5.4.2 nvidia-cusparselt-cu12==0.6.3 +nvidia-cusparselt-cu13==0.8.1 nvidia-nccl-cu12==2.26.2 +nvidia-nccl-cu13==2.29.7 +nvidia-nvjitlink==13.0.88 nvidia-nvjitlink-cu12==12.6.85 -nvidia-nvshmem-cu12==3.4.5 +nvidia-nvshmem-cu13==3.4.5 +nvidia-nvtx==13.0.85 nvidia-nvtx-cu12==12.6.77 oauth2client==4.1.3 objsize==0.7.1 openai==1.107.1 openai-harmony==0.0.8 opencv-python-headless==4.13.0.92 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.7 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 outlines_core==0.2.10 -packaging==26.0 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 prometheus-fastapi-instrumentator==7.1.0 -prometheus_client==0.24.1 -propcache==0.4.1 -proto-plus==1.27.1 -protobuf==6.33.5 +prometheus_client==0.25.0 +propcache==0.5.2 +proto-plus==1.28.0 +protobuf==6.33.6 psutil==7.2.2 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 py-cpuinfo==9.0.0 pyarrow==23.0.1 pyarrow-hotfix==0.7 -pyasn1==0.6.2 +pyasn1==0.6.3 pyasn1_modules==0.4.2 pybase64==1.4.3 pycountry==26.2.16 pycparser==3.0 -pydantic==2.12.5 -pydantic-extra-types==2.11.0 -pydantic-settings==2.13.1 -pydantic_core==2.41.5 -Pygments==2.19.2 +pydantic==2.13.4 +pydantic-extra-types==2.11.1 +pydantic-settings==2.14.1 +pydantic_core==2.46.4 +pydot==1.4.2 +Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -237,19 +253,19 @@ pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 -python-json-logger==4.0.0 -python-multipart==0.0.22 +python-json-logger==4.1.0 +python-multipart==0.0.29 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 pyzmq==27.1.0 -ray==2.54.0 +ray==2.55.1 referencing==0.37.0 -regex==2026.2.28 -requests==2.33.0 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 -rich-toolkit==0.19.7 +rich==15.0.0 +rich-toolkit==0.19.10 rignore==0.7.6 rpds-py==0.30.0 rsa==4.9.1 @@ -259,7 +275,7 @@ scipy==1.17.1 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.54.0 +sentry-sdk==2.60.0 setproctitle==1.3.7 setuptools==79.0.1 shellingham==1.5.4 @@ -268,45 +284,44 @@ sniffio==1.3.1 sortedcontainers==2.4.0 soundfile==0.13.1 soupsieve==2.8.3 -soxr==1.0.0 -SQLAlchemy==2.0.48 +soxr==1.1.0 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 starlette==0.52.1 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorboard==2.20.0 tensorboard-data-server==0.7.2 tensorflow==2.20.0 tensorflow-cpu-aws==2.20.0;platform_machine=="aarch64" termcolor==3.3.0 -testcontainers==4.14.1 +testcontainers==4.14.2 threadpoolctl==3.6.0 -tiktoken==0.12.0 +tiktoken==0.13.0 tokenizers==0.21.4 -torch==2.8.0 +torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 tqdm==4.67.3 transformers==4.55.4 triton==3.3.1 -typer==0.24.1 +typer==0.25.1 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 -urllib3==2.6.3 -uvicorn==0.41.0 +urllib3==2.7.0 +uvicorn==0.47.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 -watchfiles==1.1.1 +watchfiles==1.2.0 websockets==16.0 -Werkzeug==3.1.6 -wheel==0.46.3 -wrapt==2.1.2 +Werkzeug==3.1.8 +wheel==0.47.0 +wrapt==2.2.1 xformers==0.0.31 -xgrammar==0.1.32 -yarl==1.23.0 -zipp==3.23.0 +xgrammar==0.1.21 +yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py313/base_image_requirements.txt b/sdks/python/container/ml/py313/base_image_requirements.txt index b92d500a4339..bf8152d8027a 100644 --- a/sdks/python/container/ml/py313/base_image_requirements.txt +++ b/sdks/python/container/ml/py313/base_image_requirements.txt @@ -23,8 +23,8 @@ absl-py==2.4.0 aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -35,73 +35,73 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 -filelock==3.25.2 +filelock==3.29.0 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.3.0 +fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.35 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 +google-genai==2.6.0 google-pasta==0.2.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.4.2 +hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -109,82 +109,82 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 -keras==3.13.2 +keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 -markdown-it-py==4.0.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 mpmath==1.3.0 multidict==6.7.1 namex==0.1.0 networkx==3.6.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 opt_einsum==3.4.0 -optree==0.19.0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +optree==0.19.1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 -rich==14.3.3 +rich==15.0.0 rpds-py==0.30.0 rsa==4.9.1 safetensors==0.7.0 @@ -197,11 +197,11 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 -tenacity==8.5.0 +tenacity==9.1.4 tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 @@ -213,13 +213,12 @@ tqdm==4.67.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/py310/base_image_requirements.txt b/sdks/python/container/py310/base_image_requirements.txt index 1712f0f36806..e40d90c79472 100644 --- a/sdks/python/container/py310/base_image_requirements.txt +++ b/sdks/python/container/py310/base_image_requirements.txt @@ -22,8 +22,8 @@ # Reach out to a committer if you need help. aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -35,65 +35,65 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b7 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy-data-plane==0.2.6 exceptiongroup==1.3.1 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-genai==2.6.0 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 hpack==4.1.0 @@ -102,12 +102,12 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 +importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -120,41 +120,42 @@ keyrings.google-artifactregistry-auth==1.1.2 MarkupSafe==3.0.3 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 multidict==6.7.1 nltk==3.9.4 numpy==2.2.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pyjsparser==2.7.1 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -163,11 +164,11 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 rpds-py==0.30.0 rsa==4.9.1 @@ -180,24 +181,24 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -tenacity==8.5.0 +tenacity==9.1.4 testcontainers==4.14.2 threadpoolctl==3.6.0 tomli==2.4.1 tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 tzlocal==5.3.1 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 +zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/py311/base_image_requirements.txt b/sdks/python/container/py311/base_image_requirements.txt index c7bb3fd7280a..8362e22e172f 100644 --- a/sdks/python/container/py311/base_image_requirements.txt +++ b/sdks/python/container/py311/base_image_requirements.txt @@ -22,8 +22,8 @@ # Reach out to a committer if you need help. aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -34,65 +34,65 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-genai==2.6.0 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 hpack==4.1.0 @@ -101,12 +101,12 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 +importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -119,41 +119,42 @@ keyrings.google-artifactregistry-auth==1.1.2 MarkupSafe==3.0.3 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 multidict==6.7.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pyjsparser==2.7.1 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -162,11 +163,11 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 rpds-py==0.30.0 rsa==4.9.1 @@ -179,23 +180,23 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -tenacity==8.5.0 +tenacity==9.1.4 testcontainers==4.14.2 threadpoolctl==3.6.0 tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 tzlocal==5.3.1 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 +zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/py312/base_image_requirements.txt b/sdks/python/container/py312/base_image_requirements.txt index d0948d43a9e6..f0ca11af6e4b 100644 --- a/sdks/python/container/py312/base_image_requirements.txt +++ b/sdks/python/container/py312/base_image_requirements.txt @@ -22,8 +22,8 @@ # Reach out to a committer if you need help. aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -33,65 +33,65 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.31 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-genai==2.6.0 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 hpack==4.1.0 @@ -100,12 +100,11 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -117,53 +116,54 @@ keyrings.google-artifactregistry-auth==1.1.2 MarkupSafe==3.0.3 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 multidict==6.7.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 rpds-py==0.30.0 rsa==4.9.1 @@ -176,22 +176,21 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -tenacity==8.5.0 +tenacity==9.1.4 testcontainers==4.14.2 threadpoolctl==3.6.0 tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/py313/base_image_requirements.txt b/sdks/python/container/py313/base_image_requirements.txt index 7859cc82569d..e0b9bfb26df1 100644 --- a/sdks/python/container/py313/base_image_requirements.txt +++ b/sdks/python/container/py313/base_image_requirements.txt @@ -22,8 +22,8 @@ # Reach out to a committer if you need help. aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -33,64 +33,64 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.1 -google-api-python-client==2.193.0 +google-api-core==2.30.3 +google-api-python-client==2.196.0 google-apitools==0.5.35 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-genai==2.6.0 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 hpack==4.1.0 @@ -99,12 +99,11 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -116,53 +115,54 @@ keyrings.google-artifactregistry-auth==1.1.2 MarkupSafe==3.0.3 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 multidict==6.7.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==8.4.2 +pytest==9.0.3 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 rpds-py==0.30.0 rsa==4.9.1 @@ -175,22 +175,21 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -tenacity==8.5.0 +tenacity==9.1.4 testcontainers==4.14.2 threadpoolctl==3.6.0 tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 +tzdata==2026.2 uritemplate==4.2.0 -urllib3==2.6.3 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/py314/base_image_requirements.txt b/sdks/python/container/py314/base_image_requirements.txt index 6547fc977535..2ff940c92856 100644 --- a/sdks/python/container/py314/base_image_requirements.txt +++ b/sdks/python/container/py314/base_image_requirements.txt @@ -22,8 +22,8 @@ # Reach out to a committer if you need help. aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.4 +aiohappyeyeballs==2.6.2 +aiohttp==3.13.5 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -33,63 +33,63 @@ beartype==0.22.9 beautifulsoup4==4.14.3 betterproto==2.0.0b6 bs4==0.0.2 -build==1.4.2 +build==1.5.0 cachetools==6.2.6 -certifi==2026.2.25 +certifi==2026.5.20 cffi==2.0.0 -charset-normalizer==3.4.6 -click==8.3.1 -cloud-sql-python-connector==1.20.1 +charset-normalizer==3.4.7 +click==8.4.1 +cloud-sql-python-connector==1.20.2 crcmod==1.7 -cryptography==46.0.6 +cryptography==47.0.0 Cython==3.2.4 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 docker==7.1.0 -docstring_parser==0.17.0 +docstring_parser==0.18.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastavro==1.12.1 +fastavro==1.12.2 fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.1 +google-api-core==2.30.3 google-apitools==0.5.35 -google-auth==2.49.1 +google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.143.0 +google-cloud-aiplatform==1.153.1 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.37.0 -google-cloud-bigtable==2.35.0 +google-cloud-bigquery-storage==2.38.0 +google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 -google-cloud-core==2.5.1 +google-cloud-core==2.6.0 google-cloud-datastore==2.24.0 -google-cloud-dlp==3.35.0 -google-cloud-kms==3.12.0 +google-cloud-dlp==3.36.0 +google-cloud-kms==3.13.0 google-cloud-language==2.20.0 google-cloud-monitoring==2.30.0 -google-cloud-pubsub==2.36.0 +google-cloud-pubsub==2.38.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.27.0 -google-cloud-spanner==3.63.0 -google-cloud-storage==2.19.0 +google-cloud-secret-manager==2.28.0 +google-cloud-spanner==3.66.0 +google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.13.0 +google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==1.69.0 -google-resumable-media==2.8.2 -googleapis-common-protos==1.73.1 -greenlet==3.3.2 -grpc-google-iam-v1==0.14.3 +google-genai==2.6.0 +google-resumable-media==2.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.1 +grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 grpcio==1.80.0 grpcio-status==1.80.0 grpcio-tools==1.80.0 grpclib==0.4.9 -guppy3==3.1.6 +guppy3==3.1.7 h11==0.16.0 h2==4.3.0 hpack==4.1.0 @@ -98,12 +98,11 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.11 -importlib_metadata==8.7.1 +idna==3.16 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 -jaraco.functools==4.4.0 +jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 @@ -115,40 +114,41 @@ keyrings.google-artifactregistry-auth==1.1.2 MarkupSafe==3.0.3 mmh3==5.2.1 mock==5.2.0 -more-itertools==10.8.0 +more-itertools==11.1.0 multidict==6.7.1 nltk==3.9.4 -numpy==2.4.4 +numpy==2.4.6 oauth2client==4.1.3 objsize==0.7.1 -opentelemetry-api==1.40.0 -opentelemetry-resourcedetector-gcp==1.11.0a0 -opentelemetry-sdk==1.40.0 -opentelemetry-semantic-conventions==0.61b0 -oracledb==3.4.2 -orjson==3.11.8 -packaging==26.0 +opentelemetry-api==1.42.1 +opentelemetry-resourcedetector-gcp==1.12.0a0 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +oracledb==4.0.1 +orjson==3.11.9 +packaging==26.2 pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 -pillow==12.1.1 -pip==26.0.1 +pillow==12.2.0 +pip==26.1.1 pluggy==1.6.0 -propcache==0.4.1 -proto-plus==1.27.2 +propcache==0.5.2 +proto-plus==1.28.0 protobuf==6.33.6 -psycopg2-binary==2.9.11 +psycopg2-binary==2.9.12 pyarrow==23.0.1 pyarrow-hotfix==0.7 pyasn1==0.6.3 pyasn1_modules==0.4.2 pycparser==3.0 -pydantic==2.12.5 -pydantic_core==2.41.5 +pydantic==2.13.4 +pydantic_core==2.46.4 +pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pymongo==4.16.0 -PyMySQL==1.1.2 +pymongo==4.17.0 +PyMySQL==1.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 pytest==9.0.3 @@ -157,11 +157,11 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-tds==1.17.1 -pytz==2026.1.post1 +pytz==2026.2 PyYAML==6.0.3 referencing==0.37.0 -regex==2026.3.32 -requests==2.33.1 +regex==2026.5.9 +requests==2.34.2 requests-mock==1.12.1 rpds-py==0.30.0 rsa==4.9.1 @@ -174,21 +174,20 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.3 -SQLAlchemy==2.0.48 +SQLAlchemy==2.0.49 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -tenacity==8.5.0 +tenacity==9.1.4 testcontainers==4.14.2 threadpoolctl==3.6.0 tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 -tzdata==2025.3 -urllib3==2.6.3 +tzdata==2026.2 +urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 -wheel==0.46.3 -wrapt==2.1.2 -yarl==1.23.0 -zipp==3.23.0 +wheel==0.47.0 +wrapt==2.2.1 +yarl==1.24.2 zstandard==0.25.0 From c437dbaf67192dc038025e989512582ab6370e51 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Sat, 23 May 2026 14:27:11 +0200 Subject: [PATCH 224/490] Count cancelled runs in last-5 flaky check (#38614) --- .../sync/github/github_runs_prefetcher/code/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py index 76d8e488bce5..2b91c862a7f6 100644 --- a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py +++ b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py @@ -188,10 +188,14 @@ def filter_workflow_runs(run, issue): print(f"Success rate: {success_rate}") - # Check if last 5 runs are all failures - last_5_failed = len(workflow_runs) >= 5 and all(run.status == "failure" for run in workflow_runs[:5]) + # Check if last 5 runs are all failures or cancelled (e.g. job timeout) + last_5_failed = len(workflow_runs) >= 5 and all( + run.status in ("failure", "cancelled") for run in workflow_runs[:5] + ) if last_5_failed: - print(f"The last 5 workflow runs for {workflow.name} have all failed") + print( + f"The last 5 workflow runs for {workflow.name} have all failed or been cancelled" + ) return success_rate < workflow.threshold or last_5_failed From b93cfac51c57b0fc775c63c044fe8406851c075e Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Sun, 24 May 2026 08:12:52 -0400 Subject: [PATCH 225/490] Reduce python expansion service startup time (#38611) * Revert "Fix test hang in subprocess expansion service on port bind failure (#38572)" This reverts commit 930b94cceb69e33bd9a9a2f1287ebe5c75533536. * Ensure immediate cleanup of subprocess server on start failure When a SubprocessServer fails to start (e.g., due to a process exit or startup error), the server process could leak if standard purging is blocked by other active owners sharing the cached subprocess. To fix this: - Implement `_SharedCache.force_remove()` to immediately remove a key from the cache and run its destructor regardless of active owners. - Add `SubprocessServer.stop_force()` which calls `force_remove()` to completely terminate the server's process. - Call `stop_force()` in the `except` block of `SubprocessServer.start()` * Support modern manylinux tags based on pip version in Stager This ensures we can download pre-built wheels for environment staging rather than relying on tarball building, which is sometimes slow. * Formatting * Trigger more python tests. * Typo --- .../beam_PostCommit_Python_Versions.json | 2 +- .../portability/expansion_service_main.py | 14 +-- .../apache_beam/runners/portability/stager.py | 8 +- .../apache_beam/utils/subprocess_server.py | 106 ++++++++++-------- .../utils/subprocess_server_test.py | 87 ++++++++++++++ 5 files changed, 155 insertions(+), 62 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json index 541dc4ea8e87..8ed972c9f579 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Versions.json +++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 2 + "revision": 3 } diff --git a/sdks/python/apache_beam/runners/portability/expansion_service_main.py b/sdks/python/apache_beam/runners/portability/expansion_service_main.py index f2d03e0e898c..269d02b3efbd 100644 --- a/sdks/python/apache_beam/runners/portability/expansion_service_main.py +++ b/sdks/python/apache_beam/runners/portability/expansion_service_main.py @@ -55,9 +55,7 @@ def main(argv): with fully_qualified_named_transform.FullyQualifiedNamedTransform.with_filter( known_args.fully_qualified_name_glob): - # Bind to localhost instead of 0.0.0.0 to ensure compatibility with loopback - # connections on dual-stack (IPv4/IPv6) systems. - address = 'localhost:{}'.format(known_args.port) + address = '0.0.0.0:{}'.format(known_args.port) server = grpc.server(thread_pool_executor.shared_unbounded_instance()) if known_args.serve_loopback_worker: beam_fn_api_pb2_grpc.add_BeamFnExternalWorkerPoolServicer_to_server( @@ -73,15 +71,9 @@ def main(argv): artifact_service.ArtifactRetrievalService( artifact_service.BeamFilesystemHandler(None).file_reader), server) - # Ensure gRPC server successfully binds. If this fails (e.g., due to port collision), - # add_insecure_port returns 0. We raise an error to crash the subprocess immediately, - # allowing the parent process to detect it and fail fast rather than hanging. - bound_port = server.add_insecure_port(address) - if not bound_port: - raise RuntimeError( - "Failed to bind expansion service to {}".format(address)) + server.add_insecure_port(address) server.start() - _LOGGER.info('Listening for expansion requests at %d', bound_port) + _LOGGER.info('Listening for expansion requests at %d', known_args.port) def cleanup(unused_signum, unused_frame): _LOGGER.info('Shutting down expansion service.') diff --git a/sdks/python/apache_beam/runners/portability/stager.py b/sdks/python/apache_beam/runners/portability/stager.py index e862fde4efef..136c320da009 100644 --- a/sdks/python/apache_beam/runners/portability/stager.py +++ b/sdks/python/apache_beam/runners/portability/stager.py @@ -732,9 +732,11 @@ def _get_platform_for_default_sdk_container(): # addressed, download wheel based on glibc version in Beam's Python # Base image pip_version = distribution('pip').version - if version.parse(pip_version) >= version.parse('19.3'): - # pip can only recognize manylinux2014_x86_64 wheels - # from version 19.3. + # See more information about manylinux at + # https://github.com/pypa/manylinux + if version.parse(pip_version) >= version.parse('20.3'): + return 'manylinux_2_28_x86_64' + elif version.parse(pip_version) >= version.parse('19.3'): return 'manylinux2014_x86_64' else: return 'manylinux2010_x86_64' diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index b22e6badb5e7..0b09b364362f 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -118,6 +118,12 @@ def get(self, *key): self._cache[key].owners.add(owner) return self._cache[key].obj + def force_remove(self, *key): + with self._lock: + entry = self._cache.pop(key, None) + if entry is not None: + self._destructor(entry.obj) + class JavaHelper: @classmethod @@ -186,53 +192,45 @@ def __exit__(self, *unused_args): self.stop() def start(self): - max_attempts = 3 - for attempt in range(max_attempts): - try: - process, endpoint = self.start_process() - wait_secs = .1 - channel_options = [ - ("grpc.max_receive_message_length", -1), - ("grpc.max_send_message_length", -1), - # Default: 20000ms (20s), increased to 10 minutes for stability - ("grpc.keepalive_timeout_ms", 600_000), - # Default: 2, set to 0 to allow unlimited pings without data - ("grpc.http2.max_pings_without_data", 0), - # Default: False, set to True to allow keepalive pings when no calls - ("grpc.keepalive_permit_without_calls", True), - # Default: 2, set to 0 to allow unlimited ping strikes - ("grpc.http2.max_ping_strikes", 0), - # Default: 0 (disabled), enable socket reuse for better handling - ("grpc.so_reuseport", 1), - ] - self._grpc_channel = grpc.insecure_channel( - endpoint, options=channel_options) - channel_ready = grpc.channel_ready_future(self._grpc_channel) - while True: - if process is not None and process.poll() is not None: - _LOGGER.error("Started job service with %s", process.args) - raise RuntimeError( - 'Service failed to start up with error %s' % process.poll()) - try: - channel_ready.result(timeout=wait_secs) - break - except (grpc.FutureTimeoutError, grpc.RpcError): - wait_secs *= 1.2 - logging.log( - logging.WARNING if wait_secs > 1 else logging.DEBUG, - 'Waiting for grpc channel to be ready at %s.', - endpoint) - return self._stub_class(self._grpc_channel) - except Exception as e: - _LOGGER.warning( - "Error bringing up service on attempt %d: %s", - attempt + 1, - e, - exc_info=True) - self.stop() - if attempt == max_attempts - 1: - raise - time.sleep(1) + try: + process, endpoint = self.start_process() + wait_secs = .1 + channel_options = [ + ("grpc.max_receive_message_length", -1), + ("grpc.max_send_message_length", -1), + # Default: 20000ms (20s), increased to 10 minutes for stability + ("grpc.keepalive_timeout_ms", 600_000), + # Default: 2, set to 0 to allow unlimited pings without data + ("grpc.http2.max_pings_without_data", 0), + # Default: False, set to True to allow keepalive pings when no calls + ("grpc.keepalive_permit_without_calls", True), + # Default: 2, set to 0 to allow unlimited ping strikes + ("grpc.http2.max_ping_strikes", 0), + # Default: 0 (disabled), enable socket reuse for better handling + ("grpc.so_reuseport", 1), + ] + self._grpc_channel = grpc.insecure_channel( + endpoint, options=channel_options) + channel_ready = grpc.channel_ready_future(self._grpc_channel) + while True: + if process is not None and process.poll() is not None: + _LOGGER.error("Started job service with %s", process.args) + raise RuntimeError( + 'Service failed to start up with error %s' % process.poll()) + try: + channel_ready.result(timeout=wait_secs) + break + except (grpc.FutureTimeoutError, grpc.RpcError): + wait_secs *= 1.2 + logging.log( + logging.WARNING if wait_secs > 1 else logging.DEBUG, + 'Waiting for grpc channel to be ready at %s.', + endpoint) + return self._stub_class(self._grpc_channel) + except: # pylint: disable=bare-except + _LOGGER.exception("Error bringing up service") + self.stop_force() + raise def start_process(self): if self._owner_id is not None: @@ -282,6 +280,20 @@ def stop_process(self): finally: self._grpc_channel = None + def stop_force(self): + try: + self._cache.force_remove(tuple(self._cmd), self._port, self._logger) + finally: + self._owner_id = None + if self._grpc_channel: + try: + self._grpc_channel.close() + except: # pylint: disable=bare-except + _LOGGER.error( + "Could not close the gRPC channel started with cmd %s", self._cmd) + finally: + self._grpc_channel = None + def _really_stop_process(process_and_endpoint): process, _ = process_and_endpoint # pylint: disable=unpacking-non-sequence if not process: diff --git a/sdks/python/apache_beam/utils/subprocess_server_test.py b/sdks/python/apache_beam/utils/subprocess_server_test.py index 073b8b3bcbe8..a44b89b17e37 100644 --- a/sdks/python/apache_beam/utils/subprocess_server_test.py +++ b/sdks/python/apache_beam/utils/subprocess_server_test.py @@ -464,6 +464,93 @@ def __init__(self): # without raising ValueError. server.stop_process() + def test_force_remove(self): + destructor_calls = [] + + def custom_destructor(obj): + destructor_calls.append(obj) + + cache = subprocess_server._SharedCache(self.with_prefix, custom_destructor) + + owner1 = cache.register() + owner2 = cache.register() + + # Get object 'a' under both active owners + a = cache.get('a') + self.assertEqual(a[0], 'a') + self.assertIn(('a', ), cache._cache) + + # force_remove on a non-existent key should be a safe no-op + cache.force_remove('non_existent') + + # Call force_remove, which should bypass the owners check and delete it immediately + cache.force_remove('a') + + # The cache entry should be gone + self.assertNotIn(('a', ), cache._cache) + + # Destructor must be called on 'a' + self.assertEqual(destructor_calls, [a]) + + # Retrieving 'a' again under the active owners should construct a new object + new_a = cache.get('a') + self.assertNotEqual(new_a, a) + self.assertEqual(new_a[0], 'a') + + # Clean up + cache.purge(owner1) + cache.purge(owner2) + + def test_subprocess_server_start_failed_no_leak(self): + destructor_calls = [] + + def custom_destructor(obj): + destructor_calls.append(obj) + + class DummyProcess: + def __init__(self): + self.args = ["dummy_cmd"] + + def poll(self): + return 1 # Simulate that process exited/failed + + dummy_process = DummyProcess() + cache = subprocess_server._SharedCache( + lambda *args: (dummy_process, "localhost:12345"), custom_destructor) + + # 1. Register an independent, unrelated owner in the cache first. + other_owner = cache.register() + + class CustomServer(subprocess_server.SubprocessServer): + _cache = cache + + def __init__(self): + super().__init__(lambda channel: None, ["dummy_cmd"], port=12345) + + server = CustomServer() + # Fetch the process using other_owner, creating the cache entry and registering other_owner on it. + cache.get(tuple(server._cmd), server._port, server._logger) + + cache_key = (tuple(server._cmd), server._port, server._logger) + self.assertIn(cache_key, cache._cache) + self.assertEqual(cache._cache[cache_key].owners, {other_owner}) + + # 2. Verify starting the server (which registers its own owner and retrieves from cache) raises RuntimeError + with self.assertRaises(RuntimeError): + server.start() + + # 3. Verify that the destructor was called on the process, meaning no leak (even though other_owner was still registered!) + self.assertEqual(destructor_calls, [(dummy_process, "localhost:12345")]) + + # 4. Verify that the server has cleaned up its owner_id + self.assertIsNone(server._owner_id) + + # 5. Verify the cache entry has been removed completely + self.assertNotIn(cache_key, cache._cache) + + # Clean up the other owner + cache.purge(other_owner) + if __name__ == '__main__': unittest.main() From 1e1ca2d8102987af3920c64347d6da027c9f069d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Mon, 25 May 2026 15:49:10 +0200 Subject: [PATCH 226/490] Update action.yml upgrade gradle action to enable the CI - 5.0.0 was removed by apache/infrastructure-actions@9ef334e --- .github/actions/setup-environment-action/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-environment-action/action.yml b/.github/actions/setup-environment-action/action.yml index aa10257e3f93..fb5f91de4551 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -76,7 +76,7 @@ runs: distribution: 'temurin' java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 with: cache-disabled: ${{ inputs.disable-cache }} validate-wrappers: false From 6d2e32683dcaba9ed9584f29ddc3fc11b50b94a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:22:34 -0400 Subject: [PATCH 227/490] Bump docker/login-action from 4.1.0 to 4.2.0 (#38627) Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_release_candidate.yml | 2 +- .github/workflows/finalize_release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index 3304c189361d..e1fdb238c901 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -293,7 +293,7 @@ jobs: # settings.xml file run: rm ~/.m2/settings.xml || true - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 295b09f10837..6beb1dd4f45a 100644 --- a/.github/workflows/finalize_release.yml +++ b/.github/workflows/finalize_release.yml @@ -44,7 +44,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} From 10265f6a359123a781815b4492a22c615acdbb3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:24:01 -0400 Subject: [PATCH 228/490] Bump github.com/aws/aws-sdk-go-v2/config in /sdks (#38629) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.17 to 1.32.18. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.17...config/v1.32.18) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index fc79db727abf..82e8e35d385f 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,8 +33,8 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.1 github.com/aws/aws-sdk-go-v2 v1.41.7 - github.com/aws/aws-sdk-go-v2/config v1.32.17 - github.com/aws/aws-sdk-go-v2/credentials v1.19.16 + github.com/aws/aws-sdk-go-v2/config v1.32.18 + github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 @@ -160,7 +160,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 4060068e9e75..f82208f289d2 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,12 +207,12 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= -github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q= +github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= -github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/credentials v1.19.17 h1:gP2nkGsS+KMvF/jfFz2Vv2qiiOqWKyPACSzPsqHgoW8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.17/go.mod h1:Bsew3S/moG5iT77giPj1q8wb/s0RE5/QfH+ASjYtuQc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= @@ -266,8 +266,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 h1:nDARhv/oF55bcxF7rCI/4PDxOKnVXVWwDuDwCs2I2SQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= From e3d1e21a4eb2a60df13992edd679286494570ae5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:24:33 -0400 Subject: [PATCH 229/490] Bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#38628) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/beam_PostCommit_Go.yml | 2 +- .github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml | 2 +- .../workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml | 2 +- .github/workflows/beam_PostCommit_Python_Arm.yml | 2 +- .github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml | 2 +- .github/workflows/beam_PreCommit_CommunityMetrics.yml | 2 +- .github/workflows/beam_PreCommit_PythonDocker.yml | 2 +- .github/workflows/beam_Publish_Beam_SDK_Snapshots.yml | 2 +- .../workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml | 2 +- .github/workflows/build_release_candidate.yml | 2 +- .github/workflows/build_runner_image.yml | 2 +- .github/workflows/republish_released_docker_containers.yml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/beam_PostCommit_Go.yml b/.github/workflows/beam_PostCommit_Go.yml index 5b42821f3cd5..aacaa4f8171b 100644 --- a/.github/workflows/beam_PostCommit_Go.yml +++ b/.github/workflows/beam_PostCommit_Go.yml @@ -73,7 +73,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml index d71ca934cdff..71942e21ee30 100644 --- a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml @@ -78,7 +78,7 @@ jobs: java-version: default go-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml index ea1302c825d8..57069b34fef0 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml @@ -84,7 +84,7 @@ jobs: ${{ matrix.java_version != '11' && matrix.java_version || '' }} 11 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Python_Arm.yml b/.github/workflows/beam_PostCommit_Python_Arm.yml index 32dd751d79e7..c226f1a771a8 100644 --- a/.github/workflows/beam_PostCommit_Python_Arm.yml +++ b/.github/workflows/beam_PostCommit_Python_Arm.yml @@ -91,7 +91,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml index 3961517c2707..90d1eb786522 100644 --- a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml @@ -78,7 +78,7 @@ jobs: with: python-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: run XVR GoUsingJava Dataflow script env: USER: github-actions diff --git a/.github/workflows/beam_PreCommit_CommunityMetrics.yml b/.github/workflows/beam_PreCommit_CommunityMetrics.yml index 6ea7a5019711..72bb47fd7314 100644 --- a/.github/workflows/beam_PreCommit_CommunityMetrics.yml +++ b/.github/workflows/beam_PreCommit_CommunityMetrics.yml @@ -83,7 +83,7 @@ jobs: with: java-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Remove default github maven configuration run: rm ~/.m2/settings.xml - name: Install docker compose diff --git a/.github/workflows/beam_PreCommit_PythonDocker.yml b/.github/workflows/beam_PreCommit_PythonDocker.yml index 9499499affe3..e933df2b5ef1 100644 --- a/.github/workflows/beam_PreCommit_PythonDocker.yml +++ b/.github/workflows/beam_PreCommit_PythonDocker.yml @@ -83,7 +83,7 @@ jobs: python-version: ${{ matrix.python_version }} go-version: default - name: Setup Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 with: install: true driver: 'docker' diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index 96648c6b79bd..06214a200123 100644 --- a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml +++ b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml @@ -99,7 +99,7 @@ jobs: if: github.ref == 'refs/heads/master' run: echo "LATEST_TAG=,latest" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Authenticate on GCP uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 with: diff --git a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml index 9ce1dec8ef7b..a7f35055b0b7 100644 --- a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml +++ b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml @@ -84,7 +84,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index e1fdb238c901..c53c0712a600 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -286,7 +286,7 @@ jobs: with: python-version: '3.10' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Remove default github maven configuration # This step is a workaround to avoid a decryption issue of Beam's # net.linguica.gradle.maven.settings plugin and github's provided maven diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index e866ecf96933..7cd67ef0192e 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -45,7 +45,7 @@ jobs: run: | gcloud auth configure-docker ${{env.docker_registry}} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Build and Load to docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index 7519f61d1e5b..48e9b7101a80 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -80,7 +80,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Remove default github maven configuration # This step is a workaround to avoid a decryption issue of Beam's # net.linguica.gradle.maven.settings plugin and github's provided maven From 38448d79708920394df4465d99e9fdadb7228aa0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 12:15:25 -0400 Subject: [PATCH 230/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38630) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.18 to 1.22.19. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.18...feature/s3/manager/v1.22.19) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.19 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 82e8e35d385f..124b4c57252e 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 github.com/docker/go-connections v0.6.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index f82208f289d2..d96528191d94 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8Tc github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18 h1:9XFUd2lkr7VrbE4Qtrhm7AtNhGgZeGFI5QLZtQIflj8= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18/go.mod h1:trImuKdWelQIJALvyGj6sKolJ1W8t628JOoTdDGVL9Q= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 h1:VH0xfFwHfPYhu+EcxyCcw3VTZskpbA+/s0pTXwhSsL8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19/go.mod h1:S/XkAXcnCpzwsjC9EU0BakuvreXfSTUADHb7rC7jvaQ= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= From f44ef90aef5d5e41198e1d512a2cef923401ef16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 07:57:52 -0400 Subject: [PATCH 231/490] Update cython requirement from <4,>=3.0 to >=3.2.5,<4 in /sdks/python (#38626) Updates the requirements on [cython](https://github.com/cython/cython) to permit the latest version. - [Release notes](https://github.com/cython/cython/releases) - [Changelog](https://github.com/cython/cython/blob/master/CHANGES.rst) - [Commits](https://github.com/cython/cython/compare/3.0.0...3.2.5) --- updated-dependencies: - dependency-name: cython dependency-version: 3.2.5 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 636849f54c9e..7da1d29acef3 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -32,7 +32,7 @@ requires = [ # Numpy headers "numpy>=1.14.3,<2.5.0", # Update setup.py as well. # having cython here will create wheels that are platform dependent. - "cython>=3.0,<4", + "cython>=3.2.5,<4", ## deps for generating external transform wrappers: # also update PyYaml bounds in sdks:python:generateExternalTransformsConfig 'pyyaml>=3.12,<7.0.0', From 7d92432915f0b95a05ef11034d09802f653b7408 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 26 May 2026 08:03:20 -0400 Subject: [PATCH 232/490] Fix flaky test_csv_to_json in Beam YAML SDK (#38617) --- .github/trigger_files/beam_PostCommit_Python_Versions.json | 2 +- sdks/python/apache_beam/yaml/yaml_transform_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json index 8ed972c9f579..9cc78c7d1c6c 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Versions.json +++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 3 + "revision": 4 } diff --git a/sdks/python/apache_beam/yaml/yaml_transform_test.py b/sdks/python/apache_beam/yaml/yaml_transform_test.py index bbb60b185c01..192af63a9871 100644 --- a/sdks/python/apache_beam/yaml/yaml_transform_test.py +++ b/sdks/python/apache_beam/yaml/yaml_transform_test.py @@ -287,7 +287,7 @@ def test_csv_to_json(self): output_shard = all_output[0] result = pd.read_json( output_shard, orient='records', - lines=True).sort_values('rank').reindex() + lines=True).sort_values('rank').reset_index(drop=True) pd.testing.assert_frame_equal(data, result) def test_circular_reference_validation(self): From 6ce2ed3daac4403f7ce09a0cf2756f5b2e17b40d Mon Sep 17 00:00:00 2001 From: Joe Santos <54154621+joesantos418@users.noreply.github.com> Date: Tue, 26 May 2026 11:40:15 -0300 Subject: [PATCH 233/490] Update go dependencies (#38530) * Update go dependencies Update all go dependencies, including github.com/docker/docker which was replaced by github.com/moby/moby * Fix logging logic Improves logging logic according to the review points raised by copilot * Rebase and add new updates * chore: rebase and update * fix go.sum entries missing error --- sdks/go.mod | 79 ++++---- sdks/go.sum | 186 ++++++++---------- .../runners/prism/internal/environments.go | 93 +++++---- 3 files changed, 177 insertions(+), 181 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 124b4c57252e..e72db8be4996 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -31,14 +31,14 @@ require ( cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 - cloud.google.com/go/storage v1.62.1 + cloud.google.com/go/storage v1.62.2 github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 github.com/google/go-cmp v0.7.0 @@ -61,7 +61,7 @@ require ( golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 google.golang.org/api v0.280.0 - google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 + google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 @@ -72,21 +72,23 @@ require ( github.com/avast/retry-go/v4 v4.7.0 github.com/fsouza/fake-gcs-server v1.52.3 github.com/golang-cz/devslog v0.0.15 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 + github.com/moby/moby/api v1.54.2 + github.com/moby/moby/client v0.4.1 + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a ) require ( - cel.dev/expr v0.25.1 // indirect + cel.dev/expr v0.25.2 // indirect cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/monitoring v1.25.0 // indirect - cloud.google.com/go/pubsub/v2 v2.4.0 // indirect + cloud.google.com/go/monitoring v1.29.0 // indirect + cloud.google.com/go/pubsub/v2 v2.6.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect @@ -103,13 +105,10 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/go-tpm v0.9.8 // indirect - github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/minio/highwayhash v1.0.4 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/moby/api v1.54.1 // indirect - github.com/moby/moby/client v0.4.0 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/nats-io/jwt/v2 v2.8.1 // indirect @@ -118,32 +117,31 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/shirou/gopsutil/v4 v4.26.4 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.einride.tech/aip v0.83.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect - golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect + golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 // indirect golang.org/x/time v0.15.0 // indirect ) require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.7.0 // indirect + cloud.google.com/go/iam v1.11.0 // indirect cloud.google.com/go/longrunning v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -166,48 +164,45 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/docker/docker v28.5.2+incompatible // but required to resolve issue docker has with go1.20 github.com/docker/go-units v0.5.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/flatbuffers v24.12.23+incompatible // indirect - github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/google/pprof v0.0.0-20260507013755-92041b743c96 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.6 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/montanaflynn/stats v0.9.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/xattr v0.4.10 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect github.com/sirupsen/logrus v1.9.4 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - github.com/zeebo/xxh3 v1.0.2 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index d96528191d94..f8bfd7d24a0c 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -54,8 +54,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/datacatalog v1.27.0 h1:AnghhtHKCqYIe62gTPHcn9nJr5jtxjZHV4D/Fob23gg= -cloud.google.com/go/datacatalog v1.27.0/go.mod h1:YTI11pFlC5HCj4CphEf+qWCy/z9udd7o0HVN6c2Povg= +cloud.google.com/go/datacatalog v1.32.0 h1:fyYn8ODkGil5y3zTIqgIhOfzTu1ACaU2o+C750CO6Ac= +cloud.google.com/go/datacatalog v1.32.0/go.mod h1:DE272tynQUwheJeQAyVfV+nO8yrdkuDyOgH2LtOrkWM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.24.0 h1:auNUPJTT9gFcHNj2iKOEeE23nrjf7dE7VA6TO3jw8h0= @@ -64,20 +64,20 @@ cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= -cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= +cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= +cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= -cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= -cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= -cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= +cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k= +cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= -cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= -cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc= +cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY= +cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= cloud.google.com/go/profiler v0.6.0 h1:Vwxqgnp8CQwBNcKjO8luwLCh7qblEkcZCtMUvhU9Yik= cloud.google.com/go/profiler v0.6.0/go.mod h1:cJV7Qfj0o9PAC7q/xQTkM6qn2FO9So3TFk4P5O5yLis= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -87,8 +87,8 @@ cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjp cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= cloud.google.com/go/pubsub v1.50.2 h1:54Up97HnThdP4H8jjWJSSQ/mnYG2EKon7ZSNETRq0tM= cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkNvjj4sp0w= -cloud.google.com/go/pubsub/v2 v2.4.0 h1:oMKNiBQpXImRWnHYla9uSU66ZzByZwBSCJOEs/pTKVg= -cloud.google.com/go/pubsub/v2 v2.4.0/go.mod h1:2lS/XQKq5qtOMs6kHBK+WX1ytUC36kLl2ig3zqsGUx8= +cloud.google.com/go/pubsub/v2 v2.6.0 h1:8pjR0id+GTB+krKx5G6AGJoYrHog58w2Q89PCOrfM64= +cloud.google.com/go/pubsub/v2 v2.6.0/go.mod h1:4anqvV/w8Pcgu2tO0qr2XgsF3GXHowzryfQ5gOnVmWY= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= cloud.google.com/go/spanner v1.91.0 h1:XwXfcZ0kc1NT9Uu2IsThFiWtYptB+WgLn/KZEZcyzRg= cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= @@ -99,12 +99,12 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8= -cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= +cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4= +cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= -cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= -cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= +cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= @@ -163,14 +163,14 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0 h1:ZIT85vKP7LBS84XJ0WdJ3dPOX3iz4j3c0+lpajGQMyo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0/go.mod h1:rqP9UEhOXv9WhQ7Gjz+G5y/pf8+BJZW5/Ts0AhE0PwE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -281,8 +281,6 @@ github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/i github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -336,10 +334,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -415,8 +411,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-cz/devslog v0.0.15 h1:ejoBLTCwJHWGbAmDf2fyTJJQO3AkzcPjw8SC9LaOQMI= @@ -471,16 +467,16 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= -github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -495,7 +491,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= @@ -528,8 +523,8 @@ github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M= +github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= @@ -543,8 +538,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= -github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= +github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -557,10 +552,7 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -643,8 +635,8 @@ github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -668,8 +660,8 @@ github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolMjgVI= github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -695,14 +687,12 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= -github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= -github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -718,10 +708,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ= +github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= github.com/nats-io/nats-server/v2 v2.14.1 h1:wXs/a5fw9Hzm3CvuzLxGeIwpjPulSa7gMT3eSuhGkcg= @@ -743,13 +731,12 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= @@ -778,8 +765,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= -github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -792,8 +779,9 @@ github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -822,16 +810,16 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xitongsys/parquet-go v1.5.1/go.mod h1:xUxwM8ELydxh4edHGegYq1pA8NnMKDx0K/GyB0o2bww= @@ -853,8 +841,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.einride.tech/aip v0.83.0 h1:TI21IdeOnLTwZEJ3BxtImIZk6bsN2Q+sd0x99SLiQ+M= go.einride.tech/aip v0.83.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig= @@ -873,18 +861,14 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= @@ -896,8 +880,6 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHS go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -941,8 +923,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -957,8 +939,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -995,8 +977,8 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1178,8 +1160,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1280,8 +1262,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1437,12 +1419,12 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= -google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 h1:cTHF8xtqtBN5sQ4dcoNwOS6FFejvFTkWQbZXsTU3trM= +google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc= +google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw= +google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/sdks/go/pkg/beam/runners/prism/internal/environments.go b/sdks/go/pkg/beam/runners/prism/internal/environments.go index 1f852e0862f1..47f58f8f2532 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/environments.go +++ b/sdks/go/pkg/beam/runners/prism/internal/environments.go @@ -37,11 +37,10 @@ import ( "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/mount" - dcli "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/moby/api/pkg/stdcopy" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + dcli "github.com/moby/moby/client" ) // TODO move environment handling to the worker package. @@ -170,7 +169,7 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock logger = logger.With("worker_id", wk.ID, "image", dp.GetContainerImage()) // TODO consider preserving client? - cli, err := dcli.NewClientWithOpts(dcli.FromEnv, dcli.WithAPIVersionNegotiation()) + cli, err := dcli.New(dcli.FromEnv) if err != nil { return fmt.Errorf("couldn't connect to docker:%w", err) } @@ -196,9 +195,9 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock } else { logger.Debug("local GCP credentials environment variable not found") } - if _, _, err := cli.ImageInspectWithRaw(ctx, dp.GetContainerImage()); err != nil { + if _, err := cli.ImageInspect(ctx, dp.GetContainerImage()); err != nil { // We don't have a local image, so we should pull it. - if rc, err := cli.ImagePull(ctx, dp.GetContainerImage(), image.PullOptions{}); err == nil { + if rc, err := cli.ImagePull(ctx, dp.GetContainerImage(), dcli.ImagePullOptions{}); err == nil { // Copy the output, but discard it so we can wait until the image pull is finished. io.Copy(io.Discard, rc) rc.Close() @@ -215,16 +214,19 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock fmt.Sprintf("--provision_endpoint=%v", wk.Endpoint()), fmt.Sprintf("--logging_endpoint=%v", wk.Endpoint()), } - ccr, err := cli.ContainerCreate(ctx, &container.Config{ - Image: dp.GetContainerImage(), - Cmd: cmd, - Env: envs, - Tty: false, - }, &container.HostConfig{ - NetworkMode: "host", - Mounts: mounts, - AutoRemove: true, - }, nil, nil, "") + ccr, err := cli.ContainerCreate(ctx, dcli.ContainerCreateOptions{ + Config: &container.Config{ + Image: dp.GetContainerImage(), + Cmd: cmd, + Env: envs, + Tty: false, + }, + HostConfig: &container.HostConfig{ + NetworkMode: "host", + Mounts: mounts, + AutoRemove: true, + }, + }) if err != nil { cli.Close() return fmt.Errorf("unable to create container image %v with docker for env %v, err: %w", dp.GetContainerImage(), wk.Env, err) @@ -232,7 +234,8 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock containerID := ccr.ID logger = logger.With("container", containerID) - if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + _, err = cli.ContainerStart(ctx, containerID, dcli.ContainerStartOptions{}) + if err != nil { cli.Close() return fmt.Errorf("unable to start container image %v with docker for env %v, err: %w", dp.GetContainerImage(), wk.Env, err) } @@ -249,10 +252,24 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock }() bgctx := context.Background() - statusCh, errCh := cli.ContainerWait(bgctx, containerID, container.WaitConditionNotRunning) + + // Wait for either context cancellation or container to stop + type waitResult struct { + err error + } + done := make(chan waitResult) + go func() { + result := cli.ContainerWait(bgctx, containerID, dcli.ContainerWaitOptions{ + Condition: container.WaitConditionNotRunning, + }) + // Error is a channel in the new API + err := <-result.Error + done <- waitResult{err: err} + }() + select { case <-ctx.Done(): - rc, err := cli.ContainerLogs(bgctx, containerID, container.LogsOptions{Details: true, ShowStdout: true, ShowStderr: true}) + rc, err := cli.ContainerLogs(bgctx, containerID, dcli.ContainerLogsOptions{Details: true, ShowStdout: true, ShowStderr: true}) if err != nil { logger.Error("error fetching container logs error on context cancellation", "error", err) } @@ -268,25 +285,27 @@ func dockerEnvironment(ctx context.Context, logger *slog.Logger, dp *pipepb.Dock logger.Debug("container log", "log", msgs) } // Can't use command context, since it's already canceled here. - if err := cli.ContainerKill(bgctx, containerID, ""); err != nil { - logger.Error("docker container kill error", "error", err) - } - case err := <-errCh: + _, err = cli.ContainerKill(bgctx, containerID, dcli.ContainerKillOptions{}) if err != nil { - logger.Error("docker container wait error", "error", err) + logger.Error("docker container kill error", "error", err) } - case resp := <-statusCh: - logger.Info("docker container has self terminated", "status_code", resp.StatusCode) - - rc, err := cli.ContainerLogs(bgctx, containerID, container.LogsOptions{Details: true, ShowStdout: true, ShowStderr: true}) - if err != nil { - logger.Error("docker container logs error", "error", err) - return + case result := <-done: + // Container stopped on its own + if result.err != nil { + logger.Error("docker container wait error", "error", result.err) + // Fetch and log container output on error + rc, err := cli.ContainerLogs(bgctx, containerID, dcli.ContainerLogsOptions{Details: true, ShowStdout: true, ShowStderr: true}) + if err != nil { + logger.Error("failed to fetch container logs after wait error", "error", err) + } else { + defer rc.Close() + var buf bytes.Buffer + stdcopy.StdCopy(&buf, &buf, rc) + logger.Error("container logs after wait error", "log", buf.String()) + } + } else { + logger.Info("container terminated on its own") } - defer rc.Close() - var buf bytes.Buffer - stdcopy.StdCopy(&buf, &buf, rc) - logger.Error("container self terminated", "log", buf.String()) } }() From 04f6c346757ed4668de9aa1566596db5a8cf361b Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Tue, 26 May 2026 17:01:39 +0200 Subject: [PATCH 234/490] Fix Fn API data plane deadlock when outbound queue is full (#38581) --- .../apache_beam/runners/worker/data_plane.py | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/sdks/python/apache_beam/runners/worker/data_plane.py b/sdks/python/apache_beam/runners/worker/data_plane.py index a5589ac33a1b..cfefa37d76b6 100644 --- a/sdks/python/apache_beam/runners/worker/data_plane.py +++ b/sdks/python/apache_beam/runners/worker/data_plane.py @@ -68,6 +68,9 @@ # Keep a set of completed instructions to discard late received data. The set # can have up to _MAX_CLEANED_INSTRUCTIONS items. See _GrpcDataChannel. _MAX_CLEANED_INSTRUCTIONS = 10000 +_DEFAULT_SEND_QUEUE_MAX_ELEMENTS = 10000 +_DEFAULT_SEND_QUEUE_MAX_BYTES = 100 << 20 # 100MB +_DEFAULT_RECEIVE_QUEUE_MAX_ELEMENTS = 5 # retry on transient UNAVAILABLE grpc error from data channels. _GRPC_SERVICE_CONFIG = json.dumps({ @@ -459,10 +462,20 @@ def __init__(self, data_buffer_time_limit_ms=0): self._data_buffer_time_limit_ms = data_buffer_time_limit_ms self._to_send = ByteLimitedQueue( - maxsize=10000, - maxbytes=100 << 20) # type: ByteLimitedQueue[DataOrTimers] + maxsize=_DEFAULT_SEND_QUEUE_MAX_ELEMENTS, + maxbytes=_DEFAULT_SEND_QUEUE_MAX_BYTES + ) # type: ByteLimitedQueue[DataOrTimers] + # Staging queue so a full send buffer does not block reading inputs. + self._pending_send = ByteLimitedQueue( + maxsize=_DEFAULT_SEND_QUEUE_MAX_ELEMENTS, + maxbytes=_DEFAULT_SEND_QUEUE_MAX_BYTES + ) # type: ByteLimitedQueue[DataOrTimers] + self._send_forwarder = None # type: Optional[threading.Thread] + self._start_send_forwarder() self._received = collections.defaultdict( - lambda: ByteLimitedQueue(maxsize=5, maxbytes=100 << 20) + lambda: ByteLimitedQueue( + maxsize=_DEFAULT_RECEIVE_QUEUE_MAX_ELEMENTS, maxbytes= + _DEFAULT_SEND_QUEUE_MAX_BYTES) ) # type: DefaultDict[str, ByteLimitedQueue[DataOrTimers]] # Keep a cache of completed instructions. Data for completed instructions @@ -478,9 +491,40 @@ def __init__(self, data_buffer_time_limit_ms=0): def close(self): # type: () -> None - self._to_send.put(self._WRITES_FINISHED, 0) + self._enqueue_to_send(self._WRITES_FINISHED) + if self._send_forwarder is not None: + self._send_forwarder.join() + if self._exception: + raise self._exception self._closed = True + def _start_send_forwarder(self): + # type: () -> None + forwarder = threading.Thread( + target=self._forward_pending_to_send, name='forward_grpc_outputs') + forwarder.daemon = True + forwarder.start() + self._send_forwarder = forwarder + + def _enqueue_to_send(self, elem): + # type: (DataOrTimers) -> None + size = self._get_element_size_bytes(elem) + self._pending_send.put((elem, size), size) + + def _forward_pending_to_send(self): + # type: () -> None + try: + while True: + elem, size = self._pending_send.get() + self._to_send.put(elem, size) + if elem is self._WRITES_FINISHED: + return + except Exception as e: + if not self._closed: + _LOGGER.exception('Failed to forward outputs in the data plane.') + self._exception = e + raise + def wait(self, timeout=None): # type: (Optional[int]) -> None self._reads_finished.wait(timeout) @@ -591,7 +635,7 @@ def add_to_send_queue(data): if data: elem = beam_fn_api_pb2.Elements.Data( instruction_id=instruction_id, transform_id=transform_id, data=data) - self._to_send.put(elem, self._get_element_size_bytes(elem)) + self._enqueue_to_send(elem) def close_callback(data): # type: (bytes) -> None @@ -601,7 +645,7 @@ def close_callback(data): instruction_id=instruction_id, transform_id=transform_id, is_last=True) - self._to_send.put(elem, self._get_element_size_bytes(elem)) + self._enqueue_to_send(elem) return ClosableOutputStream.create( close_callback, add_to_send_queue, self._data_buffer_time_limit_ms) @@ -622,7 +666,7 @@ def add_to_send_queue(timer): timer_family_id=timer_family_id, timers=timer, is_last=False) - self._to_send.put(elem, self._get_element_size_bytes(elem)) + self._enqueue_to_send(elem) def close_callback(timer): # type: (bytes) -> None @@ -632,7 +676,7 @@ def close_callback(timer): transform_id=transform_id, timer_family_id=timer_family_id, is_last=True) - self._to_send.put(elem, self._get_element_size_bytes(elem)) + self._enqueue_to_send(elem) return ClosableOutputStream.create( close_callback, add_to_send_queue, self._data_buffer_time_limit_ms) From f90c8c8465875efad2070088e2ee7abecc845389 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 26 May 2026 12:41:27 -0400 Subject: [PATCH 235/490] [yaml] add support for matchall (#38512) * add support for matchall * fix lint * fix more lint * fix gemini comments * move matchAll to standard providers --- sdks/python/apache_beam/yaml/standard_io.yaml | 1 + .../apache_beam/yaml/standard_providers.yaml | 1 + .../apache_beam/yaml/tests/match_all.yaml | 96 ++++++++++++++ sdks/python/apache_beam/yaml/yaml_io.py | 57 +++++++++ sdks/python/apache_beam/yaml/yaml_io_test.py | 118 ++++++++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 sdks/python/apache_beam/yaml/tests/match_all.yaml diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index 53d336ca0fa6..a48c3accff43 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -116,6 +116,7 @@ 'ReadFromTFRecord': 'apache_beam.yaml.yaml_io.read_from_tfrecord' 'WriteToTFRecord': 'apache_beam.yaml.yaml_io.write_to_tfrecord' + # General File Formats # Declared as a renaming transform to avoid exposing all # (implementation-specific) pandas arguments and aligning with possible Java diff --git a/sdks/python/apache_beam/yaml/standard_providers.yaml b/sdks/python/apache_beam/yaml/standard_providers.yaml index 31eb5e1c6daa..a2a334bd3ae2 100644 --- a/sdks/python/apache_beam/yaml/standard_providers.yaml +++ b/sdks/python/apache_beam/yaml/standard_providers.yaml @@ -57,6 +57,7 @@ transforms: MLTransform: 'apache_beam.yaml.yaml_ml.ml_transform' RunInference: 'apache_beam.yaml.yaml_ml.run_inference' + MatchAll: 'apache_beam.yaml.yaml_io.match_all' - type: renaming transforms: diff --git a/sdks/python/apache_beam/yaml/tests/match_all.yaml b/sdks/python/apache_beam/yaml/tests/match_all.yaml new file mode 100644 index 000000000000..4ec5e245a6d4 --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/match_all.yaml @@ -0,0 +1,96 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +fixtures: + - name: TEMP_DIR + type: "tempfile.TemporaryDirectory" + +pipelines: + # 1. Write first matching file to temp directory + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - {value: "a"} + - type: WriteToText + config: + path: "{TEMP_DIR}/match1.txt" + + # 2. Write second matching file to temp directory + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - {value: "b"} + - type: WriteToText + config: + path: "{TEMP_DIR}/match2.txt" + + # 3. Write an ignore file to temp directory (should not be matched) + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - {value: "c"} + - type: WriteToText + config: + path: "{TEMP_DIR}/ignore.txt" + + # 4. Match files using MatchAll and verify only match1 and match2 are found + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - {pattern: "{TEMP_DIR}/match*.txt*"} + - type: MatchAll + config: + file_pattern: pattern + - type: MapToFields + config: + language: python + fields: + filename: "path.split('/')[-1].split('-')[0]" + - type: AssertEqual + config: + elements: + - {filename: "match1.txt"} + - {filename: "match2.txt"} + + # 5. Match non-existent files with ALLOW empty_match_treatment (should succeed but return empty PCollection) + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - {pattern: "{TEMP_DIR}/does_not_exist*.txt*"} + - type: MatchAll + config: + file_pattern: pattern + empty_match_treatment: ALLOW + - type: AssertEqual + config: + elements: [] + diff --git a/sdks/python/apache_beam/yaml/yaml_io.py b/sdks/python/apache_beam/yaml/yaml_io.py index 92f8ec47cca7..f59e730ac815 100644 --- a/sdks/python/apache_beam/yaml/yaml_io.py +++ b/sdks/python/apache_beam/yaml/yaml_io.py @@ -723,3 +723,60 @@ def write_to_tfrecord( num_shards=num_shards, shard_name_template=shard_name_template, compression_type=getattr(CompressionTypes, compression_type)) + + +@beam.ptransform_fn +def match_all( + pcoll, + *, + file_pattern: Optional[str] = None, + empty_match_treatment: str = 'ALLOW', +): + """Matches file patterns from the input PCollection. + + This transform returns a PCollection of matching files, each represented as a + Row with path, size_in_bytes, and last_updated_in_seconds fields. + + Args: + file_pattern (str): The name of the field in the input PCollection that contains + the file pattern string. If not specified and the input PCollection has + exactly one field, that field will be used. + empty_match_treatment (str): How to treat empty matches. Possible values are + 'ALLOW', 'DISALLOW', and 'ALLOW_IF_WILDCARD'. Defaults to 'ALLOW'. + """ + from apache_beam.typehints import schemas + + try: + field_names = [ + name for name, _ in schemas.named_fields_from_element_type( + pcoll.element_type) + ] + except Exception: + field_names = None + + if field_names: + if file_pattern is not None: + if file_pattern not in field_names: + raise ValueError( + f"Field '{file_pattern}' not found in input schema fields: {field_names}" + ) + pattern_field = file_pattern + elif len(field_names) == 1: + pattern_field = field_names[0] + else: + raise ValueError( + f"Input schema has multiple fields {field_names}. " + f"Please specify the 'file_pattern' parameter to select which field " + f"contains the file pattern.") + patterns = pcoll | beam.Map(lambda x: str(getattr(x, pattern_field))) + else: + patterns = pcoll + + matched = patterns | beam.io.fileio.MatchAll( + empty_match_treatment=empty_match_treatment) + + return matched | beam.Map( + lambda x: beam.Row( + path=str(x.path), size_in_bytes=int(x.size_in_bytes), + last_updated_in_seconds=float(x.last_updated_in_seconds) + if x.last_updated_in_seconds is not None else None)) diff --git a/sdks/python/apache_beam/yaml/yaml_io_test.py b/sdks/python/apache_beam/yaml/yaml_io_test.py index 1e13038512cd..e6219277bf58 100644 --- a/sdks/python/apache_beam/yaml/yaml_io_test.py +++ b/sdks/python/apache_beam/yaml/yaml_io_test.py @@ -18,6 +18,8 @@ import io import json import logging +import os +import tempfile import unittest import fastavro @@ -543,6 +545,122 @@ def test_read_proto(self): assert_that(result, equal_to(data)) +class YamlMatchAllTest(unittest.TestCase): + def test_match_all_simple(self): + with tempfile.TemporaryDirectory() as temp_dir: + file1 = os.path.join(temp_dir, 'file1.txt') + file2 = os.path.join(temp_dir, 'file2.txt') + for f in [file1, file2]: + with open(f, 'w') as fout: + fout.write('data') + + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + result = ( + p + | beam.Create( + [beam.Row(pattern=os.path.join(temp_dir, 'file*.txt'))]) + | YamlTransform( + ''' + type: MatchAll + config: + file_pattern: pattern + ''')) + paths = result | beam.Map(lambda row: row.path) + assert_that(paths, equal_to([file1, file2])) + + def test_match_all_single_field_default(self): + with tempfile.TemporaryDirectory() as temp_dir: + file1 = os.path.join(temp_dir, 'file1.txt') + with open(file1, 'w') as fout: + fout.write('data') + + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + result = ( + p + | beam.Create([beam.Row(my_sole_pattern=file1)]) + | YamlTransform( + ''' + type: MatchAll + ''')) + paths = result | beam.Map(lambda row: row.path) + assert_that(paths, equal_to([file1])) + + def test_match_all_multiple_fields_error(self): + with self.assertRaises(Exception): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + _ = ( + p + | beam.Create([beam.Row(pattern='foo', other_field='bar')]) + | YamlTransform( + ''' + type: MatchAll + ''')) + + def test_match_all_empty_match_disallow_error(self): + with self.assertRaises(Exception): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + _ = ( + p + | beam.Create([beam.Row(pattern='does_not_exist*.txt')]) + | YamlTransform( + ''' + type: MatchAll + config: + empty_match_treatment: DISALLOW + ''')) + + def test_match_all_invalid_field_error(self): + with self.assertRaisesRegex( + ValueError, "Field 'invalid_field' not found in input schema fields"): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + _ = ( + p + | beam.Create([beam.Row(pattern='foo')]) + | YamlTransform( + ''' + type: MatchAll + config: + file_pattern: invalid_field + ''')) + + def test_match_all_none_timestamp(self): + from apache_beam.io.filesystem import FileMetadata + + class MockMatchAll(beam.PTransform): + def expand(self, pcoll): + return pcoll.pipeline | beam.Create([ + FileMetadata( + path='file.txt', + size_in_bytes=100, + last_updated_in_seconds=None) + ]) + + with mock.patch('apache_beam.io.fileio.MatchAll', + return_value=MockMatchAll()): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + result = ( + p + | beam.Create([beam.Row(pattern='file.txt')]) + | YamlTransform( + ''' + type: MatchAll + ''')) + assert_that( + result, + equal_to([ + beam.Row( + path='file.txt', + size_in_bytes=100, + last_updated_in_seconds=None) + ])) + + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) unittest.main() From 9897ef6d804dfdaf718bb1ab3e041353f8245d09 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Tue, 26 May 2026 16:36:03 -0400 Subject: [PATCH 236/490] Update BEAM_DEV_SDK_CONTAINER_TAG to new version (#38699) --- sdks/python/apache_beam/runners/dataflow/internal/names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/names.py b/sdks/python/apache_beam/runners/dataflow/internal/names.py index 6ecd6b1769b3..4ea99f413fb6 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/names.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/names.py @@ -35,6 +35,6 @@ # Update this tag whenever there is a change that # requires changes to SDK harness container or SDK harness launcher. -BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260427' +BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260526' DATAFLOW_CONTAINER_IMAGE_REPOSITORY = 'gcr.io/cloud-dataflow/v1beta3' From bdcff87ce2b1cf3f9c8222ff598114d7f8d0501c Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Wed, 27 May 2026 01:17:07 -0700 Subject: [PATCH 237/490] [Dataflow Streaming] Add Operation::finishKey() and move processTimers into it (#38430) * [Dataflow] Add Operation::finishKey() and move timers into it Moving the processTimers call from finish() to finishKey(). In upcoming changes there'll be multiple streaming work items in a single beam bundle. With multiple work items, we've to process elements and timers of each work item before moving to the next work items. finishKey() will be called by the NativeIterator classes after iterating through all elements from a work item. Batch processes timers in BatchModeUngroupingParDoFn and does not rely on the processTimers() in ParDoOperation::finish(). So removing the processTimers() call from ParDoOperation::finish() is safe. Batch also does not use the new finishKey() method. --- .../runners/dataflow/worker/PubsubReader.java | 14 +---- .../worker/StreamingModeExecutionContext.java | 22 ++++++- .../worker/UngroupedWindmillReader.java | 14 +---- .../worker/WindmillReaderIteratorBase.java | 11 +++- .../worker/WindowingWindmillReader.java | 2 + .../WorkerCustomSourceOperationExecutor.java | 3 + .../dataflow/worker/WorkerCustomSources.java | 9 ++- .../streaming/ComputationWorkExecutor.java | 2 +- .../util/common/worker/FlattenOperation.java | 3 + .../util/common/worker/MapTaskExecutor.java | 7 +++ .../worker/util/common/worker/Operation.java | 3 + .../util/common/worker/ParDoOperation.java | 7 ++- .../util/common/worker/ReadOperation.java | 3 + .../util/common/worker/WorkExecutor.java | 3 + .../util/common/worker/WriteOperation.java | 3 + .../worker/IntrinsicMapTaskExecutorTest.java | 12 ++++ .../worker/StreamingDataflowWorkerTest.java | 4 +- .../StreamingModeExecutionContextTest.java | 18 ++++-- .../WindmillReaderIteratorBaseTest.java | 60 ++++++++++++++++++- .../worker/WorkerCustomSourcesTest.java | 26 +++++--- .../util/common/worker/ExecutorTestUtils.java | 3 + .../common/worker/MapTaskExecutorTest.java | 12 ++++ .../common/worker/ParDoOperationTest.java | 2 + 23 files changed, 195 insertions(+), 48 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PubsubReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PubsubReader.java index b60cb84415ff..0c80909a0f3b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PubsubReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PubsubReader.java @@ -117,20 +117,12 @@ public NativeReader create( @Override public NativeReaderIterator> iterator() throws IOException { - return new PubsubReaderIterator(context.getWorkItem()); + return new PubsubReaderIterator(); } class PubsubReaderIterator extends WindmillReaderIteratorBase { - protected PubsubReaderIterator(Windmill.WorkItem work) { - super(work, skipUndecodableElements); - } - - @Override - public boolean advance() throws IOException { - if (context.workIsFailed()) { - return false; - } - return super.advance(); + protected PubsubReaderIterator() { + super(context, skipUndecodableElements); } @Override diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java index e1f1b21e135b..25ce299adf7a 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java @@ -17,6 +17,7 @@ */ package org.apache.beam.runners.dataflow.worker; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; @@ -56,6 +57,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInput; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputState; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; +import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataId; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataRequest; @@ -157,6 +159,9 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext activeReader; + private @Nullable WorkExecutor workExecutor; + private boolean finishKeyCalled = false; + public StreamingModeExecutionContext( CounterFactory counterFactory, String computationId, @@ -240,9 +245,12 @@ public void start( Work work, WindmillStateReader stateReader, SideInputStateFetcher sideInputStateFetcher, - Windmill.WorkItemCommitRequest.Builder outputBuilder) { + Windmill.WorkItemCommitRequest.Builder outputBuilder, + WorkExecutor workExecutor) { this.key = key; this.work = work; + this.workExecutor = workExecutor; + this.finishKeyCalled = false; this.computationKey = WindmillComputationKey.create(computationId, work.getShardedKey()); this.sideInputStateFetcher = sideInputStateFetcher; StreamingGlobalConfig config = globalConfigHandle.getConfig(); @@ -271,6 +279,17 @@ public void start( } } + public void finishKey() { + checkState(!finishKeyCalled, "finishKey was already called"); + checkStateNotNull(workExecutor, "workExecutor must be set before calling finishKey()"); + try { + workExecutor.finishKey(); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.finishKeyCalled = true; + } + /** * Ensure that the processing time is greater than any fired processing time timers. Otherwise, a * trigger could ignore the timer and orphan the window. @@ -452,6 +471,7 @@ public void invalidateCache() { } public Map> flushState() { + checkState(finishKeyCalled, "finishKey must be called before flushState"); Map> callbacks = new HashMap<>(); for (StepContext stepContext : getAllStepContexts()) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java index 1bcf8cf179cd..bd68adeddfb9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java @@ -110,20 +110,12 @@ public NativeReader create( @Override public NativeReaderIterator> iterator() throws IOException { - return new UngroupedWindmillReaderIterator(context.getWorkItem()); + return new UngroupedWindmillReaderIterator(); } class UngroupedWindmillReaderIterator extends WindmillReaderIteratorBase { - UngroupedWindmillReaderIterator(Windmill.WorkItem work) { - super(work, skipUndecodableElements); - } - - @Override - public boolean advance() throws IOException { - if (context.workIsFailed()) { - return false; - } - return super.advance(); + UngroupedWindmillReaderIterator() { + super(context, skipUndecodableElements); } @Override diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java index 7e6508a4788c..075a1a8a4250 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java @@ -34,6 +34,7 @@ */ public abstract class WindmillReaderIteratorBase extends NativeReader.NativeReaderIterator> { + private final StreamingModeExecutionContext context; private final Windmill.WorkItem work; private int bundleIndex = 0; private int messageIndex = -1; @@ -42,9 +43,10 @@ public abstract class WindmillReaderIteratorBase private static final Logger LOG = LoggerFactory.getLogger(WindmillReaderIteratorBase.class); protected WindmillReaderIteratorBase( - Windmill.WorkItem work, ValueProvider skipUndecodableElements) { + StreamingModeExecutionContext context, ValueProvider skipUndecodableElements) { + this.context = context; this.skipUndecodableElements = skipUndecodableElements; - this.work = work; + this.work = context.getWorkItem(); } @Override @@ -54,9 +56,14 @@ public boolean start() throws IOException { @Override public boolean advance() throws IOException { + if (context.workIsFailed()) { + throw new WorkItemCancelledException(context.getWorkItem().getShardingKey()); + } + while (true) { if (bundleIndex >= work.getMessageBundlesCount()) { current = null; + context.finishKey(); return false; } Windmill.InputMessageBundle bundle = work.getMessageBundles(bundleIndex); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java index 173b254f6395..488684769bd9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java @@ -156,6 +156,7 @@ public NativeReaderIterator>> iterator() throw return new NativeReaderIterator>>() { @Override public boolean start() throws IOException { + context.finishKey(); return false; } @@ -182,6 +183,7 @@ public boolean start() throws IOException { @Override public boolean advance() throws IOException { current = null; + context.finishKey(); return false; } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourceOperationExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourceOperationExecutor.java index 31528e96e07f..a1321d57ebb6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourceOperationExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourceOperationExecutor.java @@ -89,6 +89,9 @@ public void execute() throws Exception { LOG.debug("Source operation execution complete"); } + @Override + public void finishKey() throws Exception {} + @Override public SourceOperationResponse getResponse() { return response; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSources.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSources.java index c00edffeaf95..29d5fb3561a1 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSources.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSources.java @@ -824,6 +824,7 @@ public boolean start() throws IOException { } try { if (!reader.start()) { + context.finishKey(); return false; } } catch (Exception e) { @@ -841,10 +842,13 @@ public boolean advance() throws IOException { // that there are regular checkpoints and that state does not become too large. BackOff backoff = backoffFactory.backoff(); while (true) { + if (context.workIsFailed()) { + throw new WorkItemCancelledException(context.getWorkItem().getShardingKey()); + } if (elemsRead >= maxElems || Instant.now().isAfter(endTime) - || context.isSinkFullHintSet() - || context.workIsFailed()) { + || context.isSinkFullHintSet()) { + context.finishKey(); return false; } try { @@ -857,6 +861,7 @@ public boolean advance() throws IOException { } long nextBackoff = backoff.nextBackOffMillis(); if (nextBackoff == BackOff.STOP) { + context.finishKey(); return false; } Uninterruptibles.sleepUninterruptibly(nextBackoff, TimeUnit.MILLISECONDS); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java index 8dc681fc640c..b4f3a22a7f52 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java @@ -74,7 +74,7 @@ public final void executeWork( SideInputStateFetcher sideInputStateFetcher, Windmill.WorkItemCommitRequest.Builder outputBuilder) throws Exception { - context().start(key, work, stateReader, sideInputStateFetcher, outputBuilder); + context().start(key, work, stateReader, sideInputStateFetcher, outputBuilder, workExecutor()); workExecutor().execute(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/FlattenOperation.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/FlattenOperation.java index 4847e9f2ea9c..af1b2b9c48bd 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/FlattenOperation.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/FlattenOperation.java @@ -43,6 +43,9 @@ public void process(Object elem) throws Exception { } } + @Override + public void finishKey() throws Exception {} + @Override public boolean supportsRestart() { return true; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java index 58b95f286d55..3c33e1904069 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java @@ -112,6 +112,13 @@ public void execute() throws Exception { // TODO: support for success / failure ports? } + @Override + public void finishKey() throws Exception { + for (Operation op : operations) { + op.finishKey(); + } + } + @Override public NativeReader.Progress getWorkerProgress() throws Exception { return getReadOperation().getProgress(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/Operation.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/Operation.java index b7b4e255cfa5..b630da33cfad 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/Operation.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/Operation.java @@ -137,6 +137,9 @@ public void finish() throws Exception { } } + /** Called when all elements for a specific key have been processed. */ + public abstract void finishKey() throws Exception; + /** Aborts this Operation's execution. */ public void abort() throws Exception { synchronized (initializationStateLock) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperation.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperation.java index 27b6e9d1fb35..68f5fbe688da 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperation.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperation.java @@ -45,13 +45,18 @@ public void process(Object elem) throws Exception { } } + // Batch mode does not use this method and instead relies on BatchModeUngroupingParDoFn + // to process timers per key. @Override - public void finish() throws Exception { + public void finishKey() throws Exception { try (Closeable scope = context.enterProcessTimers()) { checkStarted(); fn.processTimers(); } + } + @Override + public void finish() throws Exception { try (Closeable scope = context.enterFinish()) { fn.finishBundle(); super.finish(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ReadOperation.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ReadOperation.java index d6b020483d4c..fabc8d6af25b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ReadOperation.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ReadOperation.java @@ -271,6 +271,9 @@ public void finish() throws Exception { } } + @Override + public void finishKey() throws Exception {} + @Override public void abort() throws Exception { if (readerIterator != null) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WorkExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WorkExecutor.java index b7170c80ced9..1083fdbb9c42 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WorkExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WorkExecutor.java @@ -34,6 +34,9 @@ public interface WorkExecutor extends AutoCloseable { /** Executes the task. */ public abstract void execute() throws Exception; + /** Called when all elements for a specific key have been processed. */ + void finishKey() throws Exception; + /** * Returns the worker's current progress. * diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WriteOperation.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WriteOperation.java index 673140d58d89..d28e7f3e5d3d 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WriteOperation.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/WriteOperation.java @@ -105,6 +105,9 @@ public void finish() throws Exception { } } + @Override + public void finishKey() throws Exception {} + @Override public void abort() throws Exception { if (writer == null) { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorTest.java index c519efd4172c..396c8db87e6b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorTest.java @@ -104,6 +104,9 @@ public void abort() throws Exception { aborted = true; super.abort(); } + + @Override + public void finishKey() throws Exception {} } // A mock ReadOperation fed to a MapTaskExecutor in test. @@ -312,6 +315,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(1L); } } + + @Override + public void finishKey() throws Exception {} }, new Operation(new OutputReceiver[] {}, context2) { @Override @@ -321,6 +327,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(2L); } } + + @Override + public void finishKey() throws Exception {} }, new Operation(new OutputReceiver[] {}, context3) { @Override @@ -330,6 +339,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(3L); } } + + @Override + public void finishKey() throws Exception {} }); try (IntrinsicMapTaskExecutor executor = diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java index d8a1d1b90d47..ff82a1ab5c4c 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java @@ -3657,8 +3657,8 @@ public void testActiveWorkFailure() throws Exception { server.waitForAndGetCommitsWithTimeout(1, Duration.standardSeconds(5)); assertEquals(1, commits.size()); - assertEquals(0, BlockingFn.teardownCounter.get()); - assertEquals(1, BlockingFn.setupCounter.get()); + assertEquals(1, BlockingFn.teardownCounter.get()); + assertEquals(2, BlockingFn.setupCounter.get()); worker.stop(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java index a1c7609e5af1..216ca5386675 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java @@ -62,6 +62,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.config.FakeGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfig; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; +import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; @@ -99,6 +100,7 @@ public class StreamingModeExecutionContextTest { @Rule public transient Timeout globalTimeout = Timeout.seconds(600); @Mock private SideInputStateFetcher sideInputStateFetcher; @Mock private WindmillStateReader stateReader; + @Mock private WorkExecutor workExecutor; private static final String COMPUTATION_ID = "computationId"; @@ -152,7 +154,7 @@ COMPUTATION_ID, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.cla } @Test - public void testTimerInternalsSetTimer() { + public void testTimerInternalsSetTimer() throws Exception { Windmill.WorkItemCommitRequest.Builder outputBuilder = Windmill.WorkItemCommitRequest.newBuilder(); NameContext nameContext = NameContextsForTests.nameContextForTest(); @@ -168,7 +170,8 @@ public void testTimerInternalsSetTimer() { Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), stateReader, sideInputStateFetcher, - outputBuilder); + outputBuilder, + workExecutor); TimerInternals timerInternals = stepContext.timerInternals(); @@ -179,6 +182,7 @@ public void testTimerInternalsSetTimer() { new Instant(5000), TimeDomain.EVENT_TIME, CausedByDrain.NORMAL)); + executionContext.finishKey(); executionContext.flushState(); Windmill.Timer timer = outputBuilder.buildPartial().getOutputTimers(0); @@ -218,7 +222,8 @@ public void testTimerInternalsProcessingTimeSkew() { Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), stateReader, sideInputStateFetcher, - outputBuilder); + outputBuilder, + workExecutor); TimerInternals timerInternals = stepContext.timerInternals(); assertTrue(timerTimestamp.isBefore(timerInternals.currentProcessingTime())); } @@ -427,7 +432,8 @@ public void testStateTagEncodingBasedOnConfig() { Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), stateReader, sideInputStateFetcher, - outputBuilder); + outputBuilder, + workExecutor); assertEquals(expectedEncoding, executionContext.getWindmillTagEncoding().getClass()); } } @@ -449,9 +455,11 @@ public void testSetBacklogBytes() { Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), stateReader, sideInputStateFetcher, - outputBuilder); + outputBuilder, + workExecutor); stepContext.setBacklogBytes(1234.0); + executionContext.finishKey(); executionContext.flushState(); assertEquals(1234, outputBuilder.getSourceBacklogBytes()); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java index 61e2f4250d06..539c38eeb1da 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java @@ -19,6 +19,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; import java.util.ArrayList; @@ -40,8 +45,8 @@ public class WindmillReaderIteratorBaseTest { private static class TestWindmillReaderIterator extends WindmillReaderIteratorBase { protected TestWindmillReaderIterator( - Windmill.WorkItem work, ValueProvider skipUndecodableElements) { - super(work, skipUndecodableElements); + StreamingModeExecutionContext context, ValueProvider skipUndecodableElements) { + super(context, skipUndecodableElements); } @Override @@ -81,6 +86,51 @@ public void testSkipErrors() throws IOException { testForMessageBundleCounts(true, 0, 0, 1, 3, 0, 1, 0, 0, 0, 0); } + @Test + public void testWorkItemCancelledException() throws IOException { + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.workIsFailed()).thenReturn(true); + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(0L).build(); + when(mockContext.getWorkItem()).thenReturn(workItem); + + try (TestWindmillReaderIterator iter = + new TestWindmillReaderIterator(mockContext, ValueProvider.StaticValueProvider.of(false))) { + iter.start(); + fail("Expected WorkItemCancelledException"); + } catch (WorkItemCancelledException e) { + // Expected + } + } + + @Test + public void testFinishKeyCalled() throws Exception { + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.workIsFailed()).thenReturn(false); + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.EMPTY) + .setWorkToken(0L) + .addMessageBundles( + Windmill.InputMessageBundle.newBuilder() + .setSourceComputationId("foo") + .addMessages( + Windmill.Message.newBuilder() + .setTimestamp(0) + .setData(ByteString.EMPTY) + .build()) + .build()) + .build(); + when(mockContext.getWorkItem()).thenReturn(workItem); + + try (TestWindmillReaderIterator iter = + new TestWindmillReaderIterator(mockContext, ValueProvider.StaticValueProvider.of(false))) { + assertTrue(iter.start()); + assertFalse(iter.advance()); // This should trigger finishKey + verify(mockContext).finishKey(); + } + } + private void testForMessageBundleCounts(int... messageBundleCounts) throws IOException { testForMessageBundleCounts(false, messageBundleCounts); } @@ -111,9 +161,13 @@ private void testForMessageBundleCounts(boolean skipErrors, int... messageBundle .setWorkToken(0L) .addAllMessageBundles(bundles) .build(); + + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.getWorkItem()).thenReturn(workItem); + try (TestWindmillReaderIterator iter = new TestWindmillReaderIterator( - workItem, ValueProvider.StaticValueProvider.of(skipErrors))) { + mockContext, ValueProvider.StaticValueProvider.of(skipErrors))) { List actual = ReaderTestUtils.windowedValuesToValues( ReaderUtils.readRemainingFromIterator(iter, false)); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java index ce8ad32f71aa..d5cf2948d928 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java @@ -97,6 +97,7 @@ import org.apache.beam.runners.dataflow.worker.testing.TestCountingSource; import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader; import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader.NativeReaderIterator; +import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; @@ -643,7 +644,8 @@ public void testReadUnboundedReader() throws Exception { Watermarks.builder().setInputDataWatermark(new Instant(0)).build()), mock(WindmillStateReader.class), mock(SideInputStateFetcher.class), - Windmill.WorkItemCommitRequest.newBuilder()); + Windmill.WorkItemCommitRequest.newBuilder(), + mock(WorkExecutor.class)); @SuppressWarnings({"unchecked", "rawtypes"}) NativeReader>>> reader = @@ -1023,7 +1025,8 @@ public void testFailedWorkItemsAbort() throws Exception { dummyWork, mock(WindmillStateReader.class), mock(SideInputStateFetcher.class), - Windmill.WorkItemCommitRequest.newBuilder()); + Windmill.WorkItemCommitRequest.newBuilder(), + mock(WorkExecutor.class)); @SuppressWarnings({"unchecked", "rawtypes"}) NativeReader>>> reader = @@ -1038,14 +1041,19 @@ public void testFailedWorkItemsAbort() throws Exception { NativeReaderIterator>>> readerIterator = reader.iterator(); int numReads = 0; - while ((numReads == 0) ? readerIterator.start() : readerIterator.advance()) { - WindowedValue>> value = readerIterator.getCurrent(); - assertEquals(KV.of(0, numReads), value.getValue().getValue()); - numReads++; - // Fail the work item after reading two elements. - if (numReads == 2) { - dummyWork.setFailed(); + try { + while ((numReads == 0) ? readerIterator.start() : readerIterator.advance()) { + WindowedValue>> value = readerIterator.getCurrent(); + assertEquals(KV.of(0, numReads), value.getValue().getValue()); + numReads++; + // Fail the work item after reading two elements. + if (numReads == 2) { + dummyWork.setFailed(); + } } + fail("Expected WorkItemCancelledException"); + } catch (WorkItemCancelledException e) { + // Expected } assertThat(numReads, equalTo(2)); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ExecutorTestUtils.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ExecutorTestUtils.java index 2c35f4bf99db..d5e3b9c87139 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ExecutorTestUtils.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ExecutorTestUtils.java @@ -59,6 +59,9 @@ private static OutputReceiver[] createOutputReceivers(int numOutputs, CounterSet } return receivers; } + + @Override + public void finishKey() throws Exception {} } /** A {@code Reader} that yields a specified set of values. */ diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutorTest.java index 188466a50572..5d8f8eebb6f6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutorTest.java @@ -100,6 +100,9 @@ public void abort() throws Exception { aborted = true; super.abort(); } + + @Override + public void finishKey() throws Exception {} } // A mock ReadOperation fed to a MapTaskExecutor in test. @@ -309,6 +312,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(1L); } } + + @Override + public void finishKey() throws Exception {} }, new Operation(new OutputReceiver[] {}, context2) { @Override @@ -318,6 +324,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(2L); } } + + @Override + public void finishKey() throws Exception {} }, new Operation(new OutputReceiver[] {}, context3) { @Override @@ -327,6 +336,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(3L); } } + + @Override + public void finishKey() throws Exception {} }); assertEquals(TimeUnit.MINUTES.toMillis(10), stateTracker.getNextBundleLullDurationReportMs()); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperationTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperationTest.java index ba327f92cc44..5d058b1968cb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperationTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoOperationTest.java @@ -104,6 +104,7 @@ public void testRunParDoOperation() throws Exception { parDoOperation.process(""); parDoOperation.process("bob"); + parDoOperation.finishKey(); parDoOperation.finish(); parDoOperation.abort(); @@ -147,6 +148,7 @@ public void testParDoOperationContext() throws Exception { operation.start(); operation.process("hello"); + operation.finishKey(); operation.finish(); InOrder inOrder = From a72f94b3c07a9f0bf84f45724866f4e53864bcae Mon Sep 17 00:00:00 2001 From: scwhittle Date: Wed, 27 May 2026 13:50:33 +0200 Subject: [PATCH 238/490] [Cloud Spanner Change Streams] Fix inverted evaluation of cancelQueryOnHeartbeat (#38695) This meant that low latency mode for heartbeats was enabled by default and disabled in low latency mode instead of the desired opposite. --- .../spanner/changestreams/action/HeartbeatRecordAction.java | 2 +- .../changestreams/action/HeartbeatRecordActionTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordAction.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordAction.java index 1b66a548b3d2..773f54a15d24 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordAction.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordAction.java @@ -104,6 +104,6 @@ public Optional run( return Optional.empty(); } // no new data, finish reading data - return cancelQueryOnHeartbeat ? Optional.empty() : Optional.of(ProcessContinuation.resume()); + return cancelQueryOnHeartbeat ? Optional.of(ProcessContinuation.resume()) : Optional.empty(); } } diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordActionTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordActionTest.java index adfc4ea35d48..48fd7c30a1a8 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordActionTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/HeartbeatRecordActionTest.java @@ -232,7 +232,7 @@ public void testEndTimestampNotReachedOnCancellingAction() { watermarkEstimator, endTimestamp); - assertEquals(Optional.empty(), maybeContinuation); + assertEquals(Optional.of(ProcessContinuation.resume()), maybeContinuation); verify(watermarkEstimator).setWatermark(new Instant(timestamp.toSqlTimestamp().getTime())); } @@ -254,7 +254,7 @@ public void testEndTimestampNotReachedOnAction() { watermarkEstimator, endTimestamp); - assertEquals(Optional.of(ProcessContinuation.resume()), maybeContinuation); + assertEquals(Optional.empty(), maybeContinuation); verify(watermarkEstimator).setWatermark(new Instant(timestamp.toSqlTimestamp().getTime())); } } From bfa7bc506ab423c41b2a16c03cab462cc5d3055a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 08:34:30 -0400 Subject: [PATCH 239/490] Bump cloud.google.com/go/bigtable from 1.47.0 to 1.48.0 in /sdks (#38703) Bumps [cloud.google.com/go/bigtable](https://github.com/googleapis/google-cloud-go) from 1.47.0 to 1.48.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/documentai/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.47.0...pubsub/v1.48.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/bigtable dependency-version: 1.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index e72db8be4996..2ab52f0065b4 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -26,7 +26,7 @@ toolchain go1.26.2 require ( cloud.google.com/go/bigquery v1.77.0 - cloud.google.com/go/bigtable v1.47.0 + cloud.google.com/go/bigtable v1.48.0 cloud.google.com/go/datastore v1.24.0 cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 diff --git a/sdks/go.sum b/sdks/go.sum index f8bfd7d24a0c..5ca9632a03df 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -46,8 +46,8 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.77.0 h1:L5AW3jhzEKpFVg4i0mVHxKpxogrqT7dczWBSr4m9MKU= cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= -cloud.google.com/go/bigtable v1.47.0 h1:NGLgDSr/i79BTGCjxH/maPKxyvl5q8/SsBsyLK52kdI= -cloud.google.com/go/bigtable v1.47.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= +cloud.google.com/go/bigtable v1.48.0 h1:+K6difi14hvfuh+19tvWVkvD6zYjN5mc3rJ1+q8ETyw= +cloud.google.com/go/bigtable v1.48.0/go.mod h1:6TjVhBmzk7N01MZwjxn/YsSnlaw96AYzsFDYhfyBqDs= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= From b8053edbde646b7a577e72fae22223529fa2f6bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 08:35:13 -0400 Subject: [PATCH 240/490] Bump google.golang.org/api from 0.280.0 to 0.281.0 in /sdks (#38704) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.280.0 to 0.281.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.280.0...v0.281.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.281.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 2ab52f0065b4..865fe1f59fb8 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 - google.golang.org/api v0.280.0 + google.golang.org/api v0.281.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 diff --git a/sdks/go.sum b/sdks/go.sum index 5ca9632a03df..cd4e6010f6ad 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1324,8 +1324,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk= -google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU= +google.golang.org/api v0.281.0 h1:sohYgszGGg3zC0P6ncdV6J9IOBtN9LVfFKz8C9tTtJU= +google.golang.org/api v0.281.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From d077395abc82c2ea29531b1fa31a24122ed4827d Mon Sep 17 00:00:00 2001 From: liferoad Date: Wed, 27 May 2026 10:11:33 -0400 Subject: [PATCH 241/490] Remove redundant HTTP->HTTPS redirect in .htaccess (#38522) The HTTP->HTTPS redirect is already handled by Apache TLP infrastructure, so the redirect rules in website/www/site/static/.htaccess are redundant. Fixes #37432 --- website/www/site/static/.htaccess | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/website/www/site/static/.htaccess b/website/www/site/static/.htaccess index 540e3af223ef..6e82344458ac 100644 --- a/website/www/site/static/.htaccess +++ b/website/www/site/static/.htaccess @@ -1,19 +1,5 @@ RewriteEngine On -# This is a 301 (permanent) redirect from HTTP to HTTPS. - -# The next rule applies conditionally: -# * the host is "beam.apache.org", -# * the host comparison is case insensitive (NC), -# * HTTPS is not used. -RewriteCond %{HTTP_HOST} ^beam\.apache\.org [NC] -RewriteCond %{HTTPS} !on - -# Rewrite the URL as follows: -# * Redirect (R) permanently (301) to https://beam.apache.org/, -# * Stop processing more rules (L). -RewriteRule ^(.*)$ https://beam.apache.org/$1 [L,R=301] - # Javadocs / pydocs are available only on the published website, published from # https://github.com/apache/beam-site/tree/release-docs # They were previously hosted within this repository, and published at the URL From d1658402e0e60d8bd0c3a575b568a5283d8f08a4 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 27 May 2026 11:25:05 -0400 Subject: [PATCH 242/490] Refactor _SharedCache to handle context vs non-context ownership (#38620) * Refactor logging in subprocess_server.py for improved observability * Introduce context-aware ownership in SubprocessServer cache Refactors `_SharedCache` to support an `is_context` flag for registered owners. This prevents independent non-context subprocesses (e.g., prism runner and expansion service) from sharing ownership of each other's keys, while allowing context wrappers to properly track all active subprocesses. * Address comments. --- .../apache_beam/utils/subprocess_server.py | 40 ++++++++----- .../utils/subprocess_server_test.py | 57 +++++++++++++++++-- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py index 0b09b364362f..6e78ce88e0d7 100644 --- a/sdks/python/apache_beam/utils/subprocess_server.py +++ b/sdks/python/apache_beam/utils/subprocess_server.py @@ -72,7 +72,7 @@ class _SharedCache: def __init__(self, constructor, destructor): self._constructor = constructor self._destructor = destructor - self._live_owners = set() + self._live_owners = {} self._cache = {} self._lock = threading.Lock() self._counter = 0 @@ -82,10 +82,10 @@ def _next_id(self): self._counter += 1 return self._counter - def register(self): + def register(self, is_context=False): with self._lock: owner = self._next_id() - self._live_owners.add(owner) + self._live_owners[owner] = is_context return owner def purge(self, owner): @@ -97,7 +97,7 @@ def purge(self, owner): "shutdown, the subprocess was already cleaned up earlier.", owner) return - self._live_owners.remove(owner) + del self._live_owners[owner] for key, entry in list(self._cache.items()): if owner in entry.owners: entry.owners.remove(owner) @@ -108,14 +108,23 @@ def purge(self, owner): for value in to_delete: self._destructor(value) - def get(self, *key): - if not self._live_owners: - raise RuntimeError("At least one owner must be registered.") + def get(self, *key, owner=None): with self._lock: + if not self._live_owners: + raise RuntimeError("At least one owner must be registered.") + if owner is not None and owner not in self._live_owners: + raise RuntimeError("The requesting owner must be registered.") + if key not in self._cache: self._cache[key] = _SharedCacheEntry(self._constructor(*key), set()) - for owner in self._live_owners: + if owner is not None: self._cache[key].owners.add(owner) + for live_owner, is_context in self._live_owners.items(): + if is_context: + self._cache[key].owners.add(live_owner) + else: + for live_owner in self._live_owners: + self._cache[key].owners.add(live_owner) return self._cache[key].obj def force_remove(self, *key): @@ -180,7 +189,7 @@ def cache_subprocesses(cls): These subprocesses may be shared with other contexts as well. """ try: - unique_id = cls._cache.register() + unique_id = cls._cache.register(is_context=True) yield finally: cls._cache.purge(unique_id) @@ -214,7 +223,7 @@ def start(self): channel_ready = grpc.channel_ready_future(self._grpc_channel) while True: if process is not None and process.poll() is not None: - _LOGGER.error("Started job service with %s", process.args) + _LOGGER.error("Failed to start job service with %s", process.args) raise RuntimeError( 'Service failed to start up with error %s' % process.poll()) try: @@ -235,15 +244,16 @@ def start(self): def start_process(self): if self._owner_id is not None: self._cache.purge(self._owner_id) - self._owner_id = self._cache.register() - return self._cache.get(tuple(self._cmd), self._port, self._logger) + self._owner_id = self._cache.register(is_context=False) + return self._cache.get( + tuple(self._cmd), self._port, self._logger, owner=self._owner_id) def _really_start_process(cmd, port, logger): if not port: port, = pick_port(None) cmd = [arg.replace('{{PORT}}', str(port)) for arg in cmd] # pylint: disable=not-an-iterable endpoint = 'localhost:%s' % port - _LOGGER.info("Starting service with %s", str(cmd).replace("',", "'")) + _LOGGER.warning("Really starting service at %s with cmd: %s", endpoint, cmd) process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -295,9 +305,11 @@ def stop_force(self): self._grpc_channel = None def _really_stop_process(process_and_endpoint): - process, _ = process_and_endpoint # pylint: disable=unpacking-non-sequence + process, endpoint = process_and_endpoint # pylint: disable=unpacking-non-sequence if not process: return + _LOGGER.warning( + "Really destroying service at %s with cmd: %s", endpoint, process.args) for _ in range(5): if process.poll() is not None: break diff --git a/sdks/python/apache_beam/utils/subprocess_server_test.py b/sdks/python/apache_beam/utils/subprocess_server_test.py index a44b89b17e37..a008ae05c52d 100644 --- a/sdks/python/apache_beam/utils/subprocess_server_test.py +++ b/sdks/python/apache_beam/utils/subprocess_server_test.py @@ -402,16 +402,16 @@ def mock_unregister(cb): self.assertEqual(len(registered_callbacks), 1) def test_concurrent_purge_race_condition(self): - # Concurrent threads attempting to check memebership and call purge for the same owner. - # Here we explicitly define a synchronized set to mimic the behavior of _live_owners. - # This set will block two threads on __contains__, allowing us to test the race condition. + # Concurrent threads attempting to check membership and call purge for the same owner. + # Here we explicitly define a synchronized dict to mimic the behavior of _live_owners. + # This dict will block two threads on __contains__, allowing us to test the race condition. cache = subprocess_server._SharedCache(lambda x: "obj", lambda x: None) owner = cache.register() barrier = threading.Barrier(2) exceptions = [] - class SynchronizedSet(set): + class SynchronizedDict(dict): def __contains__(self, item): res = super().__contains__(item) try: @@ -421,7 +421,7 @@ def __contains__(self, item): pass return res - cache._live_owners = SynchronizedSet(cache._live_owners) + cache._live_owners = SynchronizedDict(cache._live_owners) def purge_worker(): try: @@ -551,6 +551,53 @@ def __init__(self): # Clean up the other owner cache.purge(other_owner) + def test_non_context_owners_do_not_share_keys(self): + cache = subprocess_server._SharedCache(self.with_prefix, lambda x: None) + # owner1 is a non-context owner (e.g., prism) + owner1 = cache.register(is_context=False) + a = cache.get('a', owner=owner1) + + # owner2 is another non-context owner (e.g., short-lived expansion service) + owner2 = cache.register(is_context=False) + b = cache.get('b', owner=owner2) + + # Verify that owner1 does not own 'b' + self.assertNotIn(owner1, cache._cache[('b', )].owners) + + # Verify that owner2 does not own 'a' + self.assertNotIn(owner2, cache._cache[('a', )].owners) + + # Purging owner2 should immediately destroy/remove 'b' + cache.purge(owner2) + self.assertNotIn(('b', ), cache._cache) + + # 'a' is still alive because owner1 is still registered + self.assertIn(('a', ), cache._cache) + + # Purging owner1 should destroy/remove 'a' + cache.purge(owner1) + self.assertNotIn(('a', ), cache._cache) + + def test_context_owner_owns_all_keys(self): + cache = subprocess_server._SharedCache(self.with_prefix, lambda x: None) + # owner1 is a non-context owner (e.g., prism) + owner1 = cache.register(is_context=False) + + # owner2 is a context owner (e.g., cache_subprocesses) + owner2 = cache.register(is_context=True) + + # owner3 is another non-context owner (e.g., short-lived service) + owner3 = cache.register(is_context=False) + + # owner3 requests 'b' + b = cache.get('b', owner=owner3) + + # owner2 (context) should own 'b' + self.assertIn(owner2, cache._cache[('b', )].owners) + + # owner1 (non-context) should NOT own 'b' + self.assertNotIn(owner1, cache._cache[('b', )].owners) + if __name__ == '__main__': unittest.main() From bc4df6b1357facd067d3260866f6406e9d0fa9f6 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 27 May 2026 20:30:24 -0400 Subject: [PATCH 243/490] Enforce binary-only constraints during early requirements cache attempts (#38688) * Add fallback platform for stager. * Force wheel if we have other platform to try * More comments. * Trigger PostCommit Python * Refactor code. * Refactor more. * pip download dep in requirement one by one. --- .../trigger_files/beam_PostCommit_Python.json | 2 +- .../apache_beam/runners/portability/stager.py | 151 +++++++++++------- 2 files changed, 95 insertions(+), 58 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 91226bd08ee3..931ae76b69d5 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "38069", - "modification": 41 + "modification": 42 } diff --git a/sdks/python/apache_beam/runners/portability/stager.py b/sdks/python/apache_beam/runners/portability/stager.py index 136c320da009..542a44edd702 100644 --- a/sdks/python/apache_beam/runners/portability/stager.py +++ b/sdks/python/apache_beam/runners/portability/stager.py @@ -88,6 +88,16 @@ # One of the choices for user to use for requirements cache during staging SKIP_REQUIREMENTS_CACHE = 'skip' +# Ordered list of manylinux tags from newest (strictest) to oldest (most compatible) +# paired with the minimum pip version required to support the tag. +# See https://github.com/pypa/manylinux. +_MANYLINUX_PLATFORMS = [ + ('manylinux_2_28_x86_64', '20.3'), + ('manylinux2014_x86_64', '19.3'), # equivalent to manylinux_2_17 + ('manylinux2010_x86_64', + '0.0'), # equivalent to manylinux_2_12, the fallback if pip is too old +] + _LOGGER = logging.getLogger(__name__) @@ -717,30 +727,6 @@ def _extract_local_packages(requirements_file): else: return [], requirements_file - @staticmethod - def _get_platform_for_default_sdk_container(): - """ - Get the platform for apache beam SDK container based on Pip version. - - Note: pip is still expected to download compatible wheel of a package - with platform tag manylinux1 if the package on PyPI doesn't - have (manylinux2014) or (manylinux2010) wheels. - Reference: https://www.python.org/dev/peps/pep-0599/#id21 - """ - - # TODO(anandinguva): When https://github.com/pypa/pip/issues/10760 is - # addressed, download wheel based on glibc version in Beam's Python - # Base image - pip_version = distribution('pip').version - # See more information about manylinux at - # https://github.com/pypa/manylinux - if version.parse(pip_version) >= version.parse('20.3'): - return 'manylinux_2_28_x86_64' - elif version.parse(pip_version) >= version.parse('19.3'): - return 'manylinux2014_x86_64' - else: - return 'manylinux2010_x86_64' - @staticmethod @retry.with_exponential_backoff( num_retries=4, retry_filter=retry_on_non_zero_exit) @@ -764,40 +750,44 @@ def _populate_requirements_cache( # requirements file. download_dir = tempfile.mkdtemp(dir=temp_directory) - cmd_args = [ - Stager._get_python_executable(), - '-m', - 'pip', - 'download', - '--dest', - download_dir, - '--find-links', - cache_dir, - '-r', - tmp_requirements_filepath, - '--exists-action', - 'i', - '--no-deps' - ] + # Read packages from the requirements file + requirements = [] + with open(tmp_requirements_filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + requirements.append(line) + + for req in requirements: + cmd_args = [ + Stager._get_python_executable(), + '-m', + 'pip', + 'download', + '--find-links', + cache_dir, + req, + '--exists-action', + 'i', + '--no-deps' + ] + + if populate_cache_with_sdists: + attempt_download_dir = tempfile.mkdtemp(dir=temp_directory) + cmd_args.extend( + ['--dest', attempt_download_dir, '--no-binary', ':all:']) + _LOGGER.info('Executing command: %s', cmd_args) + processes.check_output(cmd_args, stderr=processes.STDOUT) + else: + attempt_download_dir = Stager._download_pypi_packages( + cmd_args, temp_directory) - if populate_cache_with_sdists: - cmd_args.extend(['--no-binary', ':all:']) - else: - language_implementation_tag = 'cp' - abi_suffix = 'm' if sys.version_info < (3, 8) else '' - abi_tag = 'cp%d%d%s' % ( - sys.version_info[0], sys.version_info[1], abi_suffix) - platform_tag = Stager._get_platform_for_default_sdk_container() - cmd_args.extend([ - '--implementation', - language_implementation_tag, - '--abi', - abi_tag, - '--platform', - platform_tag - ]) - _LOGGER.info('Executing command: %s', cmd_args) - processes.check_output(cmd_args, stderr=processes.STDOUT) + # Move downloaded packages to our common download directory + for pkg_file in os.listdir(attempt_download_dir): + src_path = os.path.join(attempt_download_dir, pkg_file) + dest_path = os.path.join(download_dir, pkg_file) + if not os.path.exists(dest_path): + shutil.move(src_path, dest_path) # Get list of downloaded packages and copy them to the cache downloaded_packages = set() @@ -811,6 +801,53 @@ def _populate_requirements_cache( return downloaded_packages + @staticmethod + def _download_pypi_packages(cmd_args, temp_directory): + language_implementation_tag = 'cp' + abi_suffix = 'm' if sys.version_info < (3, 8) else '' + abi_tag = 'cp%d%d%s' % ( + sys.version_info[0], sys.version_info[1], abi_suffix) + pip_version = distribution('pip').version + platforms = [ + platform for platform, min_pip_version in _MANYLINUX_PLATFORMS + if version.parse(pip_version) >= version.parse(min_pip_version) + ] + + last_exception = None + for idx, platform in enumerate(platforms): + attempt_download_dir = tempfile.mkdtemp(dir=temp_directory) + attempt_cmd_args = cmd_args + [ + '--dest', + attempt_download_dir, + '--implementation', + language_implementation_tag, + '--abi', + abi_tag, + '--platform', + platform + ] + # Force binary wheel only if we have more platform fallbacks to try. + # For the last platform, we omit this flag so it can natively fall back + # to downloading a source distribution (sdist) if no matching wheel is found. + if idx < len(platforms) - 1: + attempt_cmd_args.extend(['--only-binary', ':all:']) + + _LOGGER.info('Executing command: %s', attempt_cmd_args) + try: + processes.check_output(attempt_cmd_args, stderr=processes.STDOUT) + last_exception = None + return attempt_download_dir + except Exception as e: + _LOGGER.warning( + 'Pip download failed with platform %s, trying fallback: %s', + platform, + e) + shutil.rmtree(attempt_download_dir) + last_exception = e + + if last_exception: + raise last_exception + @staticmethod def _build_setup_package( setup_file: str, From 5c565ee9f07b74d2eb45c75aea898c3eb5d60960 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 08:16:15 -0400 Subject: [PATCH 244/490] Bump docker/setup-qemu-action from 4.0.0 to 4.1.0 (#38718) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/ce360397dd3f832beb865e1373c09c0e9f86d70a...06116385d9baf250c9f4dcb4858b16962ea869c3) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 4db227385c77..532d946d4094 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -245,7 +245,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.11' - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 if: ${{matrix.os_python.arch == 'aarch64'}} name: Set up QEMU - name: Install cibuildwheel From e2d9f32db193a7f552848e689e1ca1f17aca990c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 08:16:45 -0400 Subject: [PATCH 245/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#38719) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.101.0 to 1.102.0. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.101.0...service/s3/v1.102.0) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.102.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 865fe1f59fb8..bdfb31890ff2 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -36,7 +36,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 - github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 github.com/aws/smithy-go v1.25.1 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 @@ -154,7 +154,7 @@ require ( github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index cd4e6010f6ad..d4e8762ef4b4 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -240,8 +240,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZL github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 h1:tX68nPDCoX0s2ksM7CipWP0QFw2hGDWwUdxI6+eT9ZU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= @@ -253,8 +253,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mw github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 h1:gfPQ6do5PZTCc5n/vZUHz/G8McrNrfERGSO+iHvVbCA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0/go.mod h1:wO6U9egJtCtsZEHG2AAcFf1kUWDRrH0Iif6K3bVmmdE= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= From 8d8ee69d985eb470da1cd0ee402137c14ce32de0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 08:17:12 -0400 Subject: [PATCH 246/490] Bump google.golang.org/api from 0.281.0 to 0.282.0 in /sdks (#38720) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.281.0 to 0.282.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.281.0...v0.282.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.282.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index bdfb31890ff2..17312e7929c2 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 - google.golang.org/api v0.281.0 + google.golang.org/api v0.282.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 diff --git a/sdks/go.sum b/sdks/go.sum index d4e8762ef4b4..c1bc31532b6a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1324,8 +1324,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.281.0 h1:sohYgszGGg3zC0P6ncdV6J9IOBtN9LVfFKz8C9tTtJU= -google.golang.org/api v0.281.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= +google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= +google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From dddd0f38c1d193071046b5edee71f761d28a0767 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 09:13:14 -0400 Subject: [PATCH 247/490] Bump github.com/aws/smithy-go from 1.25.1 to 1.26.0 in /sdks (#38721) Bumps [github.com/aws/smithy-go](https://github.com/aws/smithy-go) from 1.25.1 to 1.26.0. - [Release notes](https://github.com/aws/smithy-go/releases) - [Changelog](https://github.com/aws/smithy-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/smithy-go/compare/v1.25.1...v1.26.0) --- updated-dependencies: - dependency-name: github.com/aws/smithy-go dependency-version: 1.26.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 17312e7929c2..e71cd22cc3d6 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -37,7 +37,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 - github.com/aws/smithy-go v1.25.1 + github.com/aws/smithy-go v1.26.0 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 diff --git a/sdks/go.sum b/sdks/go.sum index c1bc31532b6a..9d98310f2328 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -274,8 +274,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOIt github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= -github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= +github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= From caca5762bad89689976a60586fe1d3c30b7d0edb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 09:13:42 -0400 Subject: [PATCH 248/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38722) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.19 to 1.22.20. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.19...feature/s3/manager/v1.22.20) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.20 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index e71cd22cc3d6..d38d85ccc6a3 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 github.com/aws/smithy-go v1.26.0 github.com/docker/go-connections v0.7.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 9d98310f2328..91984bb2b8a4 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8Tc github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19 h1:VH0xfFwHfPYhu+EcxyCcw3VTZskpbA+/s0pTXwhSsL8= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.19/go.mod h1:S/XkAXcnCpzwsjC9EU0BakuvreXfSTUADHb7rC7jvaQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 h1:7nIPtCHSeEKE7RC32DxsI6UyDoXeHefilHwcJz1F+nk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20/go.mod h1:lKf7JyyKRi6R8Y0cSrPuTLERZRD+4KeLbPZeSAciuik= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= From 4f0583cffabf60604e9bed2678d851c42f152cba Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 28 May 2026 15:47:20 +0200 Subject: [PATCH 249/490] Implemented MLTransform generate vocab Dataflow benchmark (#38215) * Implemented MLTransform generate vocab Dataflow benchmark * applied anomaly transform test for reshuffled ZScore order * remove redundant money nanos fallback * resolved comments --- ...m_Inference_Python_Benchmarks_Dataflow.yml | 12 + ...aflow_MLTransform_Generate_Vocab_Batch.txt | 42 +++ .test-infra/tools/refresh_looker_metrics.py | 3 +- .../examples/ml_transform/README.md | 116 ++++++++ .../mltransform_generate_vocab.py | 265 ++++++++++++++++++ ...ltransform_generate_vocab_requirements.txt | 26 ++ .../mltransform_generate_vocab_test.py | 191 +++++++++++++ .../mltransform_generate_vocab_benchmark.py | 56 ++++ .../load_tests/dataflow_cost_benchmark.py | 28 +- .../www/site/content/en/performance/_index.md | 1 + .../en/performance/mltransformvocab/_index.md | 37 +++ website/www/site/data/performance.yaml | 16 ++ 12 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt create mode 100644 sdks/python/apache_beam/examples/ml_transform/README.md create mode 100644 sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab.py create mode 100644 sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt create mode 100644 sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_test.py create mode 100644 sdks/python/apache_beam/testing/benchmarks/inference/mltransform_generate_vocab_benchmark.py create mode 100644 website/www/site/content/en/performance/mltransformvocab/_index.md diff --git a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml index 61de2b54e730..36e962bc2bab 100644 --- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml @@ -94,6 +94,7 @@ jobs: ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_VLLM_Gemma_Batch.txt ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Batch.txt ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Stream.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt # The env variables are created and populated in the test-arguments-action as "_test_arguments_" - name: get current time run: echo "NOW_UTC=$(date '+%m%d%H%M%S' --utc)" >> $GITHUB_ENV @@ -214,3 +215,14 @@ jobs: -PpythonVersion=3.10 \ -PloadTest.requirementsTxtFile=apache_beam/ml/inference/table_row_inference_requirements.txt \ '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_10 }} --autoscaling_algorithm=THROUGHPUT_BASED --max_num_workers=20 --metrics_table=result_table_row_inference_stream --influx_measurement=result_table_row_inference_stream --mode=streaming --input_subscription=projects/apache-beam-testing/subscriptions/table_row_inference_benchmark --window_size_sec=60 --trigger_interval_sec=30 --timeout_ms=900000 --output_table=apache-beam-testing:beam_run_inference.result_table_row_inference_stream_outputs --job_name=benchmark-tests-table-row-inference-stream-${{env.NOW_UTC}}' + - name: run MLTransform Generate Vocab Batch + uses: ./.github/actions/gradle-command-self-hosted-action + timeout-minutes: 180 + with: + gradle-command: :sdks:python:apache_beam:testing:load_tests:run + arguments: | + -PloadTest.mainClass=apache_beam.testing.benchmarks.inference.mltransform_generate_vocab_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --job_name=benchmark-tests-mltransform-generate-vocab-batch-${{env.NOW_UTC}}' diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt new file mode 100644 index 000000000000..24723bf8f9ce --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt @@ -0,0 +1,42 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +--project=apache-beam-testing +--region=us-central1 +--runner=DataflowRunner +--temp_location=gs://temp-storage-for-perf-tests/loadtests +--staging_location=gs://temp-storage-for-perf-tests/loadtests +--machine_type=n1-standard-4 +--disk_size_gb=100 +--num_workers=8 +--max_num_workers=16 +--autoscaling_algorithm=THROUGHPUT_BASED +--worker_zone=us-central1-b +--sdk_location=container +--requirements_file=apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt +--input_options={} +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--metrics_table=mltransform_generate_vocab_batch +--influx_measurement=mltransform_generate_vocab_batch +--input_file=gs://apache-beam-ml/testing/inputs/sentences_50k.txt +--output_vocab=gs://temp-storage-for-perf-tests/mltransform/vocab_outputs/mltransform_generate_vocab_batch +--columns=text +--vocab_size=50000 +--min_frequency=1 +--lowercase=true +--input_expand_factor=1 + diff --git a/.test-infra/tools/refresh_looker_metrics.py b/.test-infra/tools/refresh_looker_metrics.py index afd8ffa6f861..35122d5afe0a 100644 --- a/.test-infra/tools/refresh_looker_metrics.py +++ b/.test-infra/tools/refresh_looker_metrics.py @@ -44,7 +44,8 @@ ("85", ["268", "269", "270", "271", "272"]), # PyTorch Sentiment Batch DistilBERT base uncased ("86", ["284", "285", "286", "287", "288"]), # VLLM Batch Gemma ("96", ["270", "304", "305", "353", "354"]), # Table Row Inference Sklearn Batch - ("106", ["355", "356", "357", "358", "359"]) # Table Row Inference Sklearn Streaming + ("106", ["355", "356", "357", "358", "359"]), # Table Row Inference Sklearn Streaming + ("107", ["360", "361", "362", "363", "364"]), # MLTransform Generate Vocab Batch ] def get_look(id: str) -> models.Look: diff --git a/sdks/python/apache_beam/examples/ml_transform/README.md b/sdks/python/apache_beam/examples/ml_transform/README.md new file mode 100644 index 000000000000..a47fba1ff0c8 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml_transform/README.md @@ -0,0 +1,116 @@ + + +# MLTransform Examples + +This directory contains Apache Beam examples for MLTransform pipelines. + +## MLTransform - Generate Vocab (Batch only) + +`mltransform_generate_vocab.py` builds a vocabulary artifact from batch input +rows using `MLTransform` + `ComputeAndApplyVocabulary`. + +### What it does + +1. Reads input rows from JSONL (`--input_file`) or BigQuery (`--input_table`). +2. Extracts specified columns (`--columns`). +3. Normalizes and combines text values (`trim`, optional lowercasing). +4. Runs `ComputeAndApplyVocabulary` with top-k and min-frequency constraints + using space-delimited token splitting. +5. Writes the vocabulary as one token per line. + +### Required arguments + +- `--output_vocab` +- `--columns` +- and one of: + - `--input_file` + - `--input_table` + +### Optional arguments + +- `--vocab_size` (default: `50000`) +- `--min_frequency` (default: `1`) +- `--lowercase` (default: `true`) +- `--input_expand_factor` (default: `1`, useful for perf/load testing) + +### Local batch example + +```sh +python -m apache_beam.examples.ml_transform.mltransform_generate_vocab \ + --input_file=/tmp/input.jsonl \ + --output_vocab=/tmp/vocab.txt \ + --columns=text,category \ + --vocab_size=5 \ + --min_frequency=1 \ + --lowercase=true \ + --input_expand_factor=1 \ + --runner=DirectRunner +``` + +### Input format + +JSONL input with object rows, for example: + +```json +{"id":"1","text":"Beam beam ML pipeline"} +{"id":"2","text":"Beam pipeline dataflow"} +{"id":"3","text":"ML transform beam"} +{"id":"4","text":"vocab vocab vocab test"} +{"id":"5","text":"rare_token_once"} +{"id":"6","text":""} +{"id":"7","text":null} +``` + +The integration tests in `mltransform_generate_vocab_test.py` generate this +sample data programmatically. + +### Output format + +One token per line: + +1. tokens follow the vocabulary order produced by `ComputeAndApplyVocabulary`. + +Example output: + +```txt +beam +ml +``` + +For this sample and config: + +```sh +--columns=text --min_frequency=2 --vocab_size=3 +``` + +the expected output is: + +```txt +beam +vocab +ml +``` + +### Additional test datasets + +Test data for happy path and null/empty/missing columns is generated inline in +`mltransform_generate_vocab_test.py`. + +### Performance testing pattern + +- Small local files: functional correctness and output-stability tests. +- Large GCS files (or moderate file + `--input_expand_factor`): throughput/cost + benchmarking on Dataflow. + diff --git a/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab.py b/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab.py new file mode 100644 index 000000000000..f0e4b38aa3f3 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab.py @@ -0,0 +1,265 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Batch-only vocabulary generation pipeline using MLTransform. + +This pipeline creates a vocabulary artifact from one or more input columns. + +Key properties: +- Batch only (no streaming path). +- Vocabulary generation via MLTransform ComputeAndApplyVocabulary. +- Output format: one token per line. +""" + +import argparse +import json +import logging +import tempfile +from typing import Any + +import apache_beam as beam +from apache_beam.io.filesystems import FileSystems +from apache_beam.ml.transforms.base import MLTransform +from apache_beam.ml.transforms.tft import ComputeAndApplyVocabulary +from apache_beam.options.pipeline_options import PipelineOptions + + +def parse_bool_flag(value: str) -> bool: + value_lc = value.strip().lower() + if value_lc in ('1', 'true', 't', 'yes', 'y'): + return True + if value_lc in ('0', 'false', 'f', 'no', 'n'): + return False + raise ValueError( + f'Invalid boolean value {value!r}. Expected true/false style value.') + + +def normalize_text(value: Any, lowercase: bool = True) -> str: + if value is None: + return '' + text = str(value).strip() + if lowercase: + text = text.lower() + return text + + +def _parse_json_line(line: str) -> dict[str, Any]: + try: + parsed = json.loads(line) + if isinstance(parsed, dict): + return parsed + except json.JSONDecodeError: + pass + # Treat plain-text or non-object rows as values for the default "text" column. + return {'text': line} + + +def _extract_column_values(row: dict[str, Any], + columns: list[str]) -> list[str]: + values: list[str] = [] + for col in columns: + if col not in row: + continue + val = row[col] + if val is None: + continue + if isinstance(val, list): + values.extend(str(item) for item in val if item is not None) + else: + values.append(str(val)) + return values + + +def _build_vocab_text( + row: dict[str, Any], columns: list[str], lowercase: bool) -> str: + values = _extract_column_values(row, columns) + normalized_values = [ + normalize_text(value, lowercase=lowercase) for value in values + ] + non_empty_values = [value for value in normalized_values if value] + return ' '.join(non_empty_values) + + +def _resolve_vocab_asset_path( + artifact_location: str, vocab_filename: str, column_name: str) -> str: + asset_name = f'{vocab_filename}_{column_name}' + pattern = ( + f'{artifact_location.rstrip("/")}' + f'/*/transform_fn/assets/{asset_name}') + try: + match_results = FileSystems.match([pattern]) + matches = match_results[0].metadata_list if match_results else [] + except Exception as e: + raise ValueError( + f'Could not locate vocabulary artifact {asset_name!r} under ' + f'{artifact_location!r} due to error: {e}') from e + if not matches: + raise ValueError( + f'Could not locate vocabulary artifact {asset_name!r} under ' + f'{artifact_location!r}.') + return matches[0].path + + +def _read_vocab_tokens(vocab_asset_path: str) -> list[str]: + tokens = [] + with FileSystems.open(vocab_asset_path) as f: + for raw_line in f: + token = raw_line.decode('utf-8').rstrip('\r\n') + if token: + tokens.append(token) + return tokens + + +def _write_vocab_file(output_path: str, tokens: list[str]) -> None: + with FileSystems.create(output_path) as f: + for token in tokens: + f.write((token + '\n').encode('utf-8')) + + +def parse_known_args(argv): + parser = argparse.ArgumentParser( + description='Generate vocabulary from batch input with MLTransform.') + parser.add_argument('--input_file', help='Input JSONL file path.') + parser.add_argument( + '--input_table', + help='Input BigQuery table path in PROJECT:DATASET.TABLE format.') + parser.add_argument('--output_vocab', help='Output vocab file prefix/path.') + parser.add_argument( + '--columns', + help='Comma-separated source columns to include in vocabulary.') + parser.add_argument( + '--vocab_size', + type=int, + default=50000, + help='Maximum vocabulary size (top-K by frequency).') + parser.add_argument( + '--min_frequency', + type=int, + default=1, + help='Minimum token frequency required to keep token.') + parser.add_argument( + '--lowercase', + default='true', + help='Whether to lowercase text before vocabulary generation.') + parser.add_argument( + '--input_expand_factor', + type=int, + default=1, + help=( + 'Batch-only: repeat each input line this many times to scale volume ' + 'for load/perf testing.')) + parser.add_argument( + '--artifact_location', + default='', + help=( + 'Artifact directory for MLTransform output. If empty, a temporary ' + 'local directory is used.')) + return parser.parse_known_args(argv) + + +def validate_args(args) -> list[str]: + has_input_file = bool(args.input_file) + has_input_table = bool(args.input_table) + if not has_input_file and not has_input_table: + raise ValueError('One of --input_file or --input_table is required.') + if has_input_file and has_input_table: + raise ValueError('Use exactly one of --input_file or --input_table.') + if not args.output_vocab: + raise ValueError('--output_vocab is required.') + if not args.columns: + raise ValueError('--columns is required.') + if args.vocab_size is None or args.vocab_size <= 0: + raise ValueError('--vocab_size must be > 0.') + if args.min_frequency is None or args.min_frequency < 1: + raise ValueError('--min_frequency must be >= 1.') + if args.input_expand_factor is None or args.input_expand_factor < 1: + raise ValueError('--input_expand_factor must be >= 1.') + seen = set() + deduped_cols = [] + for col in (c.strip() for c in args.columns.split(',')): + if col and col not in seen: + seen.add(col) + deduped_cols.append(col) + return deduped_cols + + +def run(argv=None, test_pipeline=None): + known_args, pipeline_args = parse_known_args(argv) + columns = validate_args(known_args) + lowercase = parse_bool_flag(known_args.lowercase) + + options = PipelineOptions(pipeline_args) + artifact_location = known_args.artifact_location + if not artifact_location: + temp_location = options.get_all_options().get('temp_location') + if temp_location and (temp_location.startswith('gs://') or + temp_location.startswith('s3://')): + artifact_location = ( + f'{temp_location.rstrip("/")}/mltransform_vocab_artifacts') + else: + artifact_location = tempfile.mkdtemp( + prefix='mltransform_generate_vocab_artifacts_') + + pipeline = test_pipeline or beam.Pipeline(options=options) + + if known_args.input_file: + lines = ( + pipeline + | 'ReadInputFile' >> beam.io.ReadFromText(known_args.input_file)) + if known_args.input_expand_factor > 1: + lines = ( + lines + | 'ExpandInputForPerf' >> beam.FlatMap( + lambda line, n: [line] * n, known_args.input_expand_factor)) + rows = lines | 'ParseJSON' >> beam.Map(_parse_json_line) + else: + rows = pipeline | 'ReadInputTable' >> beam.io.ReadFromBigQuery( + table=known_args.input_table) + + vocab_text = ( + rows + | 'BuildVocabText' >> + beam.Map(lambda row: _build_vocab_text(row, columns, lowercase)) + | 'DropEmptyText' >> beam.Filter(bool)) + + _ = ( + vocab_text + | 'MLTransformInput' >> beam.Map(lambda text: {'text': text}) + | 'ApplyMLTransform' >> + MLTransform(write_artifact_location=artifact_location).with_transform( + ComputeAndApplyVocabulary( + columns=['text'], + top_k=known_args.vocab_size, + frequency_threshold=known_args.min_frequency, + split_string_by_delimiter=' ', + vocab_filename='vocab'))) + + result = pipeline.run() + result.wait_until_finish() + + vocab_tokens = _read_vocab_tokens( + _resolve_vocab_asset_path( + artifact_location=artifact_location, + vocab_filename='vocab', + column_name='text')) + _write_vocab_file(known_args.output_vocab, vocab_tokens) + return result + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + run() diff --git a/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt b/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt new file mode 100644 index 000000000000..4e00b3b4316c --- /dev/null +++ b/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt @@ -0,0 +1,26 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Dataflow worker requirements for mltransform_generate_vocab load tests. +# MLTransform TFT operations need a consistent TensorFlow Transform stack; +# otherwise workers can crash-loop with pandas/numpy ABI mismatches. +google-cloud-monitoring>=2.27.0 +tensorflow_transform>=1.14.0,<1.15.0 +tensorflow-metadata>=1.14.0,<1.15.0 +tfx-bsl>=1.14.0,<1.15.0 +numpy<2 +pandas<2 +dill diff --git a/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_test.py b/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_test.py new file mode 100644 index 000000000000..3e29bd3aae10 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab_test.py @@ -0,0 +1,191 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import os +import tempfile +import unittest + +try: + from apache_beam.examples.ml_transform import mltransform_generate_vocab +except ImportError: + raise unittest.SkipTest('tensorflow_transform is not installed.') + + +class MLTransformGenerateVocabUnitTest(unittest.TestCase): + def test_normalize_text(self): + text = mltransform_generate_vocab.normalize_text(' Hello Beam ', True) + self.assertEqual(text, 'hello beam') + + def test_null_and_empty_handling_helpers(self): + normalized_none = mltransform_generate_vocab.normalize_text(None, True) + self.assertEqual(normalized_none, '') + self.assertEqual( + mltransform_generate_vocab._build_vocab_text( + {'text': ['Beam', None, ' ', 'Flow']}, ['text'], lowercase=True), + 'beam flow') + + +class MLTransformGenerateVocabCliValidationTest(unittest.TestCase): + def test_missing_required_args(self): + args, _ = mltransform_generate_vocab.parse_known_args([]) + with self.assertRaisesRegex(ValueError, 'input_file or --input_table'): + mltransform_generate_vocab.validate_args(args) + + def test_invalid_numeric_values(self): + args, _ = mltransform_generate_vocab.parse_known_args([ + '--input_file=a.jsonl', + '--output_vocab=/tmp/vocab', + '--columns=text', + '--vocab_size=0', + '--min_frequency=0', + ]) + with self.assertRaisesRegex(ValueError, 'vocab_size'): + mltransform_generate_vocab.validate_args(args) + + def test_invalid_input_expand_factor(self): + args, _ = mltransform_generate_vocab.parse_known_args([ + '--input_file=a.jsonl', + '--output_vocab=/tmp/vocab', + '--columns=text', + '--input_expand_factor=0', + ]) + with self.assertRaisesRegex(ValueError, 'input_expand_factor'): + mltransform_generate_vocab.validate_args(args) + + +class MLTransformGenerateVocabIntegrationTest(unittest.TestCase): + def test_batch_pipeline_exact_output_order(self): + with tempfile.TemporaryDirectory() as tmpdir: + input_path = os.path.join(tmpdir, 'input.jsonl') + output_prefix = os.path.join(tmpdir, 'vocab.txt') + + rows = [ + { + 'id': '1', 'text': 'Beam beam ML pipeline' + }, + { + 'id': '2', 'text': 'Beam pipeline dataflow' + }, + { + 'id': '3', 'text': 'ML transform beam' + }, + { + 'id': '4', 'text': 'vocab vocab vocab test' + }, + { + 'id': '5', 'text': 'rare_token_once' + }, + { + 'id': '6', 'text': '' + }, + { + 'id': '7', 'text': None + }, + ] + with open(input_path, 'w', encoding='utf-8') as f: + for row in rows: + f.write(json.dumps(row) + '\n') + + mltransform_generate_vocab.run([ + f'--input_file={input_path}', + f'--output_vocab={output_prefix}', + '--columns=text', + '--vocab_size=3', + '--min_frequency=2', + '--lowercase=true', + '--runner=DirectRunner', + ]) + + output_path = output_prefix + with open(output_path, 'r', encoding='utf-8') as f: + output_tokens = [line.rstrip('\n') for line in f] + + self.assertEqual(set(output_tokens), {'beam', 'vocab', 'ml'}) + self.assertEqual(len(output_tokens), 3) + + def test_output_is_stable_across_runs(self): + with tempfile.TemporaryDirectory() as tmpdir: + input_path = os.path.join(tmpdir, 'input.jsonl') + output_prefix = os.path.join(tmpdir, 'vocab.txt') + rows = [ + { + 'text': 'apple banana' + }, + { + 'text': 'banana apple' + }, + { + 'text': 'cat dog' + }, + { + 'text': 'dog cat' + }, + ] + with open(input_path, 'w', encoding='utf-8') as f: + for row in rows: + f.write(json.dumps(row) + '\n') + + common_args = [ + f'--input_file={input_path}', + '--columns=text', + '--vocab_size=4', + '--min_frequency=2', + '--lowercase=true', + '--runner=DirectRunner', + ] + + mltransform_generate_vocab.run( + common_args + [f'--output_vocab={output_prefix}']) + + output_path = output_prefix + with open(output_path, 'r', encoding='utf-8') as f: + first_run_tokens = [line.rstrip('\n') for line in f] + + output_prefix_second = os.path.join(tmpdir, 'vocab_second.txt') + mltransform_generate_vocab.run( + common_args + [f'--output_vocab={output_prefix_second}']) + + with open(output_prefix_second, 'r', encoding='utf-8') as f: + second_run_tokens = [line.rstrip('\n') for line in f] + + self.assertEqual(first_run_tokens, second_run_tokens) + + def test_empty_filtered_result_writes_empty_vocab(self): + with tempfile.TemporaryDirectory() as tmpdir: + input_path = os.path.join(tmpdir, 'input.jsonl') + output_prefix = os.path.join(tmpdir, 'vocab.txt') + with open(input_path, 'w', encoding='utf-8') as f: + f.write(json.dumps({'text': 'beam'}) + '\n') + + mltransform_generate_vocab.run([ + f'--input_file={input_path}', + f'--output_vocab={output_prefix}', + '--columns=text', + '--vocab_size=10', + '--min_frequency=2', + '--runner=DirectRunner', + ]) + + output_path = output_prefix + with open(output_path, 'r', encoding='utf-8') as f: + output_tokens = [line.rstrip('\n') for line in f] + self.assertEqual(output_tokens, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/sdks/python/apache_beam/testing/benchmarks/inference/mltransform_generate_vocab_benchmark.py b/sdks/python/apache_beam/testing/benchmarks/inference/mltransform_generate_vocab_benchmark.py new file mode 100644 index 000000000000..98a7b59191dc --- /dev/null +++ b/sdks/python/apache_beam/testing/benchmarks/inference/mltransform_generate_vocab_benchmark.py @@ -0,0 +1,56 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Benchmark test for the batch-only MLTransform Generate Vocab pipeline.""" + +import logging + +from apache_beam.examples.ml_transform import mltransform_generate_vocab +from apache_beam.testing.load_tests.dataflow_cost_benchmark import DataflowCostBenchmark + + +class MLTransformGenerateVocabBenchmarkTest(DataflowCostBenchmark): + """Runs the MLTransform vocab generation pipeline as a cost benchmark.""" + def __init__(self): + # Namespace used by benchmark dashboards. + self.metrics_namespace = 'BeamML_MLTransformVocab' + # Monitor throughput on text rows after empty strings are filtered. + super().__init__( + metrics_namespace=self.metrics_namespace, + is_streaming=False, + pcollection='DropEmptyText.out0') + + def test(self): + """Execute the MLTransform vocab generation pipeline for benchmarking.""" + extra_opts = { + 'input_file': self.pipeline.get_option('input_file'), + 'output_vocab': self.pipeline.get_option('output_vocab'), + 'artifact_location': self.pipeline.get_option('artifact_location'), + 'columns': self.pipeline.get_option('columns'), + 'vocab_size': self.pipeline.get_option('vocab_size'), + 'min_frequency': self.pipeline.get_option('min_frequency'), + 'lowercase': self.pipeline.get_option('lowercase'), + 'input_expand_factor': self.pipeline.get_option('input_expand_factor'), + } + + self.result = mltransform_generate_vocab.run( + self.pipeline.get_full_options_as_args(**extra_opts)) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + MLTransformGenerateVocabBenchmarkTest().run() diff --git a/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py b/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py index 3df96c03a8a2..3300831b8ea1 100644 --- a/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py +++ b/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py @@ -193,6 +193,32 @@ def _get_throughput_metrics( """Query Cloud Monitoring for per-PCollection throughput.""" name = ( pcollection_name if pcollection_name is not None else self.pcollection) + + def _point_numeric_value(point) -> float: + value = point.value + # point.value is proto-plus; use the underlying protobuf oneof if present. + msg = getattr(value, '_pb', value) + if msg is not None: + try: + active_field = msg.WhichOneof('value') + except AttributeError: + active_field = None + if active_field == 'double_value': + return float(value.double_value) + if active_field == 'int64_value': + return float(value.int64_value) + if active_field == 'distribution_value': + # Use aligned mean for distribution-valued points. + distribution = value.distribution_value + if distribution.count > 0: + return float(distribution.mean) + return 0.0 + if active_field == 'money_value': + money = value.money_value + nanos = getattr(money, 'nanos', 0) + return float(money.units) + (float(nanos) / 1_000_000_000.0) + return 0.0 + interval = monitoring_v3.TimeInterval( start_time=start_time, end_time=end_time) aggregation = monitoring_v3.Aggregation( @@ -221,7 +247,7 @@ def _get_throughput_metrics( for key, req in requests.items(): time_series = self.monitoring_client.list_time_series(request=req) values = [ - point.value.double_value for series in time_series + _point_numeric_value(point) for series in time_series for point in series.points ] metrics[f"AvgThroughput{key}"] = sum(values) / len( diff --git a/website/www/site/content/en/performance/_index.md b/website/www/site/content/en/performance/_index.md index 1624c58efe2e..350524a3c86d 100644 --- a/website/www/site/content/en/performance/_index.md +++ b/website/www/site/content/en/performance/_index.md @@ -59,3 +59,4 @@ See the following pages for performance measures recorded when running various B - [TensorFlow MNIST Image Classification](/performance/tensorflowmnist) - [VLLM Gemma Batch Completion Tesla T4 GPU](/performance/vllmgemmabatchtesla) - [Table Row Inference Sklearn Batch](/performance/tablerowinference) +- [MLTransform Generate Vocab (batch)](/performance/mltransformvocab) diff --git a/website/www/site/content/en/performance/mltransformvocab/_index.md b/website/www/site/content/en/performance/mltransformvocab/_index.md new file mode 100644 index 000000000000..f7844a5a09ae --- /dev/null +++ b/website/www/site/content/en/performance/mltransformvocab/_index.md @@ -0,0 +1,37 @@ +--- +title: "MLTransform Generate Vocab Performance" +--- + + + +# MLTransform Generate Vocab Performance + +**Model**: Apache Beam MLTransform — ComputeAndApplyVocabulary (Generate Vocab) +**Accelerator**: CPU (batch) +**Host**: MLTransform vocab pipeline on Dataflow (batch) + +This batch pipeline computes a vocabulary from input text columns using +`ComputeAndApplyVocabulary` and writes vocabulary artifacts for downstream use. + +See the [glossary](/performance/glossary) for definitions. + +Full pipeline implementation is available here: +https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/ml_transform/mltransform_generate_vocab.py + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="mltransformvocab" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="mltransformvocab" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="mltransformvocab" read_or_write="write" section="date" >}} + diff --git a/website/www/site/data/performance.yaml b/website/www/site/data/performance.yaml index 8841bbb58ecb..9725a4af99a9 100644 --- a/website/www/site/data/performance.yaml +++ b/website/www/site/data/performance.yaml @@ -283,3 +283,19 @@ looks: title: AvgThroughputBytesPerSec by Version - id: P7wKZy6tQFWbbDfm4HzfCJnsQrVgfGsJ title: AvgThroughputElementsPerSec by Version + mltransformvocab: + write: + folder: 107 + cost: + - id: CccNCHQ23Js2tmxTDYVnjxwfx2zSFSjY + title: RunTime and EstimatedCost + date: + - id: n7Gn7c7KSW52ZgXCQXndHgWtjVPK8W33 + title: AvgThroughputBytesPerSec by Date + - id: 3tcg6cSh6tBg6FmpTxgzyMtvFqWKkQn3 + title: AvgThroughputElementsPerSec by Date + version: + - id: tPYwJngPDBsKjK3DNgGHGsCyTpxdfmTB + title: AvgThroughputBytesPerSec by Version + - id: cC75NnCbQT3mQmKVHtDxzptXpwPb64qz + title: AvgThroughputElementsPerSec by Version From 6dcd79bda9dc536b14d0a0a2d5222b67288b97e7 Mon Sep 17 00:00:00 2001 From: Durgaprasad M L Date: Thu, 28 May 2026 21:26:05 +0530 Subject: [PATCH 250/490] [#38708] Add Flink 2.0 to SDK pipeline options validation lists (#38726) --- .../actions/setup-default-test-properties/test-properties.json | 2 +- sdks/python/apache_beam/options/pipeline_options.py | 2 +- sdks/typescript/src/apache_beam/runners/flink.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-default-test-properties/test-properties.json b/.github/actions/setup-default-test-properties/test-properties.json index e3f1d7a3abcb..f06de5174e6c 100644 --- a/.github/actions/setup-default-test-properties/test-properties.json +++ b/.github/actions/setup-default-test-properties/test-properties.json @@ -14,7 +14,7 @@ }, "JavaTestProperties": { "SUPPORTED_VERSIONS": ["8", "11", "17", "21", "25"], - "FLINK_VERSIONS": ["1.17", "1.18", "1.19", "1.20"], + "FLINK_VERSIONS": ["1.17", "1.18", "1.19", "1.20", "2.0"], "SPARK_VERSIONS": ["3"] }, "GoTestProperties": { diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index 2533083f7e7e..e3ab13e25122 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -2021,7 +2021,7 @@ def _add_argparse_args(cls, parser): class FlinkRunnerOptions(PipelineOptions): # These should stay in sync with gradle.properties. - PUBLISHED_FLINK_VERSIONS = ['1.17', '1.18', '1.19', '1.20'] + PUBLISHED_FLINK_VERSIONS = ['1.17', '1.18', '1.19', '1.20', '2.0'] @classmethod def _add_argparse_args(cls, parser): diff --git a/sdks/typescript/src/apache_beam/runners/flink.ts b/sdks/typescript/src/apache_beam/runners/flink.ts index 5877d9186a4b..8f80b971da2a 100644 --- a/sdks/typescript/src/apache_beam/runners/flink.ts +++ b/sdks/typescript/src/apache_beam/runners/flink.ts @@ -28,7 +28,7 @@ import { JavaJarService } from "../utils/service"; const MAGIC_HOST_NAMES = ["[local]", "[auto]"]; // These should stay in sync with gradle.properties. -const PUBLISHED_FLINK_VERSIONS = ["1.17", "1.18", "1.19", "1.20"]; +const PUBLISHED_FLINK_VERSIONS = ["1.17", "1.18", "1.19", "1.20", "2.0"]; const defaultOptions = { flinkMaster: "[local]", From 3512bba71143bf470dbe756c8792028407e85712 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Thu, 28 May 2026 12:45:55 -0400 Subject: [PATCH 251/490] up timeout (#38732) --- .github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index ea1c255f7cc9..4f30d1f9f081 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -55,7 +55,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 + timeout-minutes: 180 name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) strategy: matrix: From 0096c548014923fe3c4c315de114d0a9f420a13e Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 28 May 2026 20:46:07 +0200 Subject: [PATCH 252/490] remove playground annotations from BatchElementsExample until next release (#38733) --- .../apache/beam/examples/BatchElementsExample.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java index 79130d7ac138..0103999d9256 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java @@ -29,19 +29,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// beam-playground: -// name: BatchElements -// description: Demonstration of BatchElements transform usage. -// multifile: false -// default_example: false -// context_line: 47 -// categories: -// - Core Transforms -// complexity: BASIC -// tags: -// - transforms -// - batch - public class BatchElementsExample { public static void main(String[] args) { PipelineOptions options = PipelineOptionsFactory.create(); From 192f2ba0a679b3383ea1e85527399c87ca9f23fb Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 28 May 2026 15:39:18 -0400 Subject: [PATCH 253/490] Update CHANGES.md after release cut (#38608) --- CHANGES.md | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c87c1271280b..74209bb7499c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,17 +55,50 @@ * ([#X](https://github.com/apache/beam/issues/X)). --> -# [2.74.0] - Unreleased +# [2.75.0] - Unreleased ## Highlights * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). * New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). -* Spark 4 runner support for Java SDK ([#38255](https://github.com/apache/beam/issues/38255)). ## I/Os * Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). + +## New Features / Improvements + +* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). + +## Breaking Changes + +* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). + +## Deprecations + +* X behavior is deprecated and will be removed in X versions ([#X](https://github.com/apache/beam/issues/X)). + +## Bugfixes + +* Fixed X (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). + +## Security Fixes + +* Fixed [CVE-YYYY-NNNN](https://www.cve.org/CVERecord?id=CVE-YYYY-NNNN) (Java/Python/Go) ([#X](https://github.com/apache/beam/issues/X)). + +## Known Issues + +[comment]: # ( When updating known issues after release, make sure also update website blog in website/www/site/content/blog.) +* ([#X](https://github.com/apache/beam/issues/X)). + +# [2.74.0] - 2026-XX-xx + +## Highlights + +* Spark 4 runner support for Java SDK ([#38255](https://github.com/apache/beam/issues/38255)). + +## I/Os + * IcebergIO: support declaring a table's sort order on dynamic table creation via the new `sort_fields` config ([#38269](https://github.com/apache/beam/issues/38269)). * IcebergIO: support writing with hash distribution mode, and with autosharding ([#38061](https://github.com/apache/beam/issues/38061))). @@ -87,11 +120,9 @@ ## Breaking Changes * (Python) Made Beartype the default fallback type checking tool. This can be disabled with the `--disable_beartype` pipeline option. ([#38275](https://github.com/apache/beam/issues/38275)) -* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). ## Deprecations -* X behavior is deprecated and will be removed in X versions ([#X](https://github.com/apache/beam/issues/X)). * Dropped Java 8 support ([#31678](https://github.com/apache/beam/issues/31678)). * Removed Samza Runner support ([#35448](https://github.com/apache/beam/issues/35448)). @@ -101,15 +132,6 @@ * Added `max_batch_duration_secs` passthrough support in Python Enrichment BigQuery and CloudSQL handlers so batching duration can be forwarded to `BatchElements` ([#38243](https://github.com/apache/beam/issues/38243)). * Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). -## Security Fixes - -* Fixed [CVE-YYYY-NNNN](https://www.cve.org/CVERecord?id=CVE-YYYY-NNNN) (Java/Python/Go) ([#X](https://github.com/apache/beam/issues/X)). - -## Known Issues - -[comment]: # ( When updating known issues after release, make sure also update website blog in website/www/site/content/blog.) -* ([#X](https://github.com/apache/beam/issues/X)). - # [2.73.0] - 2026-04-29 ## Highlights From 72c5d7f88f81ddb47cc7ec6e89b01df45c2b9c50 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 28 May 2026 15:42:08 -0400 Subject: [PATCH 254/490] Support infer types involving dataclass fields (#38548) --- sdks/python/apache_beam/typehints/opcodes.py | 6 ++++++ .../apache_beam/typehints/row_type_test.py | 21 +++++++++++++++++++ .../typehints/trivial_inference_test.py | 12 +++++++++++ 3 files changed, 39 insertions(+) diff --git a/sdks/python/apache_beam/typehints/opcodes.py b/sdks/python/apache_beam/typehints/opcodes.py index 8e5d7b1e40c8..963b5e0850b6 100644 --- a/sdks/python/apache_beam/typehints/opcodes.py +++ b/sdks/python/apache_beam/typehints/opcodes.py @@ -29,6 +29,7 @@ """ # pytype: skip-file +import dataclasses import inspect import logging import sys @@ -447,6 +448,11 @@ def _getattr(o, name): return Const(BoundMethod(func, o)) elif isinstance(o, row_type.RowTypeConstraint): return o.get_type_for(name) + elif inspect.isclass(o) and dataclasses.is_dataclass(o): + field = o.__dataclass_fields__.get(name) + if field is not None: + return field.type + return Any else: return Any diff --git a/sdks/python/apache_beam/typehints/row_type_test.py b/sdks/python/apache_beam/typehints/row_type_test.py index 54e64caf6fa7..30bda0cd98ba 100644 --- a/sdks/python/apache_beam/typehints/row_type_test.py +++ b/sdks/python/apache_beam/typehints/row_type_test.py @@ -172,6 +172,27 @@ class DerivedDataClass(BaseDataClass): getattr(DerivedDataClass, row_type._BEAM_SCHEMA_ID)) self.assertNotEqual(schema_for_derived.id, schema_for_base.id) + def test_dataclass_map_typehints(self): + @beam.coders.typecoders.registry.register_row + @dataclass(frozen=True) + class MyDataClass: + id: int + name: str + + p = beam.Pipeline() + pa = (p | beam.Create([MyDataClass(1, "a"), MyDataClass(2, "b")])) + self.assertEqual(pa.element_type, MyDataClass) + + pb = ( + pa | beam.Map( + lambda x: beam.Row(id=x.id, name=x.name, name_hash=hash(x.name)))) + self.assertTrue( + isinstance(pb.element_type, row_type.GeneratedClassRowTypeConstraint)) + self.assertEqual( + pb.element_type, + row_type.GeneratedClassRowTypeConstraint( + fields=[('id', int), ('name', str), ('name_hash', int)])) + if __name__ == '__main__': unittest.main() diff --git a/sdks/python/apache_beam/typehints/trivial_inference_test.py b/sdks/python/apache_beam/typehints/trivial_inference_test.py index fe60974e2806..f421819bdcae 100644 --- a/sdks/python/apache_beam/typehints/trivial_inference_test.py +++ b/sdks/python/apache_beam/typehints/trivial_inference_test.py @@ -19,6 +19,7 @@ # pytype: skip-file +import dataclasses import types import unittest @@ -487,6 +488,17 @@ def testPyCallable(self): python_callable.PythonCallableWithSource("lambda x: (x, str(x))"), [int]) + def testDataClassFields(self): + @dataclasses.dataclass + class MyDataClass: + id: int + name: str + + self.assertReturnType( + typehints.Tuple[int, str], + python_callable.PythonCallableWithSource("lambda x: (x.id, x.name)"), + [MyDataClass]) + if __name__ == '__main__': unittest.main() From 17363b5ba5b3222a07eb778deed9ae07b0ebaaf3 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 28 May 2026 17:49:51 -0400 Subject: [PATCH 255/490] [Prism] Fix race condition in element manager (#38734) --- .github/trigger_files/beam_PostCommit_Python_Versions.json | 2 +- runners/prism/java/build.gradle | 1 + .../beam/runners/prism/internal/engine/elementmanager.go | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json index 9cc78c7d1c6c..8b2c8c445c1f 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Versions.json +++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 4 + "revision": 5 } diff --git a/runners/prism/java/build.gradle b/runners/prism/java/build.gradle index c89974cb6ea5..9357515f36c2 100644 --- a/runners/prism/java/build.gradle +++ b/runners/prism/java/build.gradle @@ -185,6 +185,7 @@ def sickbayTests = [ def createPrismValidatesRunnerTask = { name, environmentType -> Task vrTask = tasks.create(name: name, type: Test, group: "Verification") { description "PrismRunner Java $environmentType ValidatesRunner suite" + outputs.upToDateWhen { false } classpath = configurations.validatesRunner var prismBuildTask = dependsOn(':runners:prism:build') diff --git a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go index f720be20e375..3949d1af3248 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go +++ b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go @@ -387,8 +387,11 @@ func (em *ElementManager) Bundles(ctx context.Context, upstreamCancelFn context. em.pendingElements.Wait() slog.Debug("no more pending elements: terminating pipeline") cancelFn(fmt.Errorf("elementManager out of elements, cleaning up")) - // Ensure the watermark evaluation goroutine exits. + // Ensure the watermark evaluation goroutine exits by locking the mutex + // before broadcasting, preventing a lost wake-up signal. + em.refreshCond.L.Lock() em.refreshCond.Broadcast() + em.refreshCond.L.Unlock() }() // Watermark evaluation goroutine. go func() { @@ -2496,7 +2499,9 @@ func (em *ElementManager) wakeUpAt(t mtime.Time) { // only create this goroutine if we have real-time clock enabled (also implying the pipeline does not have TestStream). go func(fireAt time.Time) { time.AfterFunc(time.Until(fireAt), func() { + em.refreshCond.L.Lock() em.refreshCond.Broadcast() + em.refreshCond.L.Unlock() }) }(t.ToTime()) } From 027964c942d07edb7a87f1ac1c6fe1a7927b3089 Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Thu, 28 May 2026 16:03:20 -0700 Subject: [PATCH 256/490] Add staged package hashes (#38311) --- .../org/apache/beam/runners/dataflow/util/PackageUtil.java | 3 +++ .../apache/beam/runners/dataflow/util/PackageUtilTest.java | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/PackageUtil.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/PackageUtil.java index 31c9a5f3ce0d..e33fb594e272 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/PackageUtil.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/PackageUtil.java @@ -438,6 +438,7 @@ public static PackageAttributes forFileToStage( .toString(); destination.setLocation(resourcePath); destination.setName(dest); + destination.setSha256(hash); return new AutoValue_PackageUtil_PackageAttributes( file, null, destination, file.length(), hash); } @@ -456,6 +457,7 @@ public static PackageAttributes forBytesToStage( DataflowPackage targetPackage = new DataflowPackage(); targetPackage.setName(target); targetPackage.setLocation(resourcePath); + targetPackage.setSha256(hashCode.toString()); return new AutoValue_PackageUtil_PackageAttributes( null, bytes, targetPackage, size, hashCode.toString()); @@ -465,6 +467,7 @@ public PackageAttributes withPackageName(String overridePackageName) { DataflowPackage newDestination = new DataflowPackage(); newDestination.setName(overridePackageName); newDestination.setLocation(getDestination().getLocation()); + newDestination.setSha256(getHash()); return new AutoValue_PackageUtil_PackageAttributes( getSource(), getBytes(), newDestination, getSize(), getHash()); diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/PackageUtilTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/PackageUtilTest.java index 670032425eb2..7e7ef6a8345a 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/PackageUtilTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/PackageUtilTest.java @@ -191,6 +191,8 @@ public void testFileWithExtensionPackageNamingAndSize() throws Exception { assertThat(target.getName(), endsWith(".txt")); assertThat(target.getLocation(), equalTo(STAGING_PATH + target.getName())); assertThat(attr.getSize(), equalTo((long) contents.length())); + String expectedHash = Files.asByteSource(tmpFile).hash(Hashing.sha256()).toString(); + assertThat(target.getSha256(), equalTo(expectedHash)); } @Test @@ -299,6 +301,8 @@ public void testPackageUploadWithFileSucceeds() throws Exception { assertThat(target.getName(), endsWith(".txt")); assertThat(target.getLocation(), equalTo(STAGING_PATH + target.getName())); + String expectedHash = Files.asByteSource(tmpFile).hash(Hashing.sha256()).toString(); + assertThat(target.getSha256(), equalTo(expectedHash)); assertThat( new LineReader(Channels.newReader(pipe.source(), StandardCharsets.UTF_8.name())).readLine(), equalTo(contents)); From 5ac4ba64c3210e0c33916950a9474cc9ddeb18f5 Mon Sep 17 00:00:00 2001 From: TongruiLi <12992126+TongruiLi@users.noreply.github.com> Date: Thu, 28 May 2026 18:09:51 -0700 Subject: [PATCH 257/490] Updated All Externally Visible Logs/Docs from Runner V2 to Portable Runner (#38532) * Aded portable runner to python and go runners * Removed empty if, added disable unit test for python. * Removed unnessary class in unit test * Removed unused variable * Ran formatter * Updated All Reference of Runner V2/V1 to portable runner/streaming java runner * Specified portable runner as dataflow portable runner * Updated testing logic to make things more clear * spotless * python spotless * fixed broken test * removed unnessary regex --- .../arm/build.gradle | 2 +- .../google-cloud-dataflow-java/build.gradle | 10 ++--- .../beam/runners/dataflow/DataflowRunner.java | 19 ++++---- .../runners/dataflow/DataflowRunnerTest.java | 5 ++- .../windmill/src/main/proto/windmill.proto | 2 +- sdks/go/pkg/beam/runners/dataflow/dataflow.go | 9 ++-- .../beam/runners/dataflow/dataflow_test.go | 43 ++++++++++++++++++- .../ordered_window_elements/streaming.py | 2 +- .../apache_beam/examples/kafkataxi/README.md | 2 +- .../io/external/xlang_bigqueryio_it_test.py | 2 +- .../runners/dataflow/dataflow_metrics.py | 2 +- .../runners/dataflow/dataflow_runner.py | 16 ++++--- .../runners/dataflow/dataflow_runner_test.py | 20 +++++++++ .../runners/dataflow/internal/apiclient.py | 8 ++-- sdks/python/apache_beam/transforms/core.py | 4 +- 15 files changed, 109 insertions(+), 37 deletions(-) diff --git a/runners/google-cloud-dataflow-java/arm/build.gradle b/runners/google-cloud-dataflow-java/arm/build.gradle index 2e74d7727f21..531300bb63ea 100644 --- a/runners/google-cloud-dataflow-java/arm/build.gradle +++ b/runners/google-cloud-dataflow-java/arm/build.gradle @@ -118,7 +118,7 @@ task printrunnerV2PipelineOptionsARM { dependsOn buildAndPushDockerJavaMultiarchContainer doLast { - println "To run a Dataflow job with runner V2 on ARM, add the following pipeline options to your command-line:" + println "To run a Dataflow job with Dataflow Portable Runner on ARM, add the following pipeline options to your command-line:" println runnerV2PipelineOptionsARM.join(' ') } } diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index 93fe9cb227bb..943a01daa7d0 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -431,7 +431,7 @@ task printRunnerV2PipelineOptions { dependsOn buildAndPushDockerJavaContainer doLast { - println "To run a Dataflow job with runner V2, add the following pipeline options to your command-line:" + println "To run a Dataflow job with Dataflow Portable Runner, add the following pipeline options to your command-line:" println runnerV2PipelineOptions.join(' ') println "Please delete your image upon completion with the following command:" println "docker rmi ${dockerJavaImageName}; gcloud container images delete --force-delete-tags ${dockerJavaImageName}" @@ -471,7 +471,7 @@ def validatesRunnerStreamingConfig = [ excludedTests: [ // TODO(https://github.com/apache/beam/issues/21472) 'org.apache.beam.sdk.transforms.GroupByKeyTest$BasicTests.testAfterProcessingTimeContinuationTriggerUsingState', - // GroupIntoBatches.withShardedKey not supported on streaming runner v1 + // GroupIntoBatches.withShardedKey not supported on Streaming Java Runner // https://github.com/apache/beam/issues/22592 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithShardedKeyInGlobalWindow', @@ -570,7 +570,7 @@ createCrossLanguageValidatesRunnerTask( task validatesRunnerV2 { group = "Verification" - description = "Runs the ValidatesRunner tests on Dataflow Runner V2" + description = "Runs the ValidatesRunner tests on Dataflow Portable Runner" dependsOn(createRunnerV2ValidatesRunnerTest( name: 'validatesRunnerV2Test', pipelineOptions: runnerV2PipelineOptions, @@ -610,7 +610,7 @@ task validatesRunnerV2 { task validatesRunnerV2Streaming { group = "Verification" - description = "Runs the ValidatesRunner tests on Dataflow Runner V2 forcing streaming mode" + description = "Runs the ValidatesRunner tests on Dataflow Portable Runner forcing streaming mode" dependsOn(createRunnerV2ValidatesRunnerTest( name: 'validatesRunnerV2TestStreaming', pipelineOptions: runnerV2PipelineOptions + ['--streaming', '--experiments=enable_streaming_engine'], @@ -880,7 +880,7 @@ task postCommit { task postCommitRunnerV2 { group = "Verification" - description = "Various integration tests using the Dataflow runner V2." + description = "Various integration tests using the Dataflow Portable Runner." dependsOn googleCloudPlatformRunnerV2IntegrationTest dependsOn coreSDKJavaRunnerV2IntegrationTest } diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 299e7fa21ed1..d04afb351e44 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1242,12 +1242,12 @@ private static boolean includesTransformUpgrades(Pipeline pipeline) { @Override public DataflowPipelineJob run(Pipeline pipeline) { // Multi-language pipelines and pipelines that include upgrades should automatically be upgraded - // to Runner v2. + // to Dataflow Portable Runner. if (DataflowRunner.isMultiLanguagePipeline(pipeline) || includesTransformUpgrades(pipeline)) { if (!useUnifiedWorker(options)) { List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); LOG.info( - "Automatically enabling Dataflow Runner v2 since the pipeline used cross-language" + "Automatically enabling Dataflow Portable Runner since the pipeline used cross-language" + " transforms or pipeline needed a transform upgrade."); options.setExperiments( ImmutableList.builder().addAll(experiments).add("use_runner_v2").build()); @@ -1260,7 +1260,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { || hasExperiment(options, "disable_portable_runner") || hasExperiment(options, "enable_streaming_java_runner")) { throw new IllegalArgumentException( - "Runner V2 both disabled and enabled: at least one of ['beam_fn_api', 'use_unified_worker', 'use_runner_v2', 'use_portable_job_submission'] is set and also one of ['disable_runner_v2', 'disable_runner_v2_until_2023', 'disable_prime_runner_v2'] is set."); + "Dataflow Portable Runner both disabled and enabled: at least one of ['enable_portable_runner', 'beam_fn_api', 'use_unified_worker', 'use_runner_v2', 'use_portable_job_submission'] is set and also one of ['enable_streaming_java_runner', 'disable_portable_runner', 'disable_runner_v2', 'disable_runner_v2_until_2023', 'disable_prime_runner_v2'] is set."); } List experiments = new ArrayList<>(options.getExperiments()); // non-null if useUnifiedWorker is true @@ -1374,10 +1374,13 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.as(SdkHarnessOptions.class).setPipelineProtoHash(pipelineProtoHash); if (useUnifiedWorker(options)) { - LOG.info("Skipping v1 transform replacements since job will run on v2."); + LOG.info( + "Skipping Dataflow Streaming Java Runner transform replacements since job will run on Dataflow Portable Runner."); } else { - // Now rewrite things to be as needed for v1 (mutates the pipeline) - // This way the job submitted is valid for v1 and v2, simultaneously + // Now rewrite things to be as needed for Dataflow Streaming Java Runner (mutates the + // pipeline) + // This way the job submitted is valid for Dataflow Streaming Java Runner and Dataflow + // Portable Runner, simultaneously replaceV1Transforms(pipeline); } // Capture the SdkComponents for look up during step translations @@ -1388,7 +1391,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { .addAllDependencies(getDefaultArtifacts()) .addAllCapabilities(Environments.getJavaCapabilities()) .build()); - // No need to perform transform upgrading for the Runner v1 proto. + // No need to perform transform upgrading for the Dataflow Streaming Java Runner proto. RunnerApi.Pipeline dataflowV1PipelineProto = PipelineTranslation.toProto(pipeline, dataflowV1Components, true, false); @@ -1544,7 +1547,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.setExperiments(experiments); LOG.warn( "The upload_graph experiment was specified, but it does not apply " - + "to runner v2 jobs. Option has been automatically removed."); + + "to Dataflow Portable Runner jobs. Option has been automatically removed."); } // Upload the job to GCS and remove the graph object from the API call. The graph diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java index f2f8b44a1626..122c4aeee34f 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java @@ -1855,7 +1855,10 @@ public void testSettingConflictingEnableAndDisableExperimentsThrowsException() t ExperimentalOptions.addExperiment(options, disabledExperiment); Pipeline p = Pipeline.create(options); p.apply(Create.of("A")); - assertThrows("Runner V2 both disabled and enabled", IllegalArgumentException.class, p::run); + assertThrows( + "Dataflow Portable Runner both disabled and enabled", + IllegalArgumentException.class, + p::run); } } } diff --git a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto index 58e4f7df3c34..1da7ef9be8bb 100644 --- a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto +++ b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto @@ -979,7 +979,7 @@ enum ConnectivityType { CONNECTIVITY_TYPE_DIRECTPATH = 2; } -// Settings to control runtime behavior of the java runner v1 user worker. +// Settings to control runtime behavior of the Streaming Java Runner user worker. message UserWorkerRunnerV1Settings { optional UserWorkerGrpcFlowControlSettings flow_control_settings = 3; diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow.go b/sdks/go/pkg/beam/runners/dataflow/dataflow.go index e968911fcca1..ce5e99b0a8de 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow.go @@ -334,7 +334,7 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions experiments := jobopts.GetExperiments() // Ensure that we enable the same set of experiments across all SDKs - // for runner v2. + // for Dataflow Portable Runner. var fnApiSet, v2set, uwSet, portaSubmission, seSet, wsSet bool for _, e := range experiments { if strings.Contains(e, "beam_fn_api") { @@ -349,8 +349,9 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions if strings.Contains(e, "use_portable_job_submission") { portaSubmission = true } - if strings.Contains(e, "disable_runner_v2") || strings.Contains(e, "disable_runner_v2_until_2023") || strings.Contains(e, "disable_prime_runner_v2") { - return nil, errors.New("detected one of the following experiments: disable_runner_v2 | disable_runner_v2_until_2023 | disable_prime_runner_v2. Disabling runner v2 is no longer supported as of Beam version 2.45.0+") + // enable_portable_runner is not documented and hence wont be set by default. This will be fixed in later versions. + if strings.Contains(e, "disable_runner_v2") || strings.Contains(e, "disable_runner_v2_until_2023") || strings.Contains(e, "disable_prime_runner_v2") || strings.Contains(e, "disable_portable_runner") || strings.Contains(e, "enable_streaming_java_runner") { + return nil, errors.New("detected one of the following experiments: disable_runner_v2 | disable_runner_v2_until_2023 | disable_prime_runner_v2 | disable_portable_runner | enable_streaming_java_runner. Disabling Dataflow Portable Runner is no longer supported as of Beam version 2.45.0+") } } // Enable default experiments. @@ -368,7 +369,7 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions } // Ensure that streaming specific experiments are set for streaming pipelines - // since runner v2 only supports using streaming engine. + // since Dataflow Portable Runner only supports using streaming engine. if streaming { if !seSet { experiments = append(experiments, "enable_streaming_engine") diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go index 23dcd034120a..83d3c0108439 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go @@ -20,6 +20,7 @@ import ( "flag" "reflect" "sort" + "strings" "testing" "github.com/apache/beam/sdks/v2/go/pkg/beam/core" @@ -227,7 +228,7 @@ func TestGetJobOptions_NoExperimentsSetStreaming(t *testing.T) { } } -func TestGetJobOptions_DisableRunnerV2ExperimentsSet(t *testing.T) { +func TestGetJobOptions_DisableRunnerV2ExperimentsSetFailJob(t *testing.T) { resetGlobals() *stagingLocation = "gs://testStagingLocation" *gcpopts.Project = "testProject" @@ -238,6 +239,46 @@ func TestGetJobOptions_DisableRunnerV2ExperimentsSet(t *testing.T) { if err == nil { t.Error("getJobOptions() returned error nil, want an error") + } else if !strings.Contains(err.Error(), "Disabling Dataflow Portable Runner is no longer supported") { + t.Errorf("getJobOptions() returned wrong error %q, want it to mention %q", err.Error(), "Disabling Dataflow Portable Runner is no longer supported") + } + if opts != nil { + t.Errorf("getJobOptions() returned JobOptions when it should not have, got %#v, want nil", opts) + } +} + +func TestGetJobOptions_DisablePortableRunnerExperimentsSetFailJob(t *testing.T) { + resetGlobals() + *stagingLocation = "gs://testStagingLocation" + *gcpopts.Project = "testProject" + *gcpopts.Region = "testRegion" + *jobopts.Experiments = "disable_portable_runner" + + opts, err := getJobOptions(context.Background(), false) + + if err == nil { + t.Error("getJobOptions() returned error nil, want an error") + } else if !strings.Contains(err.Error(), "Disabling Dataflow Portable Runner is no longer supported") { + t.Errorf("getJobOptions() returned wrong error %q, want it to mention %q", err.Error(), "Disabling Dataflow Portable Runner is no longer supported") + } + if opts != nil { + t.Errorf("getJobOptions() returned JobOptions when it should not have, got %#v, want nil", opts) + } +} + +func TestGetJobOptions_EnableStreamingJavaRunnerExperimentsSetFailJob(t *testing.T) { + resetGlobals() + *stagingLocation = "gs://testStagingLocation" + *gcpopts.Project = "testProject" + *gcpopts.Region = "testRegion" + *jobopts.Experiments = "enable_streaming_java_runner" + + opts, err := getJobOptions(context.Background(), false) + + if err == nil { + t.Error("getJobOptions() returned error nil, want an error") + } else if !strings.Contains(err.Error(), "Disabling Dataflow Portable Runner is no longer supported") { + t.Errorf("getJobOptions() returned wrong error %q, want it to mention %q", err.Error(), "Disabling Dataflow Portable Runner is no longer supported") } if opts != nil { t.Errorf("getJobOptions() returned JobOptions when it should not have, got %#v, want nil", opts) diff --git a/sdks/python/apache_beam/examples/cookbook/ordered_window_elements/streaming.py b/sdks/python/apache_beam/examples/cookbook/ordered_window_elements/streaming.py index aed1400bc4d8..97e93fb0bc3b 100644 --- a/sdks/python/apache_beam/examples/cookbook/ordered_window_elements/streaming.py +++ b/sdks/python/apache_beam/examples/cookbook/ordered_window_elements/streaming.py @@ -135,7 +135,7 @@ def _state_read_range(self, buffer_state, range_lo, range_hi): def _state_clear_range(self, buffer_state, range_lo, range_hi): """Clears a specified range of elements from the buffer state.""" - # TODO: Dataflow runner v2 gets stuck when MIN_TIMESTAMP is used + # TODO: Dataflow Portable Runner gets stuck when MIN_TIMESTAMP is used # as the lower bound for clear_range. Investigate this further. buffer_state.clear_range(range_lo, range_hi) diff --git a/sdks/python/apache_beam/examples/kafkataxi/README.md b/sdks/python/apache_beam/examples/kafkataxi/README.md index 86924200b1f2..ae1b6b9ba39e 100644 --- a/sdks/python/apache_beam/examples/kafkataxi/README.md +++ b/sdks/python/apache_beam/examples/kafkataxi/README.md @@ -67,7 +67,7 @@ Perform Beam runner specific setup. ℹ️ Note that cross-language transforms require portable implementations of Spark/Flink/Direct runners. Dataflow requires -[runner V2](https://cloud.google.com/dataflow/docs/guides/deploying-a-pipeline#dataflow-runner-v2). +[Dataflow Portable Runner](https://cloud.google.com/dataflow/docs/guides/deploying-a-pipeline#dataflow-runner-v2). See [here](https://beam.apache.org/documentation/runners/dataflow/) for instructions for setting up Dataflow. diff --git a/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py b/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py index bc012bd7be9d..49725d54e990 100644 --- a/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py +++ b/sdks/python/apache_beam/io/external/xlang_bigqueryio_it_test.py @@ -563,7 +563,7 @@ def test_streaming_with_fixed_num_streams(self): @unittest.skip( "Streaming to the Storage Write API sink with autosharding is broken " - "with Dataflow Runner V2.") + "with Dataflow Portable Runner.") def test_streaming_with_auto_sharding(self): self.skip_if_not_dataflow_runner() table = 'streaming_with_auto_sharding' diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py b/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py index 87456124b816..8f72d45060ca 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py @@ -103,7 +103,7 @@ def _translate_step_name(self, internal_name): user_step_name = None if (self._job_graph and internal_name in self._job_graph.proto_pipeline.components.transforms.keys()): - # Dataflow Runner v2 with portable job submission uses proto transform map + # Dataflow Portable Runner with portable job submission uses proto transform map # IDs for step names. Also PTransform.unique_name maps to user step names. # Hence we lookup user step names based on the proto. user_step_name = self._job_graph.proto_pipeline.components.transforms[ diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py b/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py index 0c23e6024dc6..c6e2a7bec3d8 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py @@ -65,7 +65,7 @@ BQ_SOURCE_UW_ERROR = ( 'The Read(BigQuerySource(...)) transform is not supported with newer stack ' - 'features (Fn API, Dataflow Runner V2, etc). Please use the transform ' + 'features (Fn API, Dataflow Portable Runner, etc). Please use the transform ' 'apache_beam.io.gcp.bigquery.ReadFromBigQuery instead.') @@ -320,7 +320,7 @@ def visit_transform(self, applied_transform): raise ValueError( 'CombineFn.setup and CombineFn.teardown are ' 'not supported with non-portable Dataflow ' - 'runner. Please use Dataflow Runner V2 instead.') + 'runner. Please use Dataflow Portable Runner instead.') @staticmethod def _overrides_setup_or_teardown(combinefn): @@ -342,7 +342,7 @@ def run_pipeline(self, pipeline, options, pipeline_proto=None): """Remotely executes entire pipeline or parts reachable from node.""" if _is_runner_v2_disabled(options): raise ValueError( - 'Disabling Runner V2 no longer supported ' + 'Disabling Dataflow Portable Runner no longer supported ' 'using Beam Python %s.' % beam.version.__version__) # Label goog-dataflow-notebook if job is started from notebook. @@ -591,6 +591,8 @@ def _add_runner_v2_missing_options(options): debug_options.add_experiment('use_unified_worker') debug_options.add_experiment('use_runner_v2') debug_options.add_experiment('use_portable_job_submission') + # enable_portable_runner is not added by default as it is not documented. + # This behavior will be fixed in later versions. def _check_and_add_missing_options(options): @@ -648,8 +650,8 @@ def _check_and_add_missing_streaming_options(options): :param options: PipelineOptions for this pipeline. """ - # Streaming only supports using runner v2 (aka unified worker). - # Runner v2 only supports using streaming engine (aka windmill service) + # Streaming only supports using Dataflow Portable Runner (aka unified worker, runner v2). + # Dataflow Portable Runner only supports using streaming engine (aka windmill service) if options.view_as(StandardOptions).streaming: debug_options = options.view_as(DebugOptions) debug_options.add_experiment('enable_streaming_engine') @@ -659,9 +661,11 @@ def _check_and_add_missing_streaming_options(options): def _is_runner_v2_disabled(options): # Type: (PipelineOptions) -> bool - """Returns true if runner v2 is disabled.""" + """Returns true if Dataflow Portable Runner is disabled.""" debug_options = options.view_as(DebugOptions) return ( + debug_options.lookup_experiment('disable_portable_runner') or + debug_options.lookup_experiment('enable_streaming_java_runner') or debug_options.lookup_experiment('disable_runner_v2') or debug_options.lookup_experiment('disable_runner_v2_until_2023') or debug_options.lookup_experiment('disable_runner_v2_until_v2.50') or diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py b/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py index e1b8be6682f9..d848b5ffe595 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py @@ -41,6 +41,7 @@ from apache_beam.runners.dataflow.dataflow_runner import DataflowRuntimeException from apache_beam.runners.dataflow.dataflow_runner import _check_and_add_missing_options from apache_beam.runners.dataflow.dataflow_runner import _check_and_add_missing_streaming_options +from apache_beam.runners.dataflow.dataflow_runner import _is_runner_v2_disabled from apache_beam.runners.dataflow.internal.clients import dataflow as dataflow_api from apache_beam.runners.internal import names from apache_beam.runners.runner import PipelineState @@ -733,6 +734,25 @@ def test_explicit_streaming_no_unbounded(self): p.result.job.proto.type, apiclient.dataflow.Job.TypeValueValuesEnum.JOB_TYPE_STREAMING) + def test_runner_v2_disabled_experiments_raise(self): + disable_experiments = [ + 'disable_portable_runner', + 'enable_streaming_java_runner', + 'disable_runner_v2', + 'disable_runner_v2_until_2023', + 'disable_runner_v2_until_v2.50', + 'disable_prime_runner_v2', + ] + for experiment in disable_experiments: + options = PipelineOptions([f'--experiments={experiment}']) + self.assertTrue( + _is_runner_v2_disabled(options), + f'Expected {experiment} to disable Portable Runner') + with self.assertRaisesRegex( + ValueError, + 'Disabling Dataflow Portable Runner no longer supported.*'): + DataflowRunner().run_pipeline(None, options) + if __name__ == '__main__': unittest.main() diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 4a7c61901de3..2cf0e79731ac 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -1153,7 +1153,7 @@ def to_split_int(n): # TODO: Used in legacy batch worker. Move under MetricUpdateTranslators -# after Runner V2 transition. +# after Dataflow Portable Runner transition. def translate_distribution(distribution_update, metric_update_proto): """Translate metrics DistributionUpdate to dataflow distribution update. @@ -1174,7 +1174,7 @@ def translate_distribution(distribution_update, metric_update_proto): metric_update_proto.distribution = dist_update_proto -# TODO: Used in legacy batch worker. Delete after Runner V2 transition. +# TODO: Used in legacy batch worker. Delete after Dataflow Portable Runner transition. def translate_value(value, metric_update_proto): metric_update_proto.integer = to_split_int(value) @@ -1203,8 +1203,8 @@ def get_container_image_from_options(pipeline_options): if worker_options.sdk_container_image: return worker_options.sdk_container_image - # Legacy and runner v2 exist in different repositories. - # Set to legacy format, override if runner v2 + # Dataflow Legacy and Portable Runner exist in different repositories. + # Set to legacy format, override if Dataflow Portable Runner container_repo = names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY image_name = '{repository}/beam_python{major}.{minor}_sdk'.format( repository=container_repo, diff --git a/sdks/python/apache_beam/transforms/core.py b/sdks/python/apache_beam/transforms/core.py index 55a2cf40b64a..cd01253cade7 100644 --- a/sdks/python/apache_beam/transforms/core.py +++ b/sdks/python/apache_beam/transforms/core.py @@ -1096,7 +1096,7 @@ def setup(self, *args, **kwargs): before executing any of the other methods. The resources can then be disposed of in ``CombineFn.teardown``. - If you are using Dataflow, you need to enable Dataflow Runner V2 + If you are using Dataflow, you need to enable Dataflow Portable Runner before using this feature. Args: @@ -1194,7 +1194,7 @@ def extract_output(self, accumulator, *args, **kwargs): def teardown(self, *args, **kwargs): """Called to clean up an instance before it is discarded. - If you are using Dataflow, you need to enable Dataflow Runner V2 + If you are using Dataflow, you need to enable Dataflow Portable Runner before using this feature. Args: From b01708340402c4663a9f294188eeeb64d8266597 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 07:34:02 -0400 Subject: [PATCH 258/490] Bump github.com/aws/aws-sdk-go-v2 from 1.41.7 to 1.41.8 in /sdks (#38740) --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index d38d85ccc6a3..45bcc6cada09 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,7 +32,7 @@ require ( cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 - github.com/aws/aws-sdk-go-v2 v1.41.7 + github.com/aws/aws-sdk-go-v2 v1.41.8 github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 diff --git a/sdks/go.sum b/sdks/go.sum index 91984bb2b8a4..01e511eac4c7 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -199,8 +199,8 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= -github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2 v1.41.8 h1:sRs7nG6/RiEBZ/K5UO2sNw0w40U02Nmz1VtARloTZXk= +github.com/aws/aws-sdk-go-v2 v1.41.8/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= From dfedf56696db4f511bf30859a630dbfa48016f58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 09:03:19 -0400 Subject: [PATCH 259/490] Bump github.com/aws/aws-sdk-go-v2/credentials in /sdks (#38737) Bumps [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) from 1.19.17 to 1.19.18. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.17...credentials/v1.19.18) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/credentials dependency-version: 1.19.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 20 ++++++++++---------- sdks/go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 45bcc6cada09..921543e6142b 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -34,7 +34,7 @@ require ( cloud.google.com/go/storage v1.62.2 github.com/aws/aws-sdk-go-v2 v1.41.8 github.com/aws/aws-sdk-go-v2/config v1.32.18 - github.com/aws/aws-sdk-go-v2/credentials v1.19.17 + github.com/aws/aws-sdk-go-v2/credentials v1.19.18 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 github.com/aws/smithy-go v1.26.0 @@ -91,7 +91,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -149,17 +149,17 @@ require ( github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 01e511eac4c7..3eb7d78bb4fe 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -211,29 +211,29 @@ github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9 github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.17 h1:gP2nkGsS+KMvF/jfFz2Vv2qiiOqWKyPACSzPsqHgoW8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.17/go.mod h1:Bsew3S/moG5iT77giPj1q8wb/s0RE5/QfH+ASjYtuQc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.18 h1:GcXQz2M/0ZvMo0v5DakUqbDBeBM1ZNaivkolEF4Esgw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.18/go.mod h1:sHJ06tMGcD3ZpmMyJqV+VBsGilhSIZPIN+ZFy5Dg0C4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 h1:FQm5ApnyzkuJdXLGskPce83CK1CQKC4RUnIHKVe4BU4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24/go.mod h1:JsC7dqQc55MlZ5mvNsDMMge71u8pVcSzU3RNz2h/5yQ= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 h1:7nIPtCHSeEKE7RC32DxsI6UyDoXeHefilHwcJz1F+nk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20/go.mod h1:lKf7JyyKRi6R8Y0cSrPuTLERZRD+4KeLbPZeSAciuik= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 h1:u6kJU2i0va1AgtJsH3RdWKWqHULlTh7zHwb35Womf74= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24/go.mod h1:7GY+xLcXOFUpCkNwDReft9qOAVg54A4/AnjHIU7sSAY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 h1:Xhbcf3KugX6vX7SDyUK205Oicyfg7EGuvoVNyP5L6DM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24/go.mod h1:rwDgb2HNOGZsnTHylOUedM7Vnl+bCfnXDqUNPsFWYfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 h1:54CTMmlJ71Rk2dYvM9qZOob+39wjlVja2zDLxCu69Ew= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25/go.mod h1:BZaHqxsS9vN1fvV5EfEl0OBLOk5+AajWsMu6MjqnZB4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= @@ -244,8 +244,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 h1:tX68nPDCoX0s2k github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 h1:CQW2FTrflfoslYWLf3fv7vG28Q219+v8YJS5QTQb2+Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24/go.mod h1:Xfx13T+u3nH6EEzgl9fBSO6nDRmze1FvnZNYkctQ2zw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= @@ -256,22 +256,22 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU66 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 h1:gfPQ6do5PZTCc5n/vZUHz/G8McrNrfERGSO+iHvVbCA= github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0/go.mod h1:wO6U9egJtCtsZEHG2AAcFf1kUWDRrH0Iif6K3bVmmdE= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 h1:yQo3eZ5qFaL1sJWqs1nL6j3yPHA2/R7c6tQ4T+0IO10= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.0/go.mod h1:3Zzou41Qt/ueXfIzHvTEjDNuR5IjCUBVF01SNhrt1e8= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 h1:ApLTFdAZfDhZSiY5uskwECKHkSNNF83y2Ru2r7SezWA= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.18/go.mod h1:A9K9qx2l6nK89hp+a350FdGfRkrkH5HdiEjHbiy/Q/c= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 h1:nDARhv/oF55bcxF7rCI/4PDxOKnVXVWwDuDwCs2I2SQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 h1:4VD7TIZOGzehrgQ8vDE+1c6BQW4ErZPGY8ohZT5LXEE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1/go.mod h1:er0SFJfdV89Rit5hIJu/EXtv+qC2XMnxoksLmcUFkqM= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 h1:XKnxlM4KZH1gktcsh3zSWc7GW4KivEv/OkifmHOhCUY= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.2/go.mod h1:KJYmkQaFB3SUW2j3aBkPsxNmAb4ZsSOvbvCpuxzHJA0= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= From 405f5746f3e6c6d2a1edb8210f880d847fb30799 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 29 May 2026 15:04:58 +0200 Subject: [PATCH 260/490] fix race condition in sdb-operator (#38698) --- .github/workflows/beam_PerformanceTests_SingleStoreIO.yml | 2 +- .github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml index 8c12533e9cc5..58ac703602a9 100644 --- a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml +++ b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml @@ -83,7 +83,7 @@ jobs: kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-rbac.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-cluster-crd.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-operator.yaml - kubectl wait --for=condition=Ready pod -l name=sdb-operator --timeout=300s + kubectl rollout status deployment/sdb-operator --timeout=300s - name: Install Singlestore cluster id: install_singlestore run: | diff --git a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml index d16069b9e915..33dcc3bd3bb7 100644 --- a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml @@ -87,7 +87,7 @@ jobs: kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-rbac.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-cluster-crd.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-operator.yaml - kubectl wait --for=condition=Ready pod -l name=sdb-operator --timeout=300s + kubectl rollout status deployment/sdb-operator --timeout=300s - name: Install SingleStore cluster id: install_singlestore run: | From 21fa423cc913e5e1d163c19ef2fc7826ba282d3e Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 29 May 2026 09:49:01 -0400 Subject: [PATCH 261/490] Disable build isolation for pip by default. (#38700) Users can re-enable it by using experiment "pip_use_build_isolation". --- sdks/python/container/boot.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdks/python/container/boot.go b/sdks/python/container/boot.go index f5a37c9cca0a..82d11dc89cb2 100644 --- a/sdks/python/container/boot.go +++ b/sdks/python/container/boot.go @@ -190,12 +190,12 @@ func launchSDKProcess() error { experiments := getExperiments(options) logger.Printf(ctx, "Experiments=%v", experiments) - pipNoBuildIsolation = false - if slices.Contains(experiments, "pip_no_build_isolation") { - pipNoBuildIsolation = true - logger.Printf(ctx, "Build isolation disabled when installing packages with pip") - } else { + pipNoBuildIsolation = true + if slices.Contains(experiments, "pip_use_build_isolation") { + pipNoBuildIsolation = false logger.Printf(ctx, "Build isolation enabled when installing packages with pip") + } else { + logger.Printf(ctx, "Build isolation disabled when installing packages with pip") } // (2) Retrieve and install the staged packages. From 47d0447cba046a99c29827dbcd9cc03806b9458d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 09:57:48 -0400 Subject: [PATCH 262/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#38741) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.102.0 to 1.102.1. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.102.0...service/s3/v1.102.1) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.102.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 921543e6142b..76e6ef5195bc 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -36,7 +36,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.18 github.com/aws/aws-sdk-go-v2/credentials v1.19.18 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 - github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 github.com/aws/smithy-go v1.26.0 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 @@ -154,9 +154,9 @@ require ( github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 3eb7d78bb4fe..2854e49a9ed2 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -240,21 +240,21 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZL github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 h1:tX68nPDCoX0s2ksM7CipWP0QFw2hGDWwUdxI6+eT9ZU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17 h1:Zma31M1f9bbD/bsl6haTxupA0+z72L3l2ujKAH37zuI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17/go.mod h1:ZNHrGwBST3tZxBCTKbindx0BEdPN0Jnh7yJ7EVnktUM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 h1:CQW2FTrflfoslYWLf3fv7vG28Q219+v8YJS5QTQb2+Y= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24/go.mod h1:Xfx13T+u3nH6EEzgl9fBSO6nDRmze1FvnZNYkctQ2zw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24 h1:yPLVC8Lbsw92eepgdIZCChHRNQek5eAvAz5wS+UIpJE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24/go.mod h1:H2h39H1AivHYkozUIUYoVJGMUOvdJ4Lv9DLyUSMAjW8= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 h1:gfPQ6do5PZTCc5n/vZUHz/G8McrNrfERGSO+iHvVbCA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0/go.mod h1:wO6U9egJtCtsZEHG2AAcFf1kUWDRrH0Iif6K3bVmmdE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 h1:vttIo8BQwfnhimKRBZBBF3Y38SAIxif72B/M91m9hDk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1/go.mod h1:2qjInACJr84m/Tm4XXCcVNpejmbKy9kz7TEa6viQHSk= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 h1:yQo3eZ5qFaL1sJWqs1nL6j3yPHA2/R7c6tQ4T+0IO10= github.com/aws/aws-sdk-go-v2/service/signin v1.1.0/go.mod h1:3Zzou41Qt/ueXfIzHvTEjDNuR5IjCUBVF01SNhrt1e8= From db8232c87e3d1a73298de31efd4bb8517265f92f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 09:58:24 -0400 Subject: [PATCH 263/490] Bump github.com/aws/aws-sdk-go-v2/config in /sdks (#38738) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.18 to 1.32.19. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.18...config/v1.32.19) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.19 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 76e6ef5195bc..f85cba958ddf 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,7 +33,7 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 github.com/aws/aws-sdk-go-v2 v1.41.8 - github.com/aws/aws-sdk-go-v2/config v1.32.18 + github.com/aws/aws-sdk-go-v2/config v1.32.19 github.com/aws/aws-sdk-go-v2/credentials v1.19.18 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 diff --git a/sdks/go.sum b/sdks/go.sum index 2854e49a9ed2..4fd1d3609468 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,8 +207,8 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q= -github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY= +github.com/aws/aws-sdk-go-v2/config v1.32.19 h1:qRhIJMbevHUvIE7X4TK8N8zye5+5AhapcslPrvB+qKE= +github.com/aws/aws-sdk-go-v2/config v1.32.19/go.mod h1:RbJ24nfoya63+Mf5VI+CGCGk9vEdv28xPeii+gojRYs= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= github.com/aws/aws-sdk-go-v2/credentials v1.19.18 h1:GcXQz2M/0ZvMo0v5DakUqbDBeBM1ZNaivkolEF4Esgw= From b52960d3193b1b781319392b6ed2227f6847c55a Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 29 May 2026 16:28:18 +0200 Subject: [PATCH 264/490] Bump setup gradle to allowlisted v6.1.0 (#38745) --- .github/actions/setup-environment-action/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-environment-action/action.yml b/.github/actions/setup-environment-action/action.yml index fb5f91de4551..06633c6c7279 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -76,7 +76,7 @@ runs: distribution: 'temurin' java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-disabled: ${{ inputs.disable-cache }} validate-wrappers: false From 70a3bbcfb9be0087a0c50fc4870f71c746cc6989 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 10:54:01 -0400 Subject: [PATCH 265/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38739) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.20 to 1.22.21. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.20...feature/s3/manager/v1.22.21) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.21 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index f85cba958ddf..618c044a63df 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.8 github.com/aws/aws-sdk-go-v2/config v1.32.19 github.com/aws/aws-sdk-go-v2/credentials v1.19.18 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 github.com/aws/smithy-go v1.26.0 github.com/docker/go-connections v0.7.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 4fd1d3609468..5a30712858b9 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 h1:FQm5ApnyzkuJdXLGskPce8 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24/go.mod h1:JsC7dqQc55MlZ5mvNsDMMge71u8pVcSzU3RNz2h/5yQ= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20 h1:7nIPtCHSeEKE7RC32DxsI6UyDoXeHefilHwcJz1F+nk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.20/go.mod h1:lKf7JyyKRi6R8Y0cSrPuTLERZRD+4KeLbPZeSAciuik= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21 h1:1k9o0SJq41F/KhjJKJvKRTsDEYaWa03Ol82GkT9DooQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21/go.mod h1:kHrUWcXXLKEbC6doUmtxs3a1dDS1sYz6a8K2/ce5K4A= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 h1:u6kJU2i0va1AgtJsH3RdWKWqHULlTh7zHwb35Womf74= From ecab429d8610c8399b1fea3d7b28ab73f6cf7a66 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 29 May 2026 17:15:12 +0200 Subject: [PATCH 266/490] Implement MLTransform One-Hot Encoding benchmark pipeline (#38404) * Implement MLTransform One-Hot Encoding benchmark pipeline * added missing title --- ...m_Inference_Python_Benchmarks_Dataflow.yml | 13 + ...low_MLTransform_One_Hot_Encoding_Batch.txt | 36 +++ .test-infra/tools/refresh_looker_metrics.py | 1 + .../mltransform_one_hot_encoding.py | 266 ++++++++++++++++++ .../mltransform_one_hot_encoding_test.py | 259 +++++++++++++++++ .../mltransform_tests_requirements.txt | 29 ++ .../mltransform_one_hot_encoding_benchmark.py | 197 +++++++++++++ .../www/site/content/en/performance/_index.md | 1 + .../performance/mltransformonehot/_index.md | 42 +++ website/www/site/data/performance.yaml | 17 ++ 10 files changed, 861 insertions(+) create mode 100644 .github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt create mode 100644 sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding.py create mode 100644 sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding_test.py create mode 100644 sdks/python/apache_beam/ml/transforms/mltransform_tests_requirements.txt create mode 100644 sdks/python/apache_beam/testing/benchmarks/inference/mltransform_one_hot_encoding_benchmark.py create mode 100644 website/www/site/content/en/performance/mltransformonehot/_index.md diff --git a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml index 36e962bc2bab..ae52cbb68e17 100644 --- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml @@ -95,6 +95,7 @@ jobs: ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Batch.txt ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Stream.txt ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt # The env variables are created and populated in the test-arguments-action as "_test_arguments_" - name: get current time run: echo "NOW_UTC=$(date '+%m%d%H%M%S' --utc)" >> $GITHUB_ENV @@ -226,3 +227,15 @@ jobs: -PpythonVersion=3.10 \ -PloadTest.requirementsTxtFile=apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt \ '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --job_name=benchmark-tests-mltransform-generate-vocab-batch-${{env.NOW_UTC}}' + - name: run MLTransform One-Hot Encoding Batch + uses: ./.github/actions/gradle-command-self-hosted-action + timeout-minutes: 180 + with: + gradle-command: :sdks:python:apache_beam:testing:load_tests:run + arguments: | + -PloadTest.mainClass=apache_beam.testing.benchmarks.inference.mltransform_one_hot_encoding_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PbeamPythonExtra=ml_test \ + -PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_tests_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --autoscaling_algorithm=NONE --metrics_table=mltransform_one_hot_encoding_batch --influx_measurement=mltransform_one_hot_encoding_batch --job_name=benchmark-tests-mltransform-one-hot-encoding-batch-${{env.NOW_UTC}} --output_file=gs://temp-storage-for-end-to-end-tests/mltransform/one_hot_output_${{env.NOW_UTC}} --artifact_location=gs://temp-storage-for-end-to-end-tests/mltransform/artifacts_${{env.NOW_UTC}}' \ No newline at end of file diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt new file mode 100644 index 000000000000..27648d0c0fb0 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--region=us-central1 +--machine_type=n1-standard-2 +--num_workers=50 +--disk_size_gb=50 +--autoscaling_algorithm=NONE +--staging_location=gs://temp-storage-for-perf-tests/loadtests +--temp_location=gs://temp-storage-for-perf-tests/loadtests +--sdk_location=container +--requirements_file=apache_beam/ml/transforms/mltransform_tests_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--metrics_table=mltransform_one_hot_encoding_batch +--input_options={} +--influx_measurement=mltransform_one_hot_encoding_batch +# Note: output_file and artifact_location are set in the workflow with unique timestamps +--input_file=gs://apache-beam-ml/testing/inputs/sentences_50k.txt +--input_format=text +--categorical_columns=category +--num_records=1000000 +--runner=DataflowRunner diff --git a/.test-infra/tools/refresh_looker_metrics.py b/.test-infra/tools/refresh_looker_metrics.py index 35122d5afe0a..c8d66f4a4bd3 100644 --- a/.test-infra/tools/refresh_looker_metrics.py +++ b/.test-infra/tools/refresh_looker_metrics.py @@ -46,6 +46,7 @@ ("96", ["270", "304", "305", "353", "354"]), # Table Row Inference Sklearn Batch ("106", ["355", "356", "357", "358", "359"]), # Table Row Inference Sklearn Streaming ("107", ["360", "361", "362", "363", "364"]), # MLTransform Generate Vocab Batch + ("108", ["365", "366", "367", "368", "369"]) # MLTransform One-Hot Encoding Batch ] def get_look(id: str) -> models.Look: diff --git a/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding.py b/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding.py new file mode 100644 index 000000000000..5db03c31f79a --- /dev/null +++ b/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding.py @@ -0,0 +1,266 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Categorical encoding pipeline using MLTransform for batch processing. + +This pipeline demonstrates MLTransform's ComputeAndApplyVocabulary transform +for categorical feature encoding. It can either read input data from a file +or generate synthetic test data, computes vocabulary on categorical columns, +and converts categorical values to integer indices. + +Example usage with input file: + python mltransform_one_hot_encoding.py \ + --input_file=gs://bucket/input.jsonl \ + --output_file=gs://bucket/output.jsonl \ + --artifact_location=gs://bucket/artifacts \ + --categorical_columns=category \ + --runner=DataflowRunner \ + --project=PROJECT \ + --region=us-central1 \ + --temp_location=gs://bucket/temp + +Example usage with synthetic data: + python mltransform_one_hot_encoding.py \ + --output_file=gs://bucket/output.jsonl \ + --categorical_columns=category \ + --num_records=100000 \ + --runner=DataflowRunner \ + --project=PROJECT \ + --region=us-central1 +""" + +import argparse +import json +import logging +import tempfile +from typing import Any + +import apache_beam as beam +from apache_beam.ml.transforms.base import MLTransform +from apache_beam.ml.transforms.tft import ComputeAndApplyVocabulary +from apache_beam.runners.runner import PipelineResult + + +def parse_json_line(line: str) -> dict[str, Any]: + """Parse a JSON line into a dictionary.""" + try: + return json.loads(line) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON line: {line[:200]}...") from e + + +def parse_text_line(line: str, + categorical_columns: list[str]) -> dict[str, Any]: + """Parse plain text line into the first categorical column.""" + text_value = line.strip() + if not text_value: + text_value = 'unknown' + return {categorical_columns[0]: text_value} + + +def format_json_output(element: Any) -> str: + """Format output element as JSON string.""" + def to_json_compatible(value: Any) -> Any: + """Recursively convert non-JSON types (e.g. numpy arrays/scalars).""" + if isinstance(value, dict): + return {k: to_json_compatible(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [to_json_compatible(v) for v in value] + + # MLTransform outputs may include numpy scalar/ndarray values. + if hasattr(value, 'tolist'): + return to_json_compatible(value.tolist()) + if hasattr(value, 'item'): + try: + return to_json_compatible(value.item()) + except (TypeError, ValueError): + pass + return value + + if hasattr(element, 'as_dict'): + return json.dumps(to_json_compatible(element.as_dict())) + if hasattr(element, '_asdict'): + return json.dumps(to_json_compatible(element._asdict())) + return json.dumps(to_json_compatible(dict(element))) + + +def generate_synthetic_record(index: int, + categorical_columns: list[str]) -> dict[str, str]: + """Generate a deterministic synthetic record with categorical values.""" + categories = [ + 'electronics', + 'clothing', + 'food', + 'books', + 'sports', + 'home', + 'toys', + 'health', + 'automotive', + 'garden' + ] + colors = [ + 'red', + 'blue', + 'green', + 'yellow', + 'black', + 'white', + 'purple', + 'orange', + 'pink', + 'gray' + ] + sizes = ['small', 'medium', 'large', 'xlarge', 'tiny', 'huge'] + + record = {} + for col in categorical_columns: + if col.lower() in ['category', 'type', 'product']: + record[col] = categories[index % len(categories)] + elif col.lower() in ['color', 'colour']: + record[col] = colors[index % len(colors)] + elif col.lower() in ['size', 'dimension']: + record[col] = sizes[index % len(sizes)] + else: + # Default to categories for unknown columns + record[col] = categories[index % len(categories)] + return record + + +def run( + argv=None, + save_main_session=True, + test_pipeline=None) -> PipelineResult | None: + """Run the categorical encoding pipeline.""" + known_args, pipeline_args = parse_known_args(argv) + + categorical_columns = [ + col.strip() for col in known_args.categorical_columns.split(',') + ] + + if not categorical_columns or not categorical_columns[0]: + raise ValueError("At least one categorical column must be specified") + + if not known_args.output_file: + raise ValueError("--output_file is required") + + # Create artifact location if not provided + artifact_location = known_args.artifact_location + if not artifact_location: + artifact_location = tempfile.mkdtemp() + logging.info("Using temporary artifact location: %s", artifact_location) + + pipeline_options = beam.options.pipeline_options.PipelineOptions( + pipeline_args) + pipeline_options.view_as( + beam.options.pipeline_options.SetupOptions + ).save_main_session = save_main_session + + pipeline = test_pipeline or beam.Pipeline(options=pipeline_options) + + # Use synthetic data or read from file + if known_args.input_file: + # Read and parse input data from file + if known_args.input_format == 'jsonl': + parse_input_fn = parse_json_line + else: + if len(categorical_columns) > 1: + logging.warning( + 'Input format is "text" but multiple categorical columns are ' + 'specified. Only the first column "%s" will be used for parsing.', + categorical_columns[0]) + parse_input_fn = lambda line: parse_text_line(line, categorical_columns) + raw_data = ( + pipeline + | 'ReadFromJSONL' >> beam.io.ReadFromText(known_args.input_file) + | 'ParseInput' >> beam.Map(parse_input_fn)) + else: + # Generate synthetic data + num_records = known_args.num_records or 100000 + logging.info("Generating %d synthetic records", num_records) + + raw_data = ( + pipeline + | 'GenerateSyntheticIndexes' >> beam.Create(range(num_records)) + | 'BuildSyntheticRecord' >> beam.Map( + lambda idx: generate_synthetic_record(idx, categorical_columns))) + + # Build MLTransform with ComputeAndApplyVocabulary + ml_transform = MLTransform( + write_artifact_location=artifact_location, + ).with_transform( + ComputeAndApplyVocabulary( + columns=categorical_columns, vocab_filename='vocab_onehot')) + + # Apply MLTransform + transformed_data = ( + raw_data + | 'ValidateAndFilterColumns' >> beam.Filter( + lambda element: all(col in element for col in categorical_columns)) + | 'MLTransform' >> ml_transform + | 'FormatOutput' >> beam.Map(format_json_output)) + + # Write output + _ = ( + transformed_data + | 'WriteToJSONL' >> beam.io.WriteToText( + known_args.output_file, file_name_suffix='.jsonl')) + + result = pipeline.run() + return result + + +def parse_known_args(argv): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description='Categorical encoding pipeline using MLTransform') + + parser.add_argument( + '--input_file', + help='Input JSONL file path (local or GCS). ' + 'If not provided, synthetic data will be generated.') + parser.add_argument( + '--input_format', + choices=['jsonl', 'text'], + default='jsonl', + help='Input file format for --input_file. Use jsonl for JSON lines ' + 'or text for plain text lines (default: jsonl).') + parser.add_argument( + '--output_file', + required=True, + help='Output file prefix for encoded results (JSONL format)') + parser.add_argument( + '--artifact_location', + help='GCS or local path to store MLTransform artifacts ' + '(vocabulary files). If not provided, a temp location is used.') + parser.add_argument( + '--categorical_columns', + required=True, + help='Comma-separated list of categorical column names to encode') + parser.add_argument( + '--num_records', + type=int, + default=100000, + help='Number of synthetic records to generate if --input_file is not ' + 'provided (default: 100000)') + + return parser.parse_known_args(argv) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + run() diff --git a/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding_test.py b/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding_test.py new file mode 100644 index 000000000000..e072a367dc35 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding_test.py @@ -0,0 +1,259 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Tests for mltransform_one_hot_encoding pipeline.""" + +import json +import logging +import os +import tempfile +import unittest +from glob import glob +from typing import Any + +import pytest + +try: + from apache_beam.examples.ml_transform import mltransform_one_hot_encoding + from apache_beam.testing.test_pipeline import TestPipeline +except ImportError: # pylint: disable=bare-except + raise unittest.SkipTest('tensorflow_transform is not installed.') + + +def create_test_input_data() -> list[dict[str, Any]]: + """Create sample test data for one-hot encoding.""" + return [ + { + 'category': 'electronics', 'color': 'red', 'size': 'small' + }, + { + 'category': 'clothing', 'color': 'blue', 'size': 'medium' + }, + { + 'category': 'electronics', 'color': 'green', 'size': 'large' + }, + { + 'category': 'food', 'color': 'red', 'size': 'small' + }, + { + 'category': 'clothing', 'color': 'blue', 'size': 'medium' + }, + ] + + +class OneHotEncodingPipelineTest(unittest.TestCase): + """Unit and integration tests for one-hot encoding pipeline.""" + def setUp(self): + """Set up test fixtures.""" + self.test_dir = tempfile.mkdtemp() + self.input_file = os.path.join(self.test_dir, 'input.jsonl') + self.output_prefix = os.path.join(self.test_dir, 'output') + self.artifact_location = os.path.join(self.test_dir, 'artifacts') + + # Create test input file + test_data = create_test_input_data() + with open(self.input_file, 'w', encoding='utf-8') as f: + for record in test_data: + f.write(json.dumps(record) + '\n') + + def tearDown(self): + """Clean up test fixtures.""" + import shutil + shutil.rmtree(self.test_dir, ignore_errors=True) + + def test_parse_json_line_valid(self): + """Test parsing valid JSON lines.""" + line = '{"category": "electronics", "color": "red"}' + result = mltransform_one_hot_encoding.parse_json_line(line) + self.assertEqual(result['category'], 'electronics') + self.assertEqual(result['color'], 'red') + + def test_parse_json_line_invalid(self): + """Test parsing invalid JSON lines raises ValueError.""" + with self.assertRaises(ValueError): + mltransform_one_hot_encoding.parse_json_line('not valid json') + + def test_format_json_output_with_row(self): + """Test formatting beam.Row output as JSON.""" + import apache_beam as beam + row = beam.Row(category='test', value=123) + result = mltransform_one_hot_encoding.format_json_output(row) + parsed = json.loads(result) + self.assertEqual(parsed['category'], 'test') + self.assertEqual(parsed['value'], 123) + + def test_format_json_output_with_dict(self): + """Test formatting dict output as JSON.""" + element = {'category': 'test', 'value': 123} + result = mltransform_one_hot_encoding.format_json_output(element) + parsed = json.loads(result) + self.assertEqual(parsed['category'], 'test') + self.assertEqual(parsed['value'], 123) + + @pytest.mark.uses_tft + def test_end_to_end_pipeline_local(self): + """Integration test running the full pipeline locally.""" + extra_opts = { + 'input_file': self.input_file, + 'output_file': self.output_prefix, + 'artifact_location': self.artifact_location, + 'categorical_columns': 'category,color,size', + } + + with TestPipeline() as pipeline: + mltransform_one_hot_encoding.run( + argv=pipeline.get_full_options_as_args(**extra_opts), + test_pipeline=pipeline) + + # Verify output shards exist. + output_files = glob(self.output_prefix + '*.jsonl') + self.assertTrue( + output_files, f"Output files not found for: {self.output_prefix}") + + # Verify output content + lines = [] + for output_file in output_files: + with open(output_file, 'r', encoding='utf-8') as f: + lines.extend(line.strip() for line in f if line.strip()) + + self.assertEqual(len(lines), 5) + + # Parse and verify structure + for line in lines: + record = json.loads(line) + # Should have original columns plus one-hot encoded versions + self.assertIn('category', record) + self.assertIn('color', record) + self.assertIn('size', record) + + @pytest.mark.uses_tft + def test_pipeline_with_missing_columns(self): + """Test pipeline handles records with missing columns gracefully.""" + # Create input with some missing columns + mixed_data = [ + { + 'category': 'electronics', 'color': 'red', 'size': 'small' + }, + { + 'category': 'clothing', 'color': 'blue' + }, # missing size + { + 'category': 'food' + }, # missing color and size + ] + + input_file = os.path.join(self.test_dir, 'mixed_input.jsonl') + with open(input_file, 'w', encoding='utf-8') as f: + for record in mixed_data: + f.write(json.dumps(record) + '\n') + + extra_opts = { + 'input_file': input_file, + 'output_file': os.path.join(self.test_dir, 'mixed_output'), + 'artifact_location': os.path.join(self.test_dir, 'mixed_artifacts'), + 'categorical_columns': 'category,color,size', + } + + with TestPipeline() as pipeline: + mltransform_one_hot_encoding.run( + argv=pipeline.get_full_options_as_args(**extra_opts), + test_pipeline=pipeline) + + # Only first record should be processed + output_files = glob(os.path.join(self.test_dir, 'mixed_output*.jsonl')) + lines = [] + for output_file in output_files: + with open(output_file, 'r', encoding='utf-8') as f: + lines.extend(line.strip() for line in f if line.strip()) + + self.assertEqual(len(lines), 1) + record = json.loads(lines[0]) + self.assertEqual(record['category'], 'electronics') + + def test_cli_synthetic_data_no_input(self): + """Test pipeline works without input file using synthetic data.""" + # Should not raise error when input_file is missing (uses synthetic data) + with tempfile.TemporaryDirectory() as tmpdir: + output_file = os.path.join(tmpdir, 'output') + artifact_location = os.path.join(tmpdir, 'artifacts') + + with TestPipeline() as pipeline: + # Should work without input_file (uses synthetic data) + mltransform_one_hot_encoding.run( + argv=pipeline.get_full_options_as_args( + output_file=output_file, + artifact_location=artifact_location, + categorical_columns='category', + num_records=100), + test_pipeline=pipeline) + + def test_cli_validation_missing_output(self): + """Test CLI argument validation for missing output file.""" + with self.assertRaises(ValueError) as context: + mltransform_one_hot_encoding.run( + argv=['--input_file=/tmp/in.jsonl', '--categorical_columns=category']) + self.assertIn('output_file', str(context.exception).lower()) + + def test_cli_validation_empty_columns(self): + """Test CLI argument validation for empty columns.""" + with self.assertRaises(ValueError) as context: + mltransform_one_hot_encoding.run( + argv=[ + '--input_file=/tmp/in.jsonl', + '--output_file=/tmp/out.jsonl', + '--categorical_columns=' + ]) + self.assertIn('categorical', str(context.exception).lower()) + + +class OneHotEncodingCLITest(unittest.TestCase): + """Tests for CLI argument handling.""" + def test_parse_known_args_basic(self): + """Test basic argument parsing.""" + args, _ = mltransform_one_hot_encoding.parse_known_args([ + '--input_file=/tmp/in.jsonl', + '--output_file=/tmp/out.jsonl', + '--categorical_columns=category,color', + ]) + self.assertEqual(args.input_file, '/tmp/in.jsonl') + self.assertEqual(args.output_file, '/tmp/out.jsonl') + self.assertEqual(args.categorical_columns, 'category,color') + + def test_parse_known_args_with_artifact(self): + """Test argument parsing with artifact location.""" + args, _ = mltransform_one_hot_encoding.parse_known_args([ + '--input_file=gs://bucket/in.jsonl', + '--output_file=gs://bucket/out', + '--artifact_location=gs://bucket/artifacts', + '--categorical_columns=size,color', + ]) + self.assertEqual(args.artifact_location, 'gs://bucket/artifacts') + + def test_parse_known_args_multiple_columns(self): + """Test parsing multiple categorical columns.""" + args, _ = mltransform_one_hot_encoding.parse_known_args([ + '--input_file=in.jsonl', + '--output_file=out.jsonl', + '--categorical_columns=col1,col2,col3,col4', + ]) + columns = [c.strip() for c in args.categorical_columns.split(',')] + self.assertEqual(columns, ['col1', 'col2', 'col3', 'col4']) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + unittest.main() diff --git a/sdks/python/apache_beam/ml/transforms/mltransform_tests_requirements.txt b/sdks/python/apache_beam/ml/transforms/mltransform_tests_requirements.txt new file mode 100644 index 000000000000..9f37e070a606 --- /dev/null +++ b/sdks/python/apache_beam/ml/transforms/mltransform_tests_requirements.txt @@ -0,0 +1,29 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Requirements for MLTransform tests on Dataflow workers. +# Keep this aligned with CloudML benchmark stack to avoid worker import errors. +dill==0.4.1 +tfx_bsl==1.16.1 +tensorflow-transform==1.16.0 +tensorflow>=2.16,<2.17 +numpy>=1.22.0,<2.0 +tensorflow-metadata>=1.16.1,<1.17.0 +pyarrow>=10,<11 +tensorflow-serving-api>=2.16.1,<2.20 +tf-keras>=2.16.0,<2.17 +google-cloud-monitoring>=2.27.0 diff --git a/sdks/python/apache_beam/testing/benchmarks/inference/mltransform_one_hot_encoding_benchmark.py b/sdks/python/apache_beam/testing/benchmarks/inference/mltransform_one_hot_encoding_benchmark.py new file mode 100644 index 000000000000..e80fca633352 --- /dev/null +++ b/sdks/python/apache_beam/testing/benchmarks/inference/mltransform_one_hot_encoding_benchmark.py @@ -0,0 +1,197 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pytype: skip-file + +"""Benchmark test for MLTransform One-Hot Encoding pipeline. + +This benchmark measures the performance of MLTransform for one-hot encoding +categorical features, including throughput, latency, and cost metrics on +Dataflow. +""" + +import logging + +from google.cloud import monitoring_v3 +from google.protobuf.duration_pb2 import Duration + +from apache_beam.examples.ml_transform import mltransform_one_hot_encoding +from apache_beam.options.pipeline_options import DebugOptions +from apache_beam.options.pipeline_options import GoogleCloudOptions +from apache_beam.options.pipeline_options import SetupOptions +from apache_beam.options.pipeline_options import StandardOptions +from apache_beam.options.pipeline_options import WorkerOptions +from apache_beam.testing.load_tests.dataflow_cost_benchmark import DataflowCostBenchmark +from apache_beam.testing.load_tests.load_test import LoadTestOptions + + +class MLTransformOneHotEncodingOptions( + LoadTestOptions, + StandardOptions, + GoogleCloudOptions, + WorkerOptions, + DebugOptions, + SetupOptions, +): + """Pipeline options for MLTransform One-Hot Encoding benchmark.""" + @classmethod + def _add_argparse_args(cls, parser): + parser.add_argument( + '--input_file', + default='', + help='Input JSONL/text file path for benchmark data.') + parser.add_argument( + '--input_format', + choices=['jsonl', 'text'], + default='jsonl', + help='Input file format for input_file: jsonl or text.') + parser.add_argument( + '--output_file', + required=True, + help='Output file path for encoded results') + parser.add_argument( + '--artifact_location', + required=True, + help='GCS path to store MLTransform artifacts') + parser.add_argument( + '--categorical_columns', + default='category', + help='Comma-separated list of categorical column names to encode') + parser.add_argument( + '--num_records', + type=int, + default=100000, + help='Number of synthetic records to generate') + + +class MLTransformOneHotEncodingBenchmarkTest(DataflowCostBenchmark): + """Benchmark for MLTransform One-Hot Encoding on Dataflow. + + This benchmark measures: + - Throughput: Elements processed per second + - Latency: Time to process input records + - Cost: Estimated cost on Dataflow + + The pcollection is chosen to capture the output of the MLTransform + step where one-hot encoding is applied. + """ + options_class = MLTransformOneHotEncodingOptions + + def __init__(self): + self.metrics_namespace = 'BeamML_MLTransform' + # Use the output of MLTransform step for throughput measurement + # This captures the processed data after vocabulary encoding + super().__init__( + metrics_namespace=self.metrics_namespace, + is_streaming=False, + pcollection='FormatOutput.out0') + + def test(self): + """Execute the one-hot encoding pipeline for benchmarking.""" + extra_opts = {} + + extra_opts['output_file'] = self.pipeline.get_option('output_file') + extra_opts['artifact_location'] = self.pipeline.get_option( + 'artifact_location') + extra_opts['categorical_columns'] = ( + self.pipeline.get_option('categorical_columns') or 'category') + + input_file = self.pipeline.get_option('input_file') + if input_file: + extra_opts['input_file'] = input_file + extra_opts['input_format'] = ( + self.pipeline.get_option('input_format') or 'jsonl') + else: + # Handle synthetic data generation + num_records = self.pipeline.get_option('num_records') + if num_records: + extra_opts['num_records'] = int(num_records) + + self.result = mltransform_one_hot_encoding.run( + self.pipeline.get_full_options_as_args(**extra_opts), + test_pipeline=self.pipeline) + + def _get_throughput_metrics( + self, + project: str, + job_id: str, + start_time: str, + end_time: str, + pcollection_name: str | None = None, + ) -> dict[str, float]: + """Get throughput metrics with runner-v2-friendly fallbacks.""" + candidate_pcollections = [] + if pcollection_name: + candidate_pcollections.append(pcollection_name) + candidate_pcollections.extend([ + self.pcollection, + 'MLTransform.out0', + 'FormatOutput.out0', + ]) + + # Deduplicate while preserving order. + seen = set() + unique_candidates = [] + for name in candidate_pcollections: + if name and name not in seen: + seen.add(name) + unique_candidates.append(name) + + for name in unique_candidates: + metrics = super()._get_throughput_metrics( + project, job_id, start_time, end_time, pcollection_name=name) + if (metrics.get('AvgThroughputBytes', 0) > 0 or + metrics.get('AvgThroughputElements', 0) > 0): + return metrics + + # Final fallback: aggregate job-level throughput without pcollection label. + interval = monitoring_v3.TimeInterval( + start_time=start_time, end_time=end_time) + aggregation = monitoring_v3.Aggregation( + alignment_period=Duration(seconds=60), + per_series_aligner=monitoring_v3.Aggregation.Aligner.ALIGN_MEAN) + requests = { + "Bytes": monitoring_v3.ListTimeSeriesRequest( + name=f"projects/{project}", + filter=( + 'metric.type="dataflow.googleapis.com/job/estimated_byte_count" ' + f'AND metric.labels.job_id="{job_id}"'), + interval=interval, + aggregation=aggregation), + "Elements": monitoring_v3.ListTimeSeriesRequest( + name=f"projects/{project}", + filter=( + 'metric.type="dataflow.googleapis.com/job/element_count" ' + f'AND metric.labels.job_id="{job_id}"'), + interval=interval, + aggregation=aggregation), + } + + fallback_metrics = {} + for key, req in requests.items(): + time_series = self.monitoring_client.list_time_series(request=req) + values = [ + point.value.double_value for series in time_series + for point in series.points + ] + fallback_metrics[f"AvgThroughput{key}"] = ( + sum(values) / len(values) if values else 0.0) + return fallback_metrics + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + MLTransformOneHotEncodingBenchmarkTest().run() diff --git a/website/www/site/content/en/performance/_index.md b/website/www/site/content/en/performance/_index.md index 350524a3c86d..a0eaba2aa0ec 100644 --- a/website/www/site/content/en/performance/_index.md +++ b/website/www/site/content/en/performance/_index.md @@ -60,3 +60,4 @@ See the following pages for performance measures recorded when running various B - [VLLM Gemma Batch Completion Tesla T4 GPU](/performance/vllmgemmabatchtesla) - [Table Row Inference Sklearn Batch](/performance/tablerowinference) - [MLTransform Generate Vocab (batch)](/performance/mltransformvocab) +- [MLTransform One-Hot Encoding](/performance/mltransformonehot) diff --git a/website/www/site/content/en/performance/mltransformonehot/_index.md b/website/www/site/content/en/performance/mltransformonehot/_index.md new file mode 100644 index 000000000000..2d641bcfb66f --- /dev/null +++ b/website/www/site/content/en/performance/mltransformonehot/_index.md @@ -0,0 +1,42 @@ +--- +title: "MLTransform One-Hot Encoding Performance" +--- + + + +# MLTransform One-Hot Encoding Performance + +**Pipeline**: MLTransform One-Hot Encoding for Categorical Features +**Type**: Batch only +**Host**: 50 × n1-standard-2 (2 vCPUs, 7.5 GB RAM) + +The following graphs show various metrics when running the MLTransform One-Hot +Encoding pipeline using Apache Beam's MLTransform TFT integration. +See the [glossary](/performance/glossary) for definitions. + +Full pipeline implementation is available +[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="mltransformonehot" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="mltransformonehot" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="mltransformonehot" read_or_write="write" section="date" >}} diff --git a/website/www/site/data/performance.yaml b/website/www/site/data/performance.yaml index 9725a4af99a9..42fb5bee92f5 100644 --- a/website/www/site/data/performance.yaml +++ b/website/www/site/data/performance.yaml @@ -299,3 +299,20 @@ looks: title: AvgThroughputBytesPerSec by Version - id: cC75NnCbQT3mQmKVHtDxzptXpwPb64qz title: AvgThroughputElementsPerSec by Version + mltransformonehot: + write: + folder: 108 + cost: + - id: 37DYwfbr5y4gt7g7g7RzRSzGTjd4Jjbj + title: RunTime and EstimatedCost + date: + - id: trcXrBCyPjYj2jTGc3px3d72xMXPCmZb + title: AvgThroughputBytesPerSec by Date + - id: yczZ4G8rYcP3SHjtQXz7p4BdRhGcXydx + title: AvgThroughputElementsPerSec by Date + version: + - id: mHtwwXKndpJVPjnkcHqRZpfPzfzHtT38 + title: AvgThroughputBytesPerSec by Version + - id: Cn5FsXkdy2ZXCxCJshSCxcjsTW3TXf3c + title: AvgThroughputElementsPerSec by Version + From ba0549dcd99a62f19238ee6287a581fbcfc9970f Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Fri, 29 May 2026 13:43:10 -0400 Subject: [PATCH 267/490] [yaml] - mongodb write normalization (#38376) * MongoDB IO write connector for beam yaml * revert bigtable change * add yaml and more transformer write support * more edits to support parity between java and python * remove read * remove updateConfiguration * remove updateField * update external transforms * remove unnecessary comments * add clarifying comments * fix gemini comments * address gemini comments * address reviewer comments * Updated to 1024 batch size * remove duplicate function due to rebasing --------- Co-authored-by: Arnav Arora --- sdks/java/io/expansion-service/build.gradle | 1 + .../beam/sdk/io/mongodb/MongoDbUtils.java | 74 ++++++++ ...goDbWriteSchemaTransformConfiguration.java | 85 +++++++++ .../MongoDbWriteSchemaTransformProvider.java | 168 ++++++++++++++++++ .../beam/sdk/io/mongodb/MongoDbUtilsTest.java | 135 ++++++++++++++ ...ngoDbWriteSchemaTransformProviderTest.java | 166 +++++++++++++++++ .../apache_beam/yaml/integration_tests.py | 50 ++++++ sdks/python/apache_beam/yaml/standard_io.yaml | 19 ++ .../apache_beam/yaml/tests/mongodb.yaml | 44 +++++ sdks/python/apache_beam/yaml/yaml_io.py | 48 +++++ sdks/standard_external_transforms.yaml | 32 +++- 11 files changed, 821 insertions(+), 1 deletion(-) create mode 100644 sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java create mode 100644 sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java create mode 100644 sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java create mode 100644 sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java create mode 100644 sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java create mode 100644 sdks/python/apache_beam/yaml/tests/mongodb.yaml diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 1045ad4aeed0..32894b978094 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -96,6 +96,7 @@ dependencies { runtimeOnly project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") runtimeOnly library.java.bigdataoss_util_hadoop + runtimeOnly project(":sdks:java:io:mongodb") runtimeOnly library.java.kafka_clients runtimeOnly library.java.slf4j_jdk14 diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java new file mode 100644 index 000000000000..a5acfb1d19fe --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.schemas.Schema.Field; +import org.apache.beam.sdk.values.Row; +import org.bson.BsonNull; +import org.bson.Document; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Utility methods for MongoDB IO. */ +public class MongoDbUtils { + + /** Converts a Beam {@link Row} to a BSON {@link Document}. */ + public static Document toDocument(Row row) { + Object converted = convertToBsonValue(row); + if (converted instanceof Document) { + return (Document) converted; + } + throw new IllegalArgumentException( + "Expected Document but got " + + (converted != null ? converted.getClass().getName() : "null")); + } + + private static @Nullable Object convertToBsonValue(@Nullable Object value) { + if (value == null) { + return new BsonNull(); + } + if (value instanceof Row) { + Row row = (Row) value; + Document doc = new Document(); + for (Field field : row.getSchema().getFields()) { + Object fieldValue = row.getValue(field.getName()); + Object converted = convertToBsonValue(fieldValue); + doc.append(field.getName(), converted != null ? converted : new BsonNull()); + } + return doc; + } else if (value instanceof Iterable) { + List bsonList = new ArrayList<>(); + for (Object item : (Iterable) value) { + Object converted = convertToBsonValue(item); + bsonList.add(converted != null ? converted : new BsonNull()); + } + return bsonList; + } else if (value instanceof Map) { + Map map = (Map) value; + Document doc = new Document(); + for (Map.Entry entry : map.entrySet()) { + Object converted = convertToBsonValue(entry.getValue()); + doc.append(String.valueOf(entry.getKey()), converted != null ? converted : new BsonNull()); + } + return doc; + } + return value; + } +} diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java new file mode 100644 index 000000000000..275cfe880014 --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Configuration class for the MongoDB Write transform. */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class MongoDbWriteSchemaTransformConfiguration implements Serializable { + + @SchemaFieldDescription("The connection URI for the MongoDB server.") + public abstract String getUri(); + + @SchemaFieldDescription("The MongoDB database to write to.") + public abstract String getDatabase(); + + @SchemaFieldDescription("The MongoDB collection to write to.") + public abstract String getCollection(); + + @SchemaFieldDescription("The number of documents to include in each batch write.") + @Nullable + public abstract Long getBatchSize(); + + @SchemaFieldDescription( + "This option specifies whether and where to output unwritable rows. Note: Error handling is currently limited to data conversion failures before sending to the MongoDB driver, as the underlying MongoDbIO does not yet support dead-letter queues for write failures.") + @Nullable + public abstract ErrorHandling getErrorHandling(); + + public void validate() { + checkArgument(getUri() != null && !getUri().isEmpty(), "MongoDB URI must be specified."); + checkArgument( + getDatabase() != null && !getDatabase().isEmpty(), "MongoDB database must be specified."); + checkArgument( + getCollection() != null && !getCollection().isEmpty(), + "MongoDB collection must be specified."); + + Long batchSize = getBatchSize(); + if (batchSize != null) { + checkArgument(batchSize > 0, "Batch size must be positive."); + } + } + + public static Builder builder() { + return new AutoValue_MongoDbWriteSchemaTransformConfiguration.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setUri(String uri); + + public abstract Builder setDatabase(String database); + + public abstract Builder setCollection(String collection); + + public abstract Builder setBatchSize(Long batchSize); + + public abstract Builder setErrorHandling(ErrorHandling errorHandling); + + public abstract MongoDbWriteSchemaTransformConfiguration build(); + } +} diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java new file mode 100644 index 000000000000..dde265ce2b81 --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import com.google.auto.service.AutoService; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import org.apache.beam.sdk.coders.AtomicCoder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; +import org.bson.Document; + +/** An implementation of {@link TypedSchemaTransformProvider} for writing to MongoDB. */ +@AutoService(SchemaTransformProvider.class) +public class MongoDbWriteSchemaTransformProvider + extends TypedSchemaTransformProvider { + + public static class DocumentCoder extends AtomicCoder implements Serializable { + private static final DocumentCoder INSTANCE = new DocumentCoder(); + + private DocumentCoder() {} + + public static DocumentCoder of() { + return INSTANCE; + } + + @Override + public void encode(Document value, OutputStream outStream) throws java.io.IOException { + StringUtf8Coder.of().encode(value.toJson(), outStream); + } + + @Override + public Document decode(InputStream inStream) throws java.io.IOException { + String json = StringUtf8Coder.of().decode(inStream); + return Document.parse(json); + } + } + + private static final String INPUT_TAG = "input"; + public static final TupleTag OUTPUT_TAG = new TupleTag() {}; + public static final TupleTag ERROR_TAG = new TupleTag() {}; + + private static final org.apache.beam.sdk.metrics.Counter errorCounter = + org.apache.beam.sdk.metrics.Metrics.counter( + MongoDbWriteSchemaTransformProvider.class, "MongoDB-write-error-counter"); + + @Override + protected SchemaTransform from(MongoDbWriteSchemaTransformConfiguration configuration) { + return new MongoDbWriteSchemaTransform(configuration); + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mongodb_write:v1"; + } + + @Override + public List inputCollectionNames() { + return Collections.singletonList(INPUT_TAG); + } + + /** The {@link SchemaTransform} that performs the write operation. */ + private static class MongoDbWriteSchemaTransform extends SchemaTransform { + private final MongoDbWriteSchemaTransformConfiguration configuration; + + MongoDbWriteSchemaTransform(MongoDbWriteSchemaTransformConfiguration configuration) { + configuration.validate(); + this.configuration = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + // Retrieve the input PCollection of Rows and its schema. + PCollection rows = input.get(INPUT_TAG); + org.apache.beam.sdk.schemas.Schema inputSchema = rows.getSchema(); + + // Determine if error handling is enabled and set up the error schema. + boolean handleErrors = ErrorHandling.hasOutput(configuration.getErrorHandling()); + org.apache.beam.sdk.schemas.Schema errorSchema = ErrorHandling.errorSchema(inputSchema); + + // Convert Beam Rows to BSON Documents, emitting errors to a separate tag if enabled. + PCollectionTuple outputTuple = + rows.apply( + "ConvertToDocument", + ParDo.of(new RowToBsonDocumentFn(handleErrors, errorSchema)) + .withOutputTags(OUTPUT_TAG, TupleTagList.of(ERROR_TAG))); + + PCollection documents = outputTuple.get(OUTPUT_TAG).setCoder(DocumentCoder.of()); + + // Configure the MongoDB write operation. + MongoDbIO.Write write = + MongoDbIO.write() + .withUri(configuration.getUri()) + .withDatabase(configuration.getDatabase()) + .withCollection(configuration.getCollection()); + + Long batchSize = configuration.getBatchSize(); + if (batchSize != null) { + write = write.withBatchSize(batchSize); + } + + // Apply the MongoDB write transform. + documents.apply("WriteToMongo", write); + + // Extract and format the error collection. + PCollection errorOutput = outputTuple.get(ERROR_TAG).setRowSchema(errorSchema); + + // Return the error collection as specified by the configuration. + ErrorHandling errorHandling = configuration.getErrorHandling(); + return PCollectionRowTuple.of( + (handleErrors && errorHandling != null) ? errorHandling.getOutput() : "errors", + errorOutput); + } + } + + /** Converts a Beam {@link Row} to a BSON {@link Document}. */ + static class RowToBsonDocumentFn extends DoFn { + private final boolean handleErrors; + private final org.apache.beam.sdk.schemas.Schema errorSchema; + + RowToBsonDocumentFn(boolean handleErrors, org.apache.beam.sdk.schemas.Schema errorSchema) { + this.handleErrors = handleErrors; + this.errorSchema = errorSchema; + } + + @ProcessElement + public void processElement(@Element Row row, MultiOutputReceiver receiver) { + try { + receiver.get(OUTPUT_TAG).output(MongoDbUtils.toDocument(row)); + } catch (Exception e) { + if (!handleErrors) { + throw new RuntimeException(e); + } + errorCounter.inc(); + receiver.get(ERROR_TAG).output(ErrorHandling.errorRecord(errorSchema, row, e)); + } + } + } +} diff --git a/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java new file mode 100644 index 000000000000..ec11bb865913 --- /dev/null +++ b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.values.Row; +import org.bson.BsonNull; +import org.bson.Document; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link MongoDbUtils}. */ +@RunWith(JUnit4.class) +public class MongoDbUtilsTest { + + @Test + public void testToDocumentWithSimplePrimitives() { + Schema schema = + Schema.builder() + .addStringField("stringField") + .addInt32Field("intField") + .addBooleanField("booleanField") + .addDoubleField("doubleField") + .build(); + + Row row = Row.withSchema(schema).addValues("hello", 42, true, 3.14).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + assertEquals("hello", doc.get("stringField")); + assertEquals(42, doc.get("intField")); + assertEquals(true, doc.get("booleanField")); + assertEquals(3.14, doc.get("doubleField")); + } + + @Test + public void testToDocumentWithNestedRow() { + Schema nestedSchema = + Schema.builder().addStringField("nestedString").addInt32Field("nestedInt").build(); + + Schema parentSchema = + Schema.builder() + .addStringField("parentString") + .addRowField("nestedRow", nestedSchema) + .build(); + + Row nestedRow = Row.withSchema(nestedSchema).addValues("nestedValue", 100).build(); + Row parentRow = Row.withSchema(parentSchema).addValues("parentValue", nestedRow).build(); + + Document doc = MongoDbUtils.toDocument(parentRow); + + assertNotNull(doc); + assertEquals("parentValue", doc.get("parentString")); + + Object nestedObj = doc.get("nestedRow"); + assertTrue(nestedObj instanceof Document); + Document nestedDoc = (Document) nestedObj; + assertEquals("nestedValue", nestedDoc.get("nestedString")); + assertEquals(100, nestedDoc.get("nestedInt")); + } + + @Test + public void testToDocumentWithIterable() { + Schema schema = Schema.builder().addArrayField("listField", FieldType.STRING).build(); + + Row row = Row.withSchema(schema).addValue(Arrays.asList("a", "b", "c")).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + Object listObj = doc.get("listField"); + assertTrue(listObj instanceof List); + List list = (List) listObj; + assertEquals(3, list.size()); + assertEquals("a", list.get(0)); + assertEquals("b", list.get(1)); + assertEquals("c", list.get(2)); + } + + @Test + public void testToDocumentWithMap() { + Schema schema = + Schema.builder().addMapField("mapField", FieldType.STRING, FieldType.INT32).build(); + + Map map = Collections.singletonMap("key", 42); + Row row = Row.withSchema(schema).addValue(map).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + Object mapObj = doc.get("mapField"); + assertTrue(mapObj instanceof Document); + Document mapDoc = (Document) mapObj; + assertEquals(42, mapDoc.get("key")); + } + + @Test + public void testToDocumentWithNullValues() { + Schema schema = Schema.builder().addNullableField("nullableString", FieldType.STRING).build(); + + Row row = Row.withSchema(schema).addValue(null).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + Object val = doc.get("nullableString"); + assertTrue(val instanceof BsonNull); + } +} diff --git a/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java new file mode 100644 index 000000000000..864c516617e5 --- /dev/null +++ b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.util.Collections; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TupleTagList; +import org.bson.Document; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link MongoDbWriteSchemaTransformProvider}. */ +@RunWith(JUnit4.class) +public class MongoDbWriteSchemaTransformProviderTest { + + @Rule public transient TestPipeline p = TestPipeline.create(); + + @Test + public void testInvalidConfigMissingUri() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setDatabase("db") + .setCollection("col") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingDatabase() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setCollection("col") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingCollection() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigNegativeBatchSize() { + assertThrows( + IllegalArgumentException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .setCollection("col") + .setBatchSize(-1L) + .build() + .validate(); + }); + } + + @Test + public void testConfigurationSchema() throws Exception { + Schema schema = + SchemaRegistry.createDefault().getSchema(MongoDbWriteSchemaTransformConfiguration.class); + + // We expect 5 fields now (uri, database, collection, batchSize, errorHandling) + assertEquals(5, schema.getFieldCount()); + assertNotNull(schema.getField("uri")); + assertNotNull(schema.getField("database")); + assertNotNull(schema.getField("collection")); + assertNotNull(schema.getField("batchSize")); + assertNotNull(schema.getField("errorHandling")); + } + + @Test + public void testRowToBsonDocumentFn() { + Schema beamSchema = + Schema.builder() + .addStringField("name") + .addInt32Field("age") + .addNullableStringField("country") + .build(); + + Row row = + Row.withSchema(beamSchema) + .withFieldValue("name", "John") + .withFieldValue("age", 30) + .withFieldValue("country", null) + .build(); + + PCollection inputRows = + p.apply(Create.of(Collections.singletonList(row))).setRowSchema(beamSchema); + + Schema errorSchema = ErrorHandling.errorSchema(beamSchema); + PCollectionTuple outputTuple = + inputRows.apply( + "ConvertToDocument", + ParDo.of( + new MongoDbWriteSchemaTransformProvider.RowToBsonDocumentFn(false, errorSchema)) + .withOutputTags( + MongoDbWriteSchemaTransformProvider.OUTPUT_TAG, + TupleTagList.of(MongoDbWriteSchemaTransformProvider.ERROR_TAG))); + + PCollection bsonDocuments = + outputTuple + .get(MongoDbWriteSchemaTransformProvider.OUTPUT_TAG) + .setCoder(MongoDbWriteSchemaTransformProvider.DocumentCoder.of()); + + outputTuple.get(MongoDbWriteSchemaTransformProvider.ERROR_TAG).setRowSchema(errorSchema); + + PAssert.that(bsonDocuments) + .satisfies( + documents -> { + Document doc = documents.iterator().next(); + assertEquals("John", doc.get("name")); + assertEquals(30, doc.get("age")); + // The RowToBsonDocumentFn retains nulls explicitly in the BSON document + assertEquals(true, doc.containsKey("country")); + assertEquals(null, doc.get("country")); + return null; + }); + + p.run().waitUntilFinish(); + } +} diff --git a/sdks/python/apache_beam/yaml/integration_tests.py b/sdks/python/apache_beam/yaml/integration_tests.py index 7aad41e0607a..2d0b2787fc9b 100644 --- a/sdks/python/apache_beam/yaml/integration_tests.py +++ b/sdks/python/apache_beam/yaml/integration_tests.py @@ -45,6 +45,7 @@ from testcontainers.core.waiting_utils import wait_for_logs from testcontainers.google import PubSubContainer from testcontainers.kafka import KafkaContainer +from testcontainers.mongodb import MongoDbContainer from testcontainers.mssql import SqlServerContainer from testcontainers.mysql import MySqlContainer from testcontainers.postgres import PostgresContainer @@ -62,6 +63,8 @@ _LOGGER = logging.getLogger(__name__) +_MONGO_CONTAINER_IMAGE = 'mongo:7.0.7' + @contextlib.contextmanager def gcs_temp_dir(bucket): @@ -200,6 +203,53 @@ def temp_bigtable_table(project, prefix='yaml_bt_it_'): _LOGGER.warning("Failed to clean up instance") +@contextlib.contextmanager +def temp_mongodb_table(): + """ + provides a temporary MongoDB instance. + + starts a MongoDB container, creates a unique database + and collection name for test isolation, and yields them as a dictionary. + + This allows YAML test files to get connection details without hardcoding them. + Example usage in a YAML test file's fixture section: + + fixtures: + - name: mongo_vars + type: path.to.this.file.mongodb_fixture + + Then, in the pipeline definition, you can use placeholders like: + - uri: ${mongo_vars.URI} + - database: ${mongo_vars.DATABASE} + - collection: ${mongo_vars.COLLECTION} + """ + _LOGGER.info("Setting up MongoDB fixture...") + mongo_container = MongoDbContainer(_MONGO_CONTAINER_IMAGE) + try: + mongo_container.start() + mongo_uri = mongo_container.get_connection_url() + + db_name = f'db_{uuid.uuid4().hex}' + collection_name = f'collection_{uuid.uuid4().hex}' + + _LOGGER.info( + "MongoDB container started. URI: [%s], DB: [%s], Collection: [%s]", + mongo_uri, + db_name, + collection_name) + + yield { + 'URI': mongo_uri, + 'DATABASE': db_name, + 'COLLECTION': collection_name, + } + + finally: + _LOGGER.info("Tearing down MongoDB fixture...") + mongo_container.stop() + _LOGGER.info("MongoDB container stopped.") + + @contextlib.contextmanager def temp_sqlite_database(prefix='yaml_jdbc_it_'): """Context manager to provide a temporary SQLite database via JDBC for diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index a48c3accff43..781d3de193ec 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -115,6 +115,7 @@ 'WriteToIceberg': 'apache_beam.yaml.yaml_io.write_to_iceberg' 'ReadFromTFRecord': 'apache_beam.yaml.yaml_io.read_from_tfrecord' 'WriteToTFRecord': 'apache_beam.yaml.yaml_io.write_to_tfrecord' + 'WriteToMongoDB': 'apache_beam.yaml.yaml_io.write_to_mongodb' # General File Formats @@ -429,3 +430,21 @@ config: gradle_target: 'sdks:java:io:expansion-service:shadowJar' +#MongoDB +- type: renaming + transforms: + 'WriteToMongoDB': 'WriteToMongoDB' + config: + mappings: + 'WriteToMongoDB': + connection_uri: "uri" + database: "database" + collection: "collection" + batch_size: "batch_size" + error_handling: "error_handling" + underlying_provider: + type: beamJar + transforms: + 'WriteToMongoDB': 'beam:schematransform:org.apache.beam:mongodb_write:v1' + config: + gradle_target: 'sdks:java:io:expansion-service:shadowJar' diff --git a/sdks/python/apache_beam/yaml/tests/mongodb.yaml b/sdks/python/apache_beam/yaml/tests/mongodb.yaml new file mode 100644 index 000000000000..efce73c89879 --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/mongodb.yaml @@ -0,0 +1,44 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +fixtures: + - name: mongo_vars + type: "apache_beam.yaml.integration_tests.temp_mongodb_table" + +pipelines: + - pipeline: + type: composite + transforms: + - type: Create + name: CreateData + config: + elements: + - { id: 1, name: "John" } + - { id: 2, name: "Jane" } + - type: WriteToMongoDB + name: WriteData + input: CreateData + config: + connection_uri: '{mongo_vars[URI]}' + database: '{mongo_vars[DATABASE]}' + collection: '{mongo_vars[COLLECTION]}' + error_handling: + output: my_error_output + - type: AssertEqual + input: WriteData.my_error_output + config: + elements: [] diff --git a/sdks/python/apache_beam/yaml/yaml_io.py b/sdks/python/apache_beam/yaml/yaml_io.py index f59e730ac815..eaa0d4317507 100644 --- a/sdks/python/apache_beam/yaml/yaml_io.py +++ b/sdks/python/apache_beam/yaml/yaml_io.py @@ -29,6 +29,7 @@ from collections.abc import Mapping from typing import Any from typing import Optional +from typing import Union import fastavro @@ -725,6 +726,53 @@ def write_to_tfrecord( compression_type=getattr(CompressionTypes, compression_type)) +@beam.ptransform_fn +@yaml_errors.maybe_with_exception_handling_transform_fn +def write_to_mongodb( + pcoll, + *, + database: str, + collection: str, + connection_uri: Optional[str] = None, + batch_size: int = 1024, + extra_client_params: Optional[Mapping[str, Any]] = None): + """Writes data to MongoDB. + + Args: + pcoll: The input PCollection of Beam Rows. + database: The MongoDB database name. + collection: The MongoDB collection name. + connection_uri: The MongoDB connection string. e.g. "mongodb://localhost:27017" + batch_size: Number of documents per bulk_write to MongoDB. + extra_client_params: Optional MongoClient parameters. + """ + from apache_beam.io import mongodbio + + def row_to_dict(value): + if value is None: + return None + if hasattr(value, '_asdict'): + return {k: row_to_dict(v) for k, v in value._asdict().items()} + elif hasattr(value, 'as_dict'): + return {k: row_to_dict(v) for k, v in value.as_dict().items()} + elif isinstance(value, (list, tuple)): + return [row_to_dict(v) for v in value] + elif isinstance(value, Mapping): + return {k: row_to_dict(v) for k, v in value.items()} + else: + return value + + return ( + pcoll + | beam.Map(row_to_dict) + | mongodbio.WriteToMongoDB( + uri=connection_uri, + db=database, + coll=collection, + batch_size=batch_size, + extra_client_params=extra_client_params)) + + @beam.ptransform_fn def match_all( pcoll, diff --git a/sdks/standard_external_transforms.yaml b/sdks/standard_external_transforms.yaml index 1c536ce319d2..057c4e3f47d1 100644 --- a/sdks/standard_external_transforms.yaml +++ b/sdks/standard_external_transforms.yaml @@ -19,7 +19,7 @@ # configuration in /sdks/standard_expansion_services.yaml. # Refer to gen_xlang_wrappers.py for more info. # -# Last updated on: 2025-06-05 +# Last updated on: 2026-05-06 - default_service: sdks:java:io:expansion-service:shadowJar description: 'Outputs a PCollection of Beam Rows, each containing a single INT64 @@ -50,6 +50,36 @@ type: int64 identifier: beam:schematransform:org.apache.beam:generate_sequence:v1 name: GenerateSequence +- default_service: sdks:java:io:expansion-service:shadowJar + description: '' + destinations: + python: apache_beam/io + fields: + - description: The number of documents to include in each batch write. + name: batch_size + nullable: true + type: int64 + - description: The MongoDB collection to write to. + name: collection + nullable: false + type: str + - description: The MongoDB database to write to. + name: database + nullable: false + type: str + - description: 'This option specifies whether and where to output unwritable rows. + Note: Error handling is currently limited to data conversion failures before + sending to the MongoDB driver, as the underlying MongoDbIO does not yet support + dead-letter queues for write failures.' + name: error_handling + nullable: true + type: Row(output=) + - description: The connection URI for the MongoDB server. + name: uri + nullable: false + type: str + identifier: beam:schematransform:org.apache.beam:mongodb_write:v1 + name: MongodbWrite - default_service: sdks:java:io:expansion-service:shadowJar description: '' destinations: From cfea98ab0c004b852f00a3d5f9d6facaec3e41eb Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 29 May 2026 14:23:59 -0400 Subject: [PATCH 268/490] Disable gRPC fork support in PortableRunnerTestWithSubprocesses (#38744) Avoids deadline exceeded failures in subprocess tests. Redundant setup and teardown in the multi-worker subclass are removed to inherit them cleanly. --- .../portability/portable_runner_test.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sdks/python/apache_beam/runners/portability/portable_runner_test.py b/sdks/python/apache_beam/runners/portability/portable_runner_test.py index a1ed7448f050..e781e13ce663 100644 --- a/sdks/python/apache_beam/runners/portability/portable_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/portable_runner_test.py @@ -295,6 +295,22 @@ def create_options(self): class PortableRunnerTestWithSubprocesses(PortableRunnerTest): _use_subprocesses = True + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + @classmethod + def setUpClass(cls): + cls._old_fork_support = os.environ.get('GRPC_ENABLE_FORK_SUPPORT') + os.environ['GRPC_ENABLE_FORK_SUPPORT'] = 'false' + super().setUpClass() + + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + @classmethod + def tearDownClass(cls): + if cls._old_fork_support is None: + os.environ.pop('GRPC_ENABLE_FORK_SUPPORT', None) + else: + os.environ['GRPC_ENABLE_FORK_SUPPORT'] = cls._old_fork_support + super().tearDownClass() + def create_options(self): options = super().create_options() options.view_as(PortableOptions).environment_type = ( @@ -329,22 +345,6 @@ class PortableRunnerTestWithSubprocessesAndMultiWorkers( PortableRunnerTestWithSubprocesses): _use_subprocesses = True - # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. - @classmethod - def setUpClass(cls): - cls._old_fork_support = os.environ.get('GRPC_ENABLE_FORK_SUPPORT') - os.environ['GRPC_ENABLE_FORK_SUPPORT'] = 'false' - super().setUpClass() - - # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. - @classmethod - def tearDownClass(cls): - if cls._old_fork_support is None: - os.environ.pop('GRPC_ENABLE_FORK_SUPPORT', None) - else: - os.environ['GRPC_ENABLE_FORK_SUPPORT'] = cls._old_fork_support - super().tearDownClass() - def create_options(self): options = super() \ .create_options() From 41bfbf7bff1d97b07629b02911ef261b207d6c40 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 29 May 2026 14:52:38 -0400 Subject: [PATCH 269/490] Fix flaky TestDataSampler/GetSamplesForPCollectionsTooManySamples (#38736) Since GetSamples is a destructive operation that clears stored samples, polling it in a loop is flaky. If it is called before all asynchronous samples have finished processing, it consumes the early samples and clears them, causing verifySampledElements to fail. In this PR, we wait for the sampler to finish before reading to ensure all elements are successfully captured. --- .../pkg/beam/core/runtime/exec/datasampler_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/sdks/go/pkg/beam/core/runtime/exec/datasampler_test.go b/sdks/go/pkg/beam/core/runtime/exec/datasampler_test.go index 7dce38360afa..29ae3a48b5e5 100644 --- a/sdks/go/pkg/beam/core/runtime/exec/datasampler_test.go +++ b/sdks/go/pkg/beam/core/runtime/exec/datasampler_test.go @@ -104,16 +104,9 @@ func TestDataSampler(t *testing.T) { for _, sample := range test.samples { dataSampler.SendSample(sample.PCollectionID, sample.Element, sample.Timestamp) } - var samplesCount = -1 - var samples map[string][]*DataSample - for i := 0; i < 5; i++ { - samples = dataSampler.GetSamples(test.pids) - if len(samples) == len(test.want) { - samplesCount = len(samples) - break - } - time.Sleep(time.Second) - } + time.Sleep(1 * time.Second) + samples := dataSampler.GetSamples(test.pids) + samplesCount := len(samples) cancel() if samplesCount != len(test.want) { t.Errorf("got an unexpected number of sampled elements: %v, want: %v", samplesCount, len(test.want)) From 28178cdbb389b4eb983770efa9e890da049f9cb5 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Fri, 29 May 2026 15:08:05 -0400 Subject: [PATCH 270/490] Re-enable call-args check in yaml_io.py (#38730) * Re-enable call-args check in yaml_io.py * yapf --- sdks/python/apache_beam/yaml/yaml_io.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdks/python/apache_beam/yaml/yaml_io.py b/sdks/python/apache_beam/yaml/yaml_io.py index eaa0d4317507..989661a6eae4 100644 --- a/sdks/python/apache_beam/yaml/yaml_io.py +++ b/sdks/python/apache_beam/yaml/yaml_io.py @@ -247,11 +247,9 @@ def _validate_schema(): _validate_schema() beam_schema = avroio.avro_schema_to_beam_schema(schema) covert_to_row = avroio.avro_dict_to_beam_row(schema, beam_schema) - # pylint: disable=line-too-long return ( - beam_schema, - lambda record: covert_to_row( - fastavro.schemaless_reader(io.BytesIO(record), schema))) # type: ignore[call-arg] + beam_schema, lambda record: covert_to_row( + fastavro.schemaless_reader(io.BytesIO(record), schema))) elif format == 'PROTO': _validate_schema() beam_schema = json_utils.json_schema_to_beam_schema(schema) From 2a658f36efe3dc83006e8a90c4ea166014b3cc19 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 29 May 2026 17:56:06 -0400 Subject: [PATCH 271/490] Fix Reshuffle handling in Prism to recursively remove nested sub-transforms (#38735) Updates `handleReshuffle` in the Go Prism runner to recursively clean up all nested sub-transforms of a Reshuffle composite transform using `removeSubTransforms`. This prevents nested sub-transforms from being processed in subsequent graph optimization stages. --- .../runners/prism/internal/handlerunner.go | 4 +- .../prism/internal/handlerunner_test.go | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 sdks/go/pkg/beam/runners/prism/internal/handlerunner_test.go diff --git a/sdks/go/pkg/beam/runners/prism/internal/handlerunner.go b/sdks/go/pkg/beam/runners/prism/internal/handlerunner.go index 3ac0d98850df..ab7d505f8c60 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/handlerunner.go +++ b/sdks/go/pkg/beam/runners/prism/internal/handlerunner.go @@ -209,8 +209,8 @@ func (h *runner) handleReshuffle(tid string, t *pipepb.PTransform, comps *pipepb } } - // And all the sub transforms. - toRemove = append(toRemove, t.GetSubtransforms()...) + // Also recursively remove all sub-transforms. + toRemove = append(toRemove, removeSubTransforms(comps, t.GetSubtransforms())...) // Return the new components which is the transforms consumer return prepareResult{ diff --git a/sdks/go/pkg/beam/runners/prism/internal/handlerunner_test.go b/sdks/go/pkg/beam/runners/prism/internal/handlerunner_test.go new file mode 100644 index 000000000000..da3ac9d14f09 --- /dev/null +++ b/sdks/go/pkg/beam/runners/prism/internal/handlerunner_test.go @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "testing" + + pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestHandleReshuffle(t *testing.T) { + h := &runner{ + config: RunnerCharacteristic{ + SDKReshuffle: false, + }, + } + + comps := &pipepb.Components{ + Transforms: map[string]*pipepb.PTransform{ + "reshuffle": { + UniqueName: "reshuffle", + Inputs: map[string]string{ + "in": "pcol_in", + }, + Outputs: map[string]string{ + "out": "pcol_out", + }, + Subtransforms: []string{"sub_1"}, + }, + "sub_1": { + UniqueName: "sub_1", + Subtransforms: []string{"sub_2"}, + }, + "sub_2": { + UniqueName: "sub_2", + }, + "consumer": { + UniqueName: "consumer", + Inputs: map[string]string{ + "in": "pcol_out", + }, + }, + }, + } + + got := h.handleReshuffle("reshuffle", comps.GetTransforms()["reshuffle"], comps) + + want := prepareResult{ + SubbedComps: nil, + RemovedLeaves: []string{"sub_1", "sub_2"}, + ForcedRoots: []string{"consumer"}, + } + + if d := cmp.Diff(want, got, protocmp.Transform()); d != "" { + t.Errorf("handleReshuffle() diff (-want, +got):\n%v", d) + } + + // Verify that the consumer's input has been remapped to the input of the reshuffle + gotInput := comps.GetTransforms()["consumer"].GetInputs()["in"] + if gotInput != "pcol_in" { + t.Errorf("consumer input got %q, want %q", gotInput, "pcol_in") + } +} From 857bb10b86be535f041bfa7f3a786e5dd7ce78bd Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Sat, 30 May 2026 14:34:10 -0400 Subject: [PATCH 272/490] Hugging face model handler #3 (#38696) --- .../beam_PostCommit_Yaml_Xlang_Direct.json | 2 +- .../yaml/tests/runinference_huggingface.yaml | 62 +++++++++++++++++++ ...erence.yaml => runinference_vertexai.yaml} | 0 sdks/python/apache_beam/yaml/yaml_ml.py | 49 +++++++++++++++ sdks/python/setup.py | 3 +- 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml rename sdks/python/apache_beam/yaml/tests/{runinference.yaml => runinference_vertexai.yaml} (100%) diff --git a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json index 541dc4ea8e87..8ed972c9f579 100644 --- a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 2 + "revision": 3 } diff --git a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml new file mode 100644 index 000000000000..8728a6f544ad --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +pipelines: + - pipeline: + type: chain + transforms: + - type: Create + config: + elements: + - text: "I love Apache Beam!" + - text: "I hate this error." + - type: RunInference + config: + model_handler: + type: "HuggingFacePipeline" + config: + task: "text-classification" + inference_fn: + callable: | + def real_inference(batch, pipeline, inference_args): + predictions = pipeline(batch, **inference_args) + + # If it's a single dictionary (batch size of 1), wrap it in a list + if isinstance(predictions, dict): + predictions = [predictions] + + return { + 'label': [p['label'] for p in predictions], + 'score': [p['score'] for p in predictions] + } + preprocess: + callable: 'lambda x: x.text' + - type: MapToFields + config: + language: python + fields: + text: text + sentiment: + callable: 'lambda x: x.inference.inference["label"]' + - type: AssertEqual + config: + elements: + - text: "I love Apache Beam!" + sentiment: "POSITIVE" + - text: "I hate this error." + sentiment: "NEGATIVE" + + options: + yaml_experimental_features: ['ML'] diff --git a/sdks/python/apache_beam/yaml/tests/runinference.yaml b/sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml similarity index 100% rename from sdks/python/apache_beam/yaml/tests/runinference.yaml rename to sdks/python/apache_beam/yaml/tests/runinference_vertexai.yaml diff --git a/sdks/python/apache_beam/yaml/yaml_ml.py b/sdks/python/apache_beam/yaml/yaml_ml.py index 51f18c733046..188530180c46 100644 --- a/sdks/python/apache_beam/yaml/yaml_ml.py +++ b/sdks/python/apache_beam/yaml/yaml_ml.py @@ -282,6 +282,55 @@ def inference_output_type(self): ('model_id', Optional[str])]) +@ModelHandlerProvider.register_handler_type('HuggingFacePipeline') +class HuggingFacePipelineProvider(ModelHandlerProvider): + def __init__( + self, + task: Optional[str] = None, + model: Optional[str] = None, + preprocess: Optional[dict[str, str]] = None, + postprocess: Optional[dict[str, str]] = None, + device: Optional[Any] = None, + inference_fn: Optional[dict[str, str]] = None, + load_pipeline_args: Optional[dict[str, Any]] = None, + **kwargs): + try: + from apache_beam.ml.inference.huggingface_inference import HuggingFacePipelineModelHandler + except ImportError: + raise ValueError( + 'Unable to import HuggingFacePipelineModelHandler. Please ' + 'install transformers dependencies.') + + kwargs = {k: v for k, v in kwargs.items() if not k.startswith('_')} + + inference_fn_obj = self.parse_processing_transform( + inference_fn, 'inference_fn') if inference_fn else None + + handler_kwargs = {} + if inference_fn_obj: + handler_kwargs['inference_fn'] = inference_fn_obj + + _handler = HuggingFacePipelineModelHandler( + task=task, + model=model, + device=device, + load_pipeline_args=load_pipeline_args, + **handler_kwargs, + **kwargs) + + super().__init__(_handler, preprocess, postprocess) + + @staticmethod + def validate(config): + if not config or (not config.get('task') and not config.get('model')): + raise ValueError( + "HuggingFacePipeline requires either 'task' or " + "'model' to be specified.") + + def inference_output_type(self): + return Any + + @beam.ptransform.ptransform_fn def run_inference( pcoll, diff --git a/sdks/python/setup.py b/sdks/python/setup.py index 4c1384c31517..dbdef30aef9d 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -654,7 +654,8 @@ def get_portability_package_data(): 'transformers': [ 'transformers>=4.28.0,<4.56.0', 'tensorflow>=2.12.0', - 'torch>=1.9.0' + # Avoid torch 2.12.0+ which fails to run unit tests with segfault + 'torch>=1.9.0,<2.12.0' ], 'ml_cpu': [ 'tensorflow>=2.12.0', From 8c74d8447b4bba7fbb14ec3474cb9a0afbf022f8 Mon Sep 17 00:00:00 2001 From: Goutam Adwant <8672451+goutamadwant@users.noreply.github.com> Date: Sat, 30 May 2026 12:10:37 -0700 Subject: [PATCH 273/490] Handle empty batches in Python worker counters (#38748) * Handle empty batches in Python worker counters * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../apache_beam/runners/worker/opcounters.py | 2 ++ .../runners/worker/opcounters_test.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/sdks/python/apache_beam/runners/worker/opcounters.py b/sdks/python/apache_beam/runners/worker/opcounters.py index 66a369ba3bbb..6f5bb60762cf 100644 --- a/sdks/python/apache_beam/runners/worker/opcounters.py +++ b/sdks/python/apache_beam/runners/worker/opcounters.py @@ -217,6 +217,8 @@ def update_from_batch(self, windowed_batch): batch_length = self.producer_batch_converter.get_length( windowed_batch.values) self.element_counter.update(batch_length) + if batch_length == 0: + return mean_element_size = self.producer_batch_converter.estimate_byte_size( windowed_batch.values) / batch_length diff --git a/sdks/python/apache_beam/runners/worker/opcounters_test.py b/sdks/python/apache_beam/runners/worker/opcounters_test.py index 83b345f47c9c..aef66a725f95 100644 --- a/sdks/python/apache_beam/runners/worker/opcounters_test.py +++ b/sdks/python/apache_beam/runners/worker/opcounters_test.py @@ -184,6 +184,22 @@ def test_update_batch(self): self.verify_counters(opcounts, 200, size_per_element) + def test_update_empty_batch(self): + opcounts = OperationCounters( + CounterFactory(), + 'some-name', + coders.FastPrimitivesCoder(), + 0, + producer_batch_converter=typehints.batch.BatchConverter.from_typehints( + element_type=typehints.Any, + batch_type=typehints.List[typehints.Any])) + + self.verify_counters(opcounts, 0, math.nan) + + opcounts.update_from_batch(GlobalWindows.windowed_batch([])) + + self.verify_counters(opcounts, 0, math.nan) + def test_should_sample(self): # Order of magnitude more buckets than highest constant in code under test. buckets = [0] * 300 From 57309755630a6953ed2ecd8b16c8d3cc06b87c68 Mon Sep 17 00:00:00 2001 From: Michael Gruschke <56450401+MichaelGruschke@users.noreply.github.com> Date: Sun, 31 May 2026 12:44:45 +0200 Subject: [PATCH 274/490] feat(ml): add qdrant ingestion (#38142) * feat(ml): add qdrant ingestion refactor: use local qdrant implementation for tests chore: clean up imports chore: add qdrant dependency to ml_test extra chore: run precommit chore: add comment to CHANGES.md fix: guard against import error fix: import * fix: typing * chore: add docstring * chore: move change to 2.74.0 release notes * fix: move batch initialization for qdrant sink into start_bundle * feat: add byte size limit for qdrant ingestion * fix: add positive batch_size check for qdrant config * feat: add factory methods for qdrant connection params * feat: add retry to qdrant ingestion * chore: add unit tests * fix: import linting errors on qdrant * fix: use direct runner for qdrant integration tests * fix: safe guard qdrant client close * fix: use testcontainers for qdrant it test * chore: fix import order * fix: move test client creation into setup * chore: mark qdrant integration tests as require_docker_in_docker * chore: move changes msg to 2.75 --- CHANGES.md | 3 +- .../apache_beam/ml/rag/ingestion/qdrant.py | 328 ++++++++++++ .../ml/rag/ingestion/qdrant_it_test.py | 326 ++++++++++++ .../ml/rag/ingestion/qdrant_test.py | 480 ++++++++++++++++++ sdks/python/setup.py | 12 +- 5 files changed, 1143 insertions(+), 6 deletions(-) create mode 100644 sdks/python/apache_beam/ml/rag/ingestion/qdrant.py create mode 100644 sdks/python/apache_beam/ml/rag/ingestion/qdrant_it_test.py create mode 100644 sdks/python/apache_beam/ml/rag/ingestion/qdrant_test.py diff --git a/CHANGES.md b/CHANGES.md index 74209bb7499c..3b4af42daf8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,6 +61,7 @@ * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). * New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). +* (Python) Added [Qdrant](https://qdrant.tech/) VectorDatabaseWriteConfig implementation ([#38141](https://github.com/apache/beam/issues/38141)). ## I/Os @@ -2462,4 +2463,4 @@ Schema Options, it will be removed in version `2.23.0`. ([BEAM-9704](https://iss ## Highlights -- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). \ No newline at end of file +- For versions 2.19.0 and older release notes are available on [Apache Beam Blog](https://beam.apache.org/blog/). diff --git a/sdks/python/apache_beam/ml/rag/ingestion/qdrant.py b/sdks/python/apache_beam/ml/rag/ingestion/qdrant.py new file mode 100644 index 000000000000..abe9efc56cbf --- /dev/null +++ b/sdks/python/apache_beam/ml/rag/ingestion/qdrant.py @@ -0,0 +1,328 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import time +from collections.abc import Callable +from dataclasses import dataclass +from dataclasses import field +from typing import Any +from typing import Optional + +import grpc +from objsize import get_deep_size + +try: + from qdrant_client import QdrantClient + from qdrant_client import models + from qdrant_client.common.client_exceptions import ResourceExhaustedResponse + from qdrant_client.http.exceptions import ResponseHandlingException + from qdrant_client.http.exceptions import UnexpectedResponse +except ImportError: + logging.warning("Qdrant client library is not installed.") + +import apache_beam as beam +from apache_beam.ml.rag.ingestion.base import VectorDatabaseWriteConfig +from apache_beam.ml.rag.types import EmbeddableItem + +DEFAULT_WRITE_BATCH_SIZE = 1000 +DEFAULT_MAX_BATCH_BYTE_SIZE = 4 << 20 + + +@dataclass +class QdrantConnectionParameters: + """Configuration parameters for connecting to Qdrant service. + + Either `location`, `url`, `host`, or `path` must be provided to establish + a connection. + + Args: + location: + If `str` - use it as a `url` parameter. + If `None` - use default values for `host` and `port`. + url: either host or str of "//:/". + Default: `None` + port: Port of the REST API interface. Default: 6333 + grpc_port: Port of the gRPC interface. Default: 6334 + prefer_grpc: If `true` - use gPRC interface whenever possible. + https: If `true` - use HTTPS(SSL) protocol. Default: `None` + api_key: API key for authentication in Qdrant Cloud. Default: `None` + prefix: + If not `None` - add `prefix` to the REST URL path. + Example: `service/v1` will result in + `http://localhost:6333/service/v1/{qdrant-endpoint}` for REST API. + Default: `None` + timeout: + Timeout for REST and gRPC API requests. + Default: 5 seconds for REST and unlimited for gRPC + host: + Host name of Qdrant service. + If url and host are None, set to 'localhost'. + Default: `None` + path: Persistence path for QdrantLocal. Default: `None` + **kwargs: Additional arguments passed directly into client initialization + """ + + location: Optional[str] = None + url: Optional[str] = None + port: Optional[int] = 6333 + grpc_port: int = 6334 + prefer_grpc: bool = False + https: Optional[bool] = None + api_key: Optional[str] = None + prefix: Optional[str] = None + timeout: Optional[int] = None + host: Optional[str] = None + path: Optional[str] = None + kwargs: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self): + if not (self.location or self.url or self.host or self.path): + raise ValueError( + "One of location, url, host, or path must be provided for Qdrant") + + @classmethod + def for_cloud( + cls, + url: str, + api_key: str, + *, + prefer_grpc: bool = False, + timeout: Optional[int] = None, + **kwargs: Any, + ) -> "QdrantConnectionParameters": + """Connect to Qdrant Cloud. Requires the cluster URL and an API key.""" + return cls( + url=url, + api_key=api_key, + https=True, + prefer_grpc=prefer_grpc, + timeout=timeout, + kwargs=kwargs, + ) + + @classmethod + def for_host( + cls, + host: str, + port: int = 6333, + *, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: bool = False, + api_key: Optional[str] = None, + timeout: Optional[int] = None, + **kwargs: Any, + ) -> "QdrantConnectionParameters": + """Connect to a self-hosted Qdrant instance by host and port.""" + return cls( + host=host, + port=port, + grpc_port=grpc_port, + prefer_grpc=prefer_grpc, + https=https, + api_key=api_key, + timeout=timeout, + kwargs=kwargs, + ) + + @classmethod + def for_url( + cls, + url: str, + *, + api_key: Optional[str] = None, + prefer_grpc: bool = False, + timeout: Optional[int] = None, + **kwargs: Any, + ) -> "QdrantConnectionParameters": + """Connect using a full URL like 'https://my-qdrant.example.com:6333'.""" + return cls( + url=url, + api_key=api_key, + prefer_grpc=prefer_grpc, + timeout=timeout, + kwargs=kwargs) + + @classmethod + def local(cls, path: str) -> "QdrantConnectionParameters": + """Use an embedded Qdrant instance persisted to the given path.""" + return cls(path=path) + + @classmethod + def in_memory(cls) -> "QdrantConnectionParameters": + """Use an embedded in-memory Qdrant instance. Useful for tests.""" + return cls(location=":memory:") + + +@dataclass +class QdrantWriteConfig(VectorDatabaseWriteConfig): + """Configuration for writing to Qdrant vector database. + + This class defines the parameters needed to write data to a qdrant collection, + including collection targeting, batching behavior, and operation timeouts. + + Args: + connection_params: QdrantConnectionParameters with connection settings. + collection_name: Name of the Qdrant collection to write to. + timeout: Optional timeout for write operations in seconds. Default is None. + batch_size: Number of points to write in each batch. Default is 1000. + kwargs: Additional keyword arguments to pass to the client's upsert method. + dense_embedding_key: name for the dense vector in the qdrant collection. + sparse_embedding_key: name for the sparse vector in the qdrant collection. + """ + + connection_params: QdrantConnectionParameters + collection_name: str + timeout: Optional[int] = None + batch_size: int = DEFAULT_WRITE_BATCH_SIZE + max_batch_byte_size: int = DEFAULT_MAX_BATCH_BYTE_SIZE + kwargs: dict[str, Any] = field(default_factory=dict) + dense_embedding_key: str = "dense" + sparse_embedding_key: str = "sparse" + + def __post_init__(self): + if not self.collection_name: + raise ValueError("Collection name must be provided") + if self.batch_size <= 0: + raise ValueError("Batch size must be a positive integer") + + def create_write_transform(self) -> beam.PTransform[EmbeddableItem, Any]: + return _QdrantWriteTransform(self) + + def create_converter( + self, + ) -> Callable[[EmbeddableItem], "models.PointStruct"]: + def convert(item: EmbeddableItem) -> "models.PointStruct": + if item.dense_embedding is None and item.sparse_embedding is None: + raise ValueError( + "EmbeddableItem must have at least one embedding (dense or sparse)") + vector = {} + if item.dense_embedding is not None: + vector[self.dense_embedding_key] = item.dense_embedding + if item.sparse_embedding is not None: + sparse_indices, sparse_values = item.sparse_embedding + vector[self.sparse_embedding_key] = models.SparseVector( + indices=sparse_indices, + values=sparse_values, + ) + id = ( + int(item.id) + if isinstance(item.id, str) and item.id.isdigit() else item.id) + return models.PointStruct( + id=id, + vector=vector, + payload=item.metadata if item.metadata else None, + ) + + return convert + + +class _QdrantWriteTransform(beam.PTransform): + def __init__(self, config: QdrantWriteConfig): + self.config = config + + def expand(self, input_or_inputs: beam.PCollection[EmbeddableItem]): + return ( + input_or_inputs + | "Convert to Records" >> beam.Map(self.config.create_converter()) + | beam.ParDo(_QdrantWriteFn(self.config))) + + +class _QdrantWriteFn(beam.DoFn): + def __init__(self, config: QdrantWriteConfig): + self.config = config + self._client: "Optional[QdrantClient]" = None + + def start_bundle(self): + self._batch = [] + self._batch_byte_size = 0 + + def process(self, element, *args, **kwargs): + element_byte_size = get_deep_size(element) + new_batch_byte_size = self._batch_byte_size + element_byte_size + + is_batch_full = len(self._batch) >= self.config.batch_size + is_batch_too_large = new_batch_byte_size > self.config.max_batch_byte_size + if (is_batch_full or is_batch_too_large): + self._flush() + self._batch.append(element) + self._batch_byte_size += element_byte_size + + def setup(self): + params = self.config.connection_params + self._client = QdrantClient( + location=params.location, + url=params.url, + port=params.port, + grpc_port=params.grpc_port, + prefer_grpc=params.prefer_grpc, + https=params.https, + api_key=params.api_key, + prefix=params.prefix, + timeout=params.timeout, + host=params.host, + path=params.path, + check_compatibility=False, + **params.kwargs, + ) + + def teardown(self): + if self._client: + try: + self._client.close() + finally: + self._client = None + + def finish_bundle(self): + self._flush() + + def _flush(self): + if not self._batch: + return + if not self._client: + raise RuntimeError("Qdrant client is not initialized") + + max_retries = 3 + attempt = 1 + while True: + try: + self._client.upsert( + collection_name=self.config.collection_name, + points=self._batch, + timeout=self.config.timeout, + **self.config.kwargs, + ) + break + except ResourceExhaustedResponse as e: + time.sleep(e.retry_after_s) + # don't count rate-limit against max_retries + continue + except (UnexpectedResponse, ResponseHandlingException, + grpc.RpcError) as e: + if attempt > max_retries: + raise + time.sleep(2**attempt) + attempt += 1 + self._batch = [] + self._batch_byte_size = 0 + + def display_data(self): + res = super().display_data() + res["collection"] = self.config.collection_name + res["batch_size"] = self.config.batch_size + res["max_batch_byte_size"] = self.config.max_batch_byte_size + return res diff --git a/sdks/python/apache_beam/ml/rag/ingestion/qdrant_it_test.py b/sdks/python/apache_beam/ml/rag/ingestion/qdrant_it_test.py new file mode 100644 index 000000000000..b82ce2b6e74d --- /dev/null +++ b/sdks/python/apache_beam/ml/rag/ingestion/qdrant_it_test.py @@ -0,0 +1,326 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import pytest + +import apache_beam as beam +from apache_beam.ml.rag.ingestion.qdrant import QdrantConnectionParameters +from apache_beam.ml.rag.ingestion.qdrant import QdrantWriteConfig +from apache_beam.ml.rag.types import Content +from apache_beam.ml.rag.types import EmbeddableItem +from apache_beam.ml.rag.types import Embedding +from apache_beam.testing.test_pipeline import TestPipeline + +# pylint: disable=ungrouped-imports +try: + from qdrant_client import models + + QDRANT_AVAILABLE = True +except ImportError: + QDRANT_AVAILABLE = False + +try: + from testcontainers.qdrant import QdrantContainer +except ImportError: + QdrantContainer = None +# pylint: enable=ungrouped-imports + +TEST_CORPUS = [ + EmbeddableItem( + id="1", + content=Content(text="Test document one"), + metadata={"source": "test1"}, + embedding=Embedding(dense_embedding=[1.0, 0.0]), + ), + EmbeddableItem( + id="2", + content=Content(text="Test document two"), + metadata={"source": "test2"}, + embedding=Embedding(dense_embedding=[0.0, 1.0]), + ), + EmbeddableItem( + id="3", + content=Content(text="Test document three"), + metadata={"source": "test3"}, + embedding=Embedding(dense_embedding=[-1.0, 0.0]), + ), +] + + +@unittest.skipIf(not QDRANT_AVAILABLE, "qdrant dependencies not installed.") +@unittest.skipIf(not QdrantContainer, "qdrant test_container not installed.") +@pytest.mark.require_docker_in_docker +class TestQdrantIngestion(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._container = QdrantContainer() + cls._container.start() + cls._host = cls._container.get_container_host_ip() + cls._port = int(cls._container.get_exposed_port(6333)) + cls._connection_params = QdrantConnectionParameters( + host=cls._host, port=cls._port) + + def setUp(self): + self._collection_name = f"test_collection_{self._testMethodName}" + + self.client = self._container.get_client() + self.client.create_collection( + collection_name=self._collection_name, + vectors_config={ + "dense": models.VectorParams( + size=2, distance=models.Distance.COSINE) + }, + sparse_vectors_config={"sparse": models.SparseVectorParams()}, + ) + assert self.client.collection_exists(collection_name=self._collection_name) + + @classmethod + def tearDownClass(cls): + cls._container.stop() + + def test_write_on_non_existent_collection(self): + non_existent = "nonexistent_collection" + write_config = QdrantWriteConfig( + connection_params=self._connection_params, + collection_name=non_existent, + batch_size=1, + ) + + with self.assertRaises(Exception): + with TestPipeline(is_integration_test=True) as p: + _ = p | beam.Create(TEST_CORPUS) | write_config.create_write_transform() + + def test_write_dense_embeddings_only(self): + write_config = QdrantWriteConfig( + connection_params=self._connection_params, + collection_name=self._collection_name, + batch_size=len(TEST_CORPUS), + ) + + with TestPipeline(is_integration_test=True) as p: + _ = p | beam.Create(TEST_CORPUS) | write_config.create_write_transform() + + count_result = self.client.count(collection_name=self._collection_name) + self.assertEqual(count_result.count, len(TEST_CORPUS)) + + points, _ = self.client.scroll( + collection_name=self._collection_name, + limit=100, + with_payload=True, + with_vectors=True, + ) + points_by_id = {p.id: p for p in points} + + for item in TEST_CORPUS: + expected_record = models.Record( + id=int(item.id), + vector={"dense": item.dense_embedding}, + payload=item.metadata, + ) + self.assertEqual(expected_record, points_by_id[int(item.id)]) + + def test_write_sparse_embeddings_only(self): + sparse_corpus = [ + EmbeddableItem( + id="1", + content=Content(text="Sparse doc one"), + metadata={"source": "sparse1"}, + embedding=Embedding(sparse_embedding=([0, 1, 2], [0.1, 0.2, 0.3])), + ), + EmbeddableItem( + id="2", + content=Content(text="Sparse doc two"), + metadata={"source": "sparse2"}, + embedding=Embedding(sparse_embedding=([1, 3, 5], [0.4, 0.5, 0.6])), + ), + ] + + write_config = QdrantWriteConfig( + connection_params=self._connection_params, + collection_name=self._collection_name, + batch_size=len(sparse_corpus), + ) + + with TestPipeline(is_integration_test=True) as p: + _ = p | beam.Create(sparse_corpus) | write_config.create_write_transform() + + count_result = self.client.count(collection_name=self._collection_name) + self.assertEqual(count_result.count, len(sparse_corpus)) + + points, _ = self.client.scroll( + collection_name=self._collection_name, + limit=100, + with_payload=True, + with_vectors=True, + ) + points_by_id = {p.id: p for p in points} + + for item in sparse_corpus: + expected_record = models.Record( + id=int(item.id), + vector={ + "sparse": models.SparseVector( + indices=item.sparse_embedding[0], + values=item.sparse_embedding[1], + ) + }, + payload=item.metadata, + ) + self.assertEqual(expected_record, points_by_id[int(item.id)]) + + def test_write_both_dense_and_sparse(self): + hybrid_corpus = [ + EmbeddableItem( + id="1", + content=Content(text="Hybrid doc one"), + metadata={"source": "hybrid1"}, + embedding=Embedding( + dense_embedding=[1.0, 0.0], + sparse_embedding=([0, 1], [0.1, 0.2])), + ), + EmbeddableItem( + id="2", + content=Content(text="Hybrid doc two"), + metadata={"source": "hybrid2"}, + embedding=Embedding( + dense_embedding=[0.0, 1.0], + sparse_embedding=([2, 3], [0.3, 0.4])), + ), + ] + + write_config = QdrantWriteConfig( + connection_params=self._connection_params, + collection_name=self._collection_name, + batch_size=len(hybrid_corpus), + ) + + with TestPipeline(is_integration_test=True) as p: + _ = p | beam.Create(hybrid_corpus) | write_config.create_write_transform() + + count_result = self.client.count(collection_name=self._collection_name) + self.assertEqual(count_result.count, len(hybrid_corpus)) + + points, _ = self.client.scroll( + collection_name=self._collection_name, + limit=100, + with_payload=True, + with_vectors=True, + ) + points_by_id = {p.id: p for p in points} + + for item in hybrid_corpus: + expected_record = models.Record( + id=int(item.id), + vector={ + "dense": item.dense_embedding, + "sparse": models.SparseVector( + indices=item.sparse_embedding[0], + values=item.sparse_embedding[1], + ), + }, + payload=item.metadata, + ) + self.assertEqual(expected_record, points_by_id[int(item.id)]) + + def test_write_with_batching(self): + batch_corpus = [ + EmbeddableItem( + id=str(i), + content=Content(text=f"Batch doc {i}"), + metadata={"batch_id": i}, + embedding=Embedding(dense_embedding=[1.0, 0.0]), + ) for i in range(1, 8) + ] + + write_config = QdrantWriteConfig( + connection_params=self._connection_params, + collection_name=self._collection_name, + batch_size=3, + ) + + with TestPipeline(is_integration_test=True) as p: + _ = p | beam.Create(batch_corpus) | write_config.create_write_transform() + + count_result = self.client.count(collection_name=self._collection_name) + self.assertEqual(count_result.count, len(batch_corpus)) + + points, _ = self.client.scroll( + collection_name=self._collection_name, + limit=100, + with_payload=True, + with_vectors=True, + ) + points_by_id = {p.id: p for p in points} + + for item in batch_corpus: + expected_record = models.Record( + id=int(item.id), + vector={ + "dense": item.dense_embedding, + }, + payload=item.metadata, + ) + self.assertEqual(expected_record, points_by_id[int(item.id)]) + + def test_write_with_byte_size_limit(self): + byte_size_corpus = [ + EmbeddableItem( + id=str(i), + content=Content(text=f"Byte size doc {i}"), + metadata={"data": "x" * 9000}, + embedding=Embedding(dense_embedding=[1.0, 0.0]), + ) for i in range(5) + ] + + write_config = QdrantWriteConfig( + connection_params=self._connection_params, + collection_name=self._collection_name, + batch_size=100, + max_batch_byte_size=15_000, + ) + + with TestPipeline(is_integration_test=True) as p: + _ = ( + p + | beam.Create(byte_size_corpus) + | write_config.create_write_transform()) + + count_result = self.client.count(collection_name=self._collection_name) + self.assertEqual(count_result.count, len(byte_size_corpus)) + + points, _ = self.client.scroll( + collection_name=self._collection_name, + limit=100, + with_payload=True, + with_vectors=True, + ) + points_by_id = {p.id: p for p in points} + + for item in byte_size_corpus: + expected_record = models.Record( + id=int(item.id), + vector={ + "dense": item.dense_embedding, + }, + payload=item.metadata, + ) + self.assertEqual(expected_record, points_by_id[int(item.id)]) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/python/apache_beam/ml/rag/ingestion/qdrant_test.py b/sdks/python/apache_beam/ml/rag/ingestion/qdrant_test.py new file mode 100644 index 000000000000..ff4ee14e97a0 --- /dev/null +++ b/sdks/python/apache_beam/ml/rag/ingestion/qdrant_test.py @@ -0,0 +1,480 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +try: + from qdrant_client import models + from qdrant_client.common.client_exceptions import ResourceExhaustedResponse + from qdrant_client.http.exceptions import ResponseHandlingException + from qdrant_client.http.exceptions import UnexpectedResponse + + QDRANT_AVAILABLE = True +except ImportError: + QDRANT_AVAILABLE = False + +import grpc +from objsize import get_deep_size + +from apache_beam.ml.rag.ingestion.qdrant import QdrantConnectionParameters +from apache_beam.ml.rag.ingestion.qdrant import QdrantWriteConfig +from apache_beam.ml.rag.ingestion.qdrant import _QdrantWriteFn +from apache_beam.ml.rag.types import Content +from apache_beam.ml.rag.types import EmbeddableItem +from apache_beam.ml.rag.types import Embedding + + +class TestQdrantConnectionParameters(unittest.TestCase): + def test_no_params_raises_value_error(self): + with self.assertRaises(ValueError): + QdrantConnectionParameters() + + def test_location_is_sufficient(self): + QdrantConnectionParameters(location=":memory:") + + def test_url_is_sufficient(self): + QdrantConnectionParameters(url="http://localhost:6333") + + def test_host_is_sufficient(self): + QdrantConnectionParameters(host="localhost") + + def test_path_is_sufficient(self): + QdrantConnectionParameters(path="/tmp/qdrant") + + +class TestQdrantWriteConfig(unittest.TestCase): + def test_empty_collection_name_raises_value_error(self): + with self.assertRaises(ValueError): + QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="", + ) + + def test_none_collection_name_raises_value_error(self): + with self.assertRaises(ValueError): + QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name=None, + ) + + def test_batch_size_zero_raises_value_error(self): + with self.assertRaises(ValueError): + QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + batch_size=0, + ) + + def test_batch_size_negative_raises_value_error(self): + with self.assertRaises(ValueError): + QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + batch_size=-1, + ) + + def test_display_data(self): + config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + batch_size=100, + max_batch_byte_size=5000, + ) + fn = _QdrantWriteFn(config) + data = fn.display_data() + self.assertEqual(data["collection"], "test") + self.assertEqual(data["batch_size"], 100) + self.assertEqual(data["max_batch_byte_size"], 5000) + + def test_for_cloud_creates_connection(self): + params = QdrantConnectionParameters.for_cloud( + url="https://test.cloud.qdrant.io", + api_key="my-key", + ) + self.assertEqual(params.url, "https://test.cloud.qdrant.io") + self.assertEqual(params.api_key, "my-key") + self.assertTrue(params.https) + + def test_for_host_creates_connection(self): + params = QdrantConnectionParameters.for_host(host="localhost", port=6333) + self.assertEqual(params.host, "localhost") + self.assertEqual(params.port, 6333) + + def test_in_memory_creates_connection(self): + params = QdrantConnectionParameters.in_memory() + self.assertEqual(params.location, ":memory:") + + def test_for_url_creates_connection(self): + params = QdrantConnectionParameters.for_url(url="http://localhost:6333") + self.assertEqual(params.url, "http://localhost:6333") + + def test_kwargs_passthrough(self): + config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + kwargs={"parallel": 4}, + ) + self.assertEqual(config.kwargs, {"parallel": 4}) + + +@unittest.skipIf(not QDRANT_AVAILABLE, "qdrant dependencies not installed.") +class TestQdrantCreateConverter(unittest.TestCase): + def setUp(self): + self.config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + ) + self.convert = self.config.create_converter() + + def test_dense_embedding_only(self): + item = EmbeddableItem( + id="1", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[1.0, 2.0]), + ) + result = self.convert(item) + self.assertIsInstance(result, models.PointStruct) + self.assertEqual(result.id, 1) + self.assertEqual(result.vector, {"dense": [1.0, 2.0]}) + self.assertIsNone(result.payload) + + def test_sparse_embedding_only(self): + item = EmbeddableItem( + id="2", + content=Content(text="test"), + embedding=Embedding(sparse_embedding=([0, 1], [0.5, 0.3])), + ) + result = self.convert(item) + self.assertIsInstance(result, models.PointStruct) + self.assertIn("sparse", result.vector) + sparse_vec = result.vector["sparse"] + self.assertIsInstance(sparse_vec, models.SparseVector) + self.assertEqual(sparse_vec.indices, [0, 1]) + self.assertEqual(sparse_vec.values, [0.5, 0.3]) + + def test_both_dense_and_sparse(self): + item = EmbeddableItem( + id="3", + content=Content(text="test"), + embedding=Embedding( + dense_embedding=[1.0, 2.0], + sparse_embedding=([0], [0.9]), + ), + ) + result = self.convert(item) + self.assertEqual(set(result.vector.keys()), {"dense", "sparse"}) + self.assertEqual(result.vector["dense"], [1.0, 2.0]) + self.assertEqual(result.id, 3) + + def test_raises_when_no_embedding(self): + item = EmbeddableItem( + id="4", + content=Content(text="test"), + ) + with self.assertRaises(ValueError): + self.convert(item) + + def test_string_digit_id_converted_to_int(self): + item = EmbeddableItem( + id="42", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[0.1, 0.2]), + ) + result = self.convert(item) + self.assertEqual(result.id, 42) + self.assertIsInstance(result.id, int) + + def test_non_digit_string_id_preserved(self): + item = EmbeddableItem( + id="abc-123", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[0.1, 0.2]), + ) + result = self.convert(item) + self.assertEqual(result.id, "abc-123") + self.assertIsInstance(result.id, str) + + def test_integer_id_preserved(self): + item = EmbeddableItem( + id="99", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[0.1, 0.2]), + ) + result = self.convert(item) + self.assertEqual(result.id, 99) + self.assertIsInstance(result.id, int) + + def test_none_metadata_becomes_none_payload(self): + item = EmbeddableItem( + id="1", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[0.1, 0.2]), + metadata={}, + ) + result = self.convert(item) + self.assertIsNone(result.payload) + + def test_custom_vector_keys(self): + config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + dense_embedding_key="my_dense", + sparse_embedding_key="my_sparse", + ) + convert = config.create_converter() + item = EmbeddableItem( + id="1", + content=Content(text="test"), + embedding=Embedding( + dense_embedding=[1.0], + sparse_embedding=([0], [0.5]), + ), + ) + result = convert(item) + self.assertIn("my_dense", result.vector) + self.assertIn("my_sparse", result.vector) + self.assertNotIn("dense", result.vector) + self.assertNotIn("sparse", result.vector) + + def test_payload_includes_metadata(self): + item = EmbeddableItem( + id="1", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[1.0]), + metadata={ + "source": "test", "score": 0.95 + }, + ) + result = self.convert(item) + self.assertEqual(result.payload, {"source": "test", "score": 0.95}) + + def test_convert_from_text_factory(self): + item = EmbeddableItem.from_text("hello", metadata={"source": "test"}) + item.embedding = Embedding(dense_embedding=[0.5, 0.5]) + result = self.convert(item) + self.assertIsInstance(result, models.PointStruct) + self.assertIn("dense", result.vector) + + +@unittest.skipIf(not QDRANT_AVAILABLE, "qdrant dependencies not installed.") +class TestQdrantWriteFnBatching(unittest.TestCase): + def setUp(self): + self.config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + batch_size=3, + ) + self.fn = _QdrantWriteFn(self.config) + self.fn._client = mock.MagicMock() + self.fn.start_bundle() + + def test_batch_size_triggers_flush_correctly(self): + client = self.fn._client + for i in range(5): + self.fn.process( + EmbeddableItem( + id=str(i), + content=Content(text="test"), + embedding=Embedding(dense_embedding=[float(i)]), + )) + self.fn.finish_bundle() + + self.assertEqual(client.upsert.call_count, 2) + first = client.upsert.call_args_list[0][1]["points"] + second = client.upsert.call_args_list[1][1]["points"] + self.assertEqual(len(first), 3) + self.assertEqual(len(second), 2) + self.assertEqual(first[0].id, "0") + self.assertEqual(first[1].id, "1") + self.assertEqual(first[2].id, "2") + self.assertEqual(second[0].id, "3") + self.assertEqual(second[1].id, "4") + + def test_partial_batch_flushed_on_finish_bundle(self): + for i in range(2): + self.fn.process( + EmbeddableItem( + id=str(i), + content=Content(text="test"), + embedding=Embedding(dense_embedding=[float(i)]), + )) + self.fn.finish_bundle() + + points = self.fn._client.upsert.call_args[1]["points"] + self.assertEqual(len(points), 2) + + def test_byte_size_exceeded_triggers_flush(self): + item = EmbeddableItem( + id="1", + content=Content( + text="a" * 256, + image=b"x" * 1024, + ), + ) + item_size = get_deep_size(item) + + config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + batch_size=10, + max_batch_byte_size=item_size * 2, + ) + fn = _QdrantWriteFn(config) + fn._client = mock.MagicMock() + fn.start_bundle() + client = fn._client + + for i in range(3): + fn.process( + EmbeddableItem( + id=str(i), + content=Content( + text="a" * 256, + image=b"x" * 1024, + ), + )) + fn.finish_bundle() + + self.assertEqual(client.upsert.call_count, 2) + first = client.upsert.call_args_list[0][1]["points"] + second = client.upsert.call_args_list[1][1]["points"] + self.assertEqual(len(first), 2) + self.assertEqual(len(second), 1) + + +@unittest.skipIf(not QDRANT_AVAILABLE, "qdrant dependencies not installed.") +class TestQdrantWriteFnRetries(unittest.TestCase): + def setUp(self): + self.config = QdrantWriteConfig( + connection_params=QdrantConnectionParameters(location=":memory:"), + collection_name="test", + ) + self.fn = _QdrantWriteFn(self.config) + self.fn._client = mock.MagicMock() + self.fn._batch = [ + EmbeddableItem( + id="1", + content=Content(text="test"), + embedding=Embedding(dense_embedding=[1.0]), + ) + ] + self.fn._batch_byte_size = 100 + + def test_retry_on_unexpected_response(self): + self.fn._client.upsert.side_effect = [ + UnexpectedResponse(429, "error", b"", None), + None, + ] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 2) + mock_sleep.assert_called_once_with(2) + + def test_retry_on_response_handling_exception(self): + self.fn._client.upsert.side_effect = [ + ResponseHandlingException(Exception("error")), + None, + ] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 2) + mock_sleep.assert_called_once_with(2) + + def test_retry_on_grpc_error(self): + self.fn._client.upsert.side_effect = [ + grpc.RpcError("error"), + None, + ] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 2) + mock_sleep.assert_called_once_with(2) + + def test_rate_limit_does_not_increment_attempt(self): + exc = ResourceExhaustedResponse("rate limited", 0) + exc.retry_after_s = 0.01 + self.fn._client.upsert.side_effect = [exc, None] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 2) + mock_sleep.assert_called_once_with(0.01) + + def test_multiple_rate_limits_dont_exhaust_retries(self): + exc = ResourceExhaustedResponse("rate limited", 0) + exc.retry_after_s = 0.01 + self.fn._client.upsert.side_effect = [exc, exc, exc, None] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 4) + self.assertEqual(mock_sleep.call_count, 3) + + def test_rate_limit_then_error_then_success(self): + exc_rate = ResourceExhaustedResponse("rate limited", 0) + exc_rate.retry_after_s = 0.01 + exc_error = UnexpectedResponse(429, "error", b"", None) + self.fn._client.upsert.side_effect = [exc_error, exc_rate, None] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 3) + self.assertEqual(mock_sleep.call_args_list[0], mock.call(2)) + self.assertEqual(mock_sleep.call_args_list[1], mock.call(0.01)) + + def test_exponential_backoff_values(self): + self.fn._client.upsert.side_effect = [ + UnexpectedResponse(429, "e1", b"", None), + UnexpectedResponse(429, "e2", b"", None), + UnexpectedResponse(429, "e3", b"", None), + None, + ] + with mock.patch("time.sleep") as mock_sleep: + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 4) + self.assertEqual(mock_sleep.call_args_list[0], mock.call(2)) + self.assertEqual(mock_sleep.call_args_list[1], mock.call(4)) + self.assertEqual(mock_sleep.call_args_list[2], mock.call(8)) + + def test_raises_after_max_retries(self): + self.fn._client.upsert.side_effect = [ + UnexpectedResponse(429, "e1", b"", None), + UnexpectedResponse(429, "e2", b"", None), + UnexpectedResponse(429, "e3", b"", None), + UnexpectedResponse(429, "e4", b"", None), + ] + with mock.patch("time.sleep") as mock_sleep: + with self.assertRaises(UnexpectedResponse): + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 4) + self.assertEqual(mock_sleep.call_count, 3) + + def test_raises_on_last_non_rate_limit_attempt(self): + exc_rate = ResourceExhaustedResponse("rate limited", 0) + exc_rate.retry_after_s = 0.01 + self.fn._client.upsert.side_effect = [ + exc_rate, + UnexpectedResponse(429, "e1", b"", None), + UnexpectedResponse(429, "e2", b"", None), + UnexpectedResponse(429, "e3", b"", None), + UnexpectedResponse(429, "e4", b"", None), + ] + with mock.patch("time.sleep") as mock_sleep: + with self.assertRaises(UnexpectedResponse): + self.fn._flush() + self.assertEqual(self.fn._client.upsert.call_count, 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/python/setup.py b/sdks/python/setup.py index dbdef30aef9d..961f890e1edf 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -166,6 +166,7 @@ def cythonize(*args, **kwargs): ] milvus_dependency = ['pymilvus>=2.5.10,<3.0.0'] +qdrant_dependency = ['qdrant-client>=1.15.0'] # google-adk / OpenTelemetry require protobuf>=5; tensorflow-transform in # ml_test is pinned to versions that require protobuf<5 on Python 3.10. Those @@ -508,7 +509,7 @@ def get_portability_package_data(): 'scikit-learn>=0.20.0,<1.8.0', 'sqlalchemy>=1.3,<3.0', 'psycopg2-binary>=2.8.5,<3.0', - 'testcontainers[mysql,kafka,milvus]>=4.0.0,<5.0.0', + 'testcontainers[mysql,kafka,milvus,qdrant]>=4.0.0,<5.0.0', 'cryptography>=41.0.2', # TODO(https://github.com/apache/beam/issues/36951): need to # further investigate the cause @@ -607,14 +608,14 @@ def get_portability_package_data(): 'tf2onnx>=1.16.1,<1.17', ] + ml_base_core, 'p310_ml_test': [ - 'datatable', - ] + ml_base, + 'datatable', + ] + ml_base + qdrant_dependency, 'p312_ml_test': [ 'datatable', - ] + ml_base, + ] + ml_base + qdrant_dependency, # maintainer: milvus tests only run with this extension. Make sure it # is covered by docker-in-docker test when changing py version - 'p313_ml_test': ml_base + milvus_dependency, + 'p313_ml_test': ml_base + milvus_dependency + qdrant_dependency, 'aws': ['boto3>=1.9,<2'], 'azure': [ 'azure-storage-blob>=12.3.2,<13', @@ -686,6 +687,7 @@ def get_portability_package_data(): 'xgboost': ['xgboost>=1.6.0,<2.1.3', 'datatable==1.0.0'], 'tensorflow-hub': ['tensorflow-hub>=0.14.0,<0.16.0'], 'milvus': milvus_dependency, + 'qdrant': qdrant_dependency, 'vllm': ['openai==1.107.1', 'vllm==0.10.1.1', 'triton==3.3.1'] }, zip_safe=False, From 40c2d7bfca6abd3bd095f0838312db00dc111a7d Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Mon, 1 Jun 2026 02:04:18 -0700 Subject: [PATCH 275/490] [Dataflow Streaming] Enable state tag encoding v2 (#38705) * Enable state tag encoding v2 by default for new Dataflow Streaming Engine jobs Also added CHANGES.md entry detailing the change, how to disable it, and job update/downgrade limitations. * Respect UpdateCompatibility for tag encoding v2 --- CHANGES.md | 2 +- .../beam/runners/dataflow/DataflowRunner.java | 6 +++ .../runners/dataflow/DataflowRunnerTest.java | 44 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3b4af42daf8d..669cabd7c5a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,7 +69,7 @@ ## New Features / Improvements -* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). +* (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). ## Breaking Changes diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index d04afb351e44..4864f2cf4537 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -101,6 +101,7 @@ import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.PipelineOptionsValidator; import org.apache.beam.sdk.options.SdkHarnessOptions; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider; import org.apache.beam.sdk.runners.AppliedPTransform; import org.apache.beam.sdk.runners.PTransformOverride; @@ -1310,6 +1311,11 @@ public DataflowPipelineJob run(Pipeline pipeline) { // Experiment marking that the harness supports tag encoding v2 // Backend will enable tag encoding v2 only if the harness supports it. experiments.add("streaming_engine_state_tag_encoding_v2_supported"); + // Experiment requesting tag encoding v2 on new jobs starting with 2.75.0. During job + // updates old job's tag encoding version is carried over by the backend. + if (!StreamingOptions.updateCompatibilityVersionLessThan(options, "2.75.0")) { + experiments.add("enable_streaming_engine_state_tag_encoding_v2"); + } options.setExperiments(ImmutableList.copyOf(experiments)); } diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java index 122c4aeee34f..073b30f928dc 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java @@ -2923,4 +2923,48 @@ public void processElement( PAssert.that(output).containsInAnyOrder("value:UPDATE_BEFORE"); pipeline.run(); } + + @Test + public void testStreamingStateTagEncodingV2PreCompatibility() throws Exception { + DataflowPipelineOptions options = buildPipelineOptions(); + options.as(StreamingOptions.class).setStreaming(true); + options.as(StreamingOptions.class).setUpdateCompatibilityVersion("2.74.0"); + Pipeline p = Pipeline.create(options); + + p.run(); + + List experiments = options.getExperiments(); + assertNotNull(experiments); + assertTrue(experiments.contains("streaming_engine_state_tag_encoding_v2_supported")); + assertFalse(experiments.contains("enable_streaming_engine_state_tag_encoding_v2")); + } + + @Test + public void testStreamingStateTagEncodingV2PostCompatibility() throws Exception { + DataflowPipelineOptions options = buildPipelineOptions(); + options.as(StreamingOptions.class).setStreaming(true); + options.as(StreamingOptions.class).setUpdateCompatibilityVersion("2.75.0"); + Pipeline p = Pipeline.create(options); + + p.run(); + + List experiments = options.getExperiments(); + assertNotNull(experiments); + assertTrue(experiments.contains("streaming_engine_state_tag_encoding_v2_supported")); + assertTrue(experiments.contains("enable_streaming_engine_state_tag_encoding_v2")); + } + + @Test + public void testStreamingStateTagEncodingV2NoCompatibility() throws Exception { + DataflowPipelineOptions options = buildPipelineOptions(); + options.as(StreamingOptions.class).setStreaming(true); + Pipeline p = Pipeline.create(options); + + p.run(); + + List experiments = options.getExperiments(); + assertNotNull(experiments); + assertTrue(experiments.contains("streaming_engine_state_tag_encoding_v2_supported")); + assertTrue(experiments.contains("enable_streaming_engine_state_tag_encoding_v2")); + } } From 00c418822ef0f7c65b23681ea5cdea7c5dcd4c01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:02:08 -0400 Subject: [PATCH 276/490] Bump nanasess/setup-chromedriver from 2 to 3 (#38757) Bumps [nanasess/setup-chromedriver](https://github.com/nanasess/setup-chromedriver) from 2 to 3. - [Release notes](https://github.com/nanasess/setup-chromedriver/releases) - [Commits](https://github.com/nanasess/setup-chromedriver/compare/v2...v3) --- updated-dependencies: - dependency-name: nanasess/setup-chromedriver dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/playground_frontend_test.yml | 2 +- .github/workflows/tour_of_beam_frontend_test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playground_frontend_test.yml b/.github/workflows/playground_frontend_test.yml index 3c2fa18e18d3..9fc7ae83230d 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -89,7 +89,7 @@ jobs: working-directory: playground/frontend run: flutter test - - uses: nanasess/setup-chromedriver@v2 + - uses: nanasess/setup-chromedriver@v3 - name: 'Integration tests' run: | diff --git a/.github/workflows/tour_of_beam_frontend_test.yml b/.github/workflows/tour_of_beam_frontend_test.yml index f1afd9b377d8..0cbdb01ad864 100644 --- a/.github/workflows/tour_of_beam_frontend_test.yml +++ b/.github/workflows/tour_of_beam_frontend_test.yml @@ -67,7 +67,7 @@ jobs: cd playground/frontend/playground_components_dev && flutter pub get && cd - cd learning/tour-of-beam/frontend && flutter pub get && cd - - - uses: nanasess/setup-chromedriver@v2 + - uses: nanasess/setup-chromedriver@v3 - name: 'Integration tests' run: | From 66589ec7284629a790db8f0fca1bf4e9f4990021 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:03:02 -0400 Subject: [PATCH 277/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#38758) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.102.1 to 1.102.2. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.102.1...service/s3/v1.102.2) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.102.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 20 ++++++++++---------- sdks/go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 618c044a63df..696ba9d1f587 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,11 +32,11 @@ require ( cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 - github.com/aws/aws-sdk-go-v2 v1.41.8 + github.com/aws/aws-sdk-go-v2 v1.41.9 github.com/aws/aws-sdk-go-v2/config v1.32.19 github.com/aws/aws-sdk-go-v2/credentials v1.19.18 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21 - github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 github.com/aws/smithy-go v1.26.0 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 @@ -148,15 +148,15 @@ require ( github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 5a30712858b9..22bfdf8a28a5 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -199,12 +199,12 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.8 h1:sRs7nG6/RiEBZ/K5UO2sNw0w40U02Nmz1VtARloTZXk= -github.com/aws/aws-sdk-go-v2 v1.41.8/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4= +github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= github.com/aws/aws-sdk-go-v2/config v1.32.19 h1:qRhIJMbevHUvIE7X4TK8N8zye5+5AhapcslPrvB+qKE= @@ -223,38 +223,38 @@ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21 h1:1k9o0SJq41F/KhjJKJvK github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21/go.mod h1:kHrUWcXXLKEbC6doUmtxs3a1dDS1sYz6a8K2/ce5K4A= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 h1:u6kJU2i0va1AgtJsH3RdWKWqHULlTh7zHwb35Womf74= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24/go.mod h1:7GY+xLcXOFUpCkNwDReft9qOAVg54A4/AnjHIU7sSAY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 h1:Xhbcf3KugX6vX7SDyUK205Oicyfg7EGuvoVNyP5L6DM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24/go.mod h1:rwDgb2HNOGZsnTHylOUedM7Vnl+bCfnXDqUNPsFWYfk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 h1:54CTMmlJ71Rk2dYvM9qZOob+39wjlVja2zDLxCu69Ew= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25/go.mod h1:BZaHqxsS9vN1fvV5EfEl0OBLOk5+AajWsMu6MjqnZB4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17 h1:Zma31M1f9bbD/bsl6haTxupA0+z72L3l2ujKAH37zuI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17/go.mod h1:ZNHrGwBST3tZxBCTKbindx0BEdPN0Jnh7yJ7EVnktUM= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 h1:CQW2FTrflfoslYWLf3fv7vG28Q219+v8YJS5QTQb2+Y= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24/go.mod h1:Xfx13T+u3nH6EEzgl9fBSO6nDRmze1FvnZNYkctQ2zw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24 h1:yPLVC8Lbsw92eepgdIZCChHRNQek5eAvAz5wS+UIpJE= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24/go.mod h1:H2h39H1AivHYkozUIUYoVJGMUOvdJ4Lv9DLyUSMAjW8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 h1:vttIo8BQwfnhimKRBZBBF3Y38SAIxif72B/M91m9hDk= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1/go.mod h1:2qjInACJr84m/Tm4XXCcVNpejmbKy9kz7TEa6viQHSk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298= +github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 h1:yQo3eZ5qFaL1sJWqs1nL6j3yPHA2/R7c6tQ4T+0IO10= github.com/aws/aws-sdk-go-v2/service/signin v1.1.0/go.mod h1:3Zzou41Qt/ueXfIzHvTEjDNuR5IjCUBVF01SNhrt1e8= From b29d7ec19ebf2c74001e12068d315a291fa636ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:03:42 -0400 Subject: [PATCH 278/490] Bump github.com/golang-cz/devslog from 0.0.15 to 0.0.16 in /sdks (#38759) Bumps [github.com/golang-cz/devslog](https://github.com/golang-cz/devslog) from 0.0.15 to 0.0.16. - [Release notes](https://github.com/golang-cz/devslog/releases) - [Commits](https://github.com/golang-cz/devslog/compare/v0.0.15...v0.0.16) --- updated-dependencies: - dependency-name: github.com/golang-cz/devslog dependency-version: 0.0.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 696ba9d1f587..bc3e698f9053 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -71,7 +71,7 @@ require ( require ( github.com/avast/retry-go/v4 v4.7.0 github.com/fsouza/fake-gcs-server v1.52.3 - github.com/golang-cz/devslog v0.0.15 + github.com/golang-cz/devslog v0.0.16 github.com/moby/moby/api v1.54.2 github.com/moby/moby/client v0.4.1 golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a diff --git a/sdks/go.sum b/sdks/go.sum index 22bfdf8a28a5..6f54b17d120b 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -415,8 +415,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang-cz/devslog v0.0.15 h1:ejoBLTCwJHWGbAmDf2fyTJJQO3AkzcPjw8SC9LaOQMI= -github.com/golang-cz/devslog v0.0.15/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= +github.com/golang-cz/devslog v0.0.16 h1:3tmUAvVRzX3+lzaLdpuEpfnc72e2pLGrIyok+FHjLP8= +github.com/golang-cz/devslog v0.0.16/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= From 70406461d02f01a8cd895d89de0a625a716e5bf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:04:24 -0400 Subject: [PATCH 279/490] Bump github.com/aws/aws-sdk-go-v2 from 1.41.8 to 1.41.9 in /sdks (#38761) Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.41.8 to 1.41.9. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.8...v1.41.9) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2 dependency-version: 1.41.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From d9f603a3c249668fd23c34d9377e814810518ed5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:04:53 -0400 Subject: [PATCH 280/490] Bump github.com/tetratelabs/wazero from 1.11.0 to 1.12.0 in /sdks (#38762) Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.11.0 to 1.12.0. - [Release notes](https://github.com/tetratelabs/wazero/releases) - [Commits](https://github.com/tetratelabs/wazero/compare/v1.11.0...v1.12.0) --- updated-dependencies: - dependency-name: github.com/tetratelabs/wazero dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index bc3e698f9053..22462d772d0c 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -51,7 +51,7 @@ require ( github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 github.com/testcontainers/testcontainers-go v0.42.0 - github.com/tetratelabs/wazero v1.11.0 + github.com/tetratelabs/wazero v1.12.0 github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 diff --git a/sdks/go.sum b/sdks/go.sum index 6f54b17d120b..1650b2aed2a2 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -806,8 +806,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= From 2e8717eaf94db7c77727a2dc7244610706c2c8e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:03:02 -0400 Subject: [PATCH 281/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38760) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.21 to 1.22.22. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.21...feature/s3/manager/v1.22.22) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.22 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 16 ++++++++-------- sdks/go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 22462d772d0c..ad4629b61f6b 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,9 +33,9 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 github.com/aws/aws-sdk-go-v2 v1.41.9 - github.com/aws/aws-sdk-go-v2/config v1.32.19 - github.com/aws/aws-sdk-go-v2/credentials v1.19.18 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21 + github.com/aws/aws-sdk-go-v2/config v1.32.20 + github.com/aws/aws-sdk-go-v2/credentials v1.19.19 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 github.com/aws/smithy-go v1.26.0 github.com/docker/go-connections v0.7.0 // indirect @@ -91,7 +91,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -149,7 +149,7 @@ require ( github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect @@ -157,9 +157,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 1650b2aed2a2..22a716110ec1 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,20 +207,20 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24g github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.19 h1:qRhIJMbevHUvIE7X4TK8N8zye5+5AhapcslPrvB+qKE= -github.com/aws/aws-sdk-go-v2/config v1.32.19/go.mod h1:RbJ24nfoya63+Mf5VI+CGCGk9vEdv28xPeii+gojRYs= +github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc= +github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.18 h1:GcXQz2M/0ZvMo0v5DakUqbDBeBM1ZNaivkolEF4Esgw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.18/go.mod h1:sHJ06tMGcD3ZpmMyJqV+VBsGilhSIZPIN+ZFy5Dg0C4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 h1:FQm5ApnyzkuJdXLGskPce83CK1CQKC4RUnIHKVe4BU4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24/go.mod h1:JsC7dqQc55MlZ5mvNsDMMge71u8pVcSzU3RNz2h/5yQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21 h1:1k9o0SJq41F/KhjJKJvKRTsDEYaWa03Ol82GkT9DooQ= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.21/go.mod h1:kHrUWcXXLKEbC6doUmtxs3a1dDS1sYz6a8K2/ce5K4A= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 h1:eBnBL5sAT9aIg014yU6DG4YT/ov8r9jnOPW/t7rr8Ig= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22/go.mod h1:PjXTcjNr3cv5pfRG+PakZ9Yypx2vLRjCAcm1kEfHSjk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ= @@ -256,22 +256,22 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU66 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298= github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 h1:yQo3eZ5qFaL1sJWqs1nL6j3yPHA2/R7c6tQ4T+0IO10= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.0/go.mod h1:3Zzou41Qt/ueXfIzHvTEjDNuR5IjCUBVF01SNhrt1e8= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 h1:ApLTFdAZfDhZSiY5uskwECKHkSNNF83y2Ru2r7SezWA= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.18/go.mod h1:A9K9qx2l6nK89hp+a350FdGfRkrkH5HdiEjHbiy/Q/c= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 h1:4VD7TIZOGzehrgQ8vDE+1c6BQW4ErZPGY8ohZT5LXEE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1/go.mod h1:er0SFJfdV89Rit5hIJu/EXtv+qC2XMnxoksLmcUFkqM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 h1:XKnxlM4KZH1gktcsh3zSWc7GW4KivEv/OkifmHOhCUY= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.2/go.mod h1:KJYmkQaFB3SUW2j3aBkPsxNmAb4ZsSOvbvCpuxzHJA0= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= From 3a473c3c4f9f848dabf8ff262240a8b72bd0c206 Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Mon, 1 Jun 2026 11:13:23 -0700 Subject: [PATCH 282/490] Add Worker hashes for Golang (#38754) --- .../beam/runners/dataflow/dataflowlib/execute.go | 1 + .../pkg/beam/runners/dataflow/dataflowlib/job.go | 4 ++++ .../runners/dataflow/dataflowlib/job_test.go | 16 ++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go index 396eefab7318..c5cb8a636f33 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go @@ -71,6 +71,7 @@ func Execute(ctx context.Context, raw *pipepb.Pipeline, opts *JobOptions, worker if err != nil { return presult, err } + opts.WorkerHash = hash log.Infof(ctx, "Staged worker binary: %v", workerURL) if err := graphx.UpdateDefaultEnvWorkerType( diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go index f0adb21cf714..27a0a0d0f224 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go @@ -85,6 +85,9 @@ type JobOptions struct { // Worker is the worker binary override. Worker string + // WorkerHash is the SHA-256 hash of the worker binary. + WorkerHash string + // -- Internal use only. Not supported in public Dataflow. -- TeardownPolicy string @@ -136,6 +139,7 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker packages := []*df.Package{{ Name: "worker", Location: workerURL, + Sha256: opts.WorkerHash, }} for _, url := range opts.ArtifactURLs { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go index 901adb6c7b72..07f2d39e552c 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go @@ -292,6 +292,7 @@ func TestTranslate(t *testing.T) { Name: "test-job", DiskProvisionedIops: 4000, DiskProvisionedThroughputMibps: 200, + WorkerHash: "worker-sha256-hash", } workerURL := "gs://any-location/temp" modelURL := "gs://any-location/temp" @@ -312,6 +313,21 @@ func TestTranslate(t *testing.T) { if wp.DiskProvisionedThroughputMibps != 200 { t.Errorf("DiskProvisionedThroughputMibps = %v, want 200", wp.DiskProvisionedThroughputMibps) } + + // Verify worker package has Sha256 + found := false + for _, pkg := range wp.Packages { + if pkg.Name == "worker" { + found = true + if pkg.Sha256 != "worker-sha256-hash" { + t.Errorf("worker package Sha256 = %q, want %q", pkg.Sha256, "worker-sha256-hash") + } + break + } + } + if !found { + t.Fatalf("worker package not found in wp.Packages") + } } func TestTranslateWithPipelineHash(t *testing.T) { From 550bcf74bb18250d8ec769b074862ec7cae07a1a Mon Sep 17 00:00:00 2001 From: Chamikara Jayalath Date: Mon, 1 Jun 2026 17:42:09 -0700 Subject: [PATCH 283/490] Updates Beam master container tags for Dataflow (#38766) --- runners/google-cloud-dataflow-java/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index 943a01daa7d0..be2b7b2b01e4 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -52,8 +52,8 @@ evaluationDependsOn(":sdks:java:container:java11") ext.dataflowLegacyEnvironmentMajorVersion = '8' ext.dataflowFnapiEnvironmentMajorVersion = '8' -ext.dataflowLegacyContainerVersion = 'beam-master-20260513' -ext.dataflowFnapiContainerVersion = 'beam-master-20260513' +ext.dataflowLegacyContainerVersion = 'beam-master-20260601' +ext.dataflowFnapiContainerVersion = 'beam-master-20260601' ext.dataflowContainerBaseRepository = 'gcr.io/cloud-dataflow/v1beta3' processResources { From 627b27f3193ee89d25501fe7573b85527487d3e8 Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Tue, 2 Jun 2026 00:43:26 -0700 Subject: [PATCH 284/490] [Dataflow Streaming] Prepare BoundedQueueExecutor for MultiKey bundles (#38592) --- .../BoundedQueueExecutorWorkHandle.java | 24 +++ .../worker/streaming/ExecutableWork.java | 51 ++++-- .../worker/util/BoundedQueueExecutor.java | 166 ++++++++++++++++-- .../dataflow/worker/util/ExceptionUtils.java | 43 +++++ .../processing/StreamingCommitFinalizer.java | 2 +- .../processing/StreamingWorkScheduler.java | 18 +- .../worker/StreamingDataflowWorkerTest.java | 4 +- .../worker/streaming/ActiveWorkStateTest.java | 4 +- .../streaming/ComputationStateCacheTest.java | 2 +- .../worker/util/BoundedQueueExecutorTest.java | 107 ++++++++--- .../failures/WorkFailureProcessorTest.java | 4 +- .../work/refresh/ActiveWorkRefresherTest.java | 4 +- 12 files changed, 363 insertions(+), 66 deletions(-) create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java new file mode 100644 index 000000000000..1ca534966947 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.streaming; + +/** + * A handle to use when requesting pulling more work from @BoundedQueueExecutor + * via @BoundedQueueExecutor.pollWork + */ +public interface BoundedQueueExecutorWorkHandle {} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java index db279f066630..ecaa673f5570 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java @@ -17,25 +17,49 @@ */ package org.apache.beam.runners.dataflow.worker.streaming; -import com.google.auto.value.AutoValue; -import java.util.function.Consumer; +import java.util.Objects; +import java.util.function.BiConsumer; +import org.apache.beam.runners.dataflow.worker.util.ExceptionUtils; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; /** {@link Work} instance and a processing function used to process the work. */ -@AutoValue -public abstract class ExecutableWork implements Runnable { +public final class ExecutableWork { - public static ExecutableWork create(Work work, Consumer executeWorkFn) { - return new AutoValue_ExecutableWork(work, executeWorkFn); + private final Work work; + private final BiConsumer executeWorkFn; + + private ExecutableWork( + Work work, BiConsumer executeWorkFn) { + this.work = Objects.requireNonNull(work); + this.executeWorkFn = Objects.requireNonNull(executeWorkFn); } - public abstract Work work(); + /** + * Creates an {@link ExecutableWork} instance. + * + * @param executeWorkFn The function executing the work. It'll be called along with a + * BoundedQueueExecutorWorkHandle. The handle needs to be passed to BoundedQueueExecutor when + * requesting more work to process inline. + */ + public static ExecutableWork create( + Work work, BiConsumer executeWorkFn) { + return new ExecutableWork(work, executeWorkFn); + } - public abstract Consumer executeWorkFn(); + public Work work() { + return work; + } - @Override - public void run() { - executeWorkFn().accept(work()); + public BiConsumer executeWorkFn() { + return executeWorkFn; + } + + public void run(BoundedQueueExecutorWorkHandle handle) { + try { + executeWorkFn().accept(work(), handle); + } catch (Throwable t) { + throw ExceptionUtils.safeWrapThrowableAsException(t); + } } public final WorkId id() { @@ -45,4 +69,9 @@ public final WorkId id() { public final Windmill.WorkItem getWorkItem() { return work().getWorkItem(); } + + @Override + public String toString() { + return "ExecutableWork{" + id() + "}"; + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java index 9079c3cc69b8..c6fd96e0a4cb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java @@ -17,6 +17,9 @@ */ package org.apache.beam.runners.dataflow.worker.util; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -24,6 +27,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.concurrent.GuardedBy; +import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle; +import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard; @@ -38,7 +45,19 @@ public class BoundedQueueExecutor { // Used to guard elementsOutstanding and bytesOutstanding. private final Monitor monitor; - private final ConcurrentLinkedQueue decrementQueue = new ConcurrentLinkedQueue<>(); + + private static class Budget { + + final int elements; + final long bytes; + + Budget(int elements, long bytes) { + this.elements = elements; + this.bytes = bytes; + } + } + + private final ConcurrentLinkedQueue decrementQueue = new ConcurrentLinkedQueue<>(); private final Object decrementQueueDrainLock = new Object(); private final AtomicBoolean isDecrementBatchPending = new AtomicBoolean(false); private int elementsOutstanding = 0; @@ -106,7 +125,7 @@ protected void afterExecute(Runnable r, Throwable t) { // Before adding a Work to the queue, check that there are enough bytes of space or no other // outstanding elements of work. - public void execute(Runnable work, long workBytes) { + public void execute(ExecutableWork work, long workBytes) { monitor.enterWhenUninterruptibly( new Guard(monitor) { @Override @@ -119,12 +138,18 @@ public boolean isSatisfied() { executeMonitorHeld(work, workBytes); } - // Forcibly add something to the queue, ignoring the length limit. - public void forceExecute(Runnable work, long workBytes) { + // Forcibly add ExecutableWork to the queue, ignoring the limits. + public void forceExecute(ExecutableWork work, long workBytes) { monitor.enter(); executeMonitorHeld(work, workBytes); } + /** Forcibly execute a Runnable callback with 0 bytes of size. */ + public void forceExecute(Runnable runnable) { + monitor.enter(); + executeMonitorHeld(runnable); + } + // Set the maximum/core pool size of the executor. public synchronized void setMaximumPoolSize(int maximumPoolSize, int maximumElementsOutstanding) { // For ThreadPoolExecutor, the maximum pool size should always greater than or equal to core @@ -221,8 +246,115 @@ public String summaryHtml() { } } - private void executeMonitorHeld(Runnable work, long workBytes) { + /** + * A handle to use when requesting pulling more work from @BoundedQueueExecutor + * via @BoundedQueueExecutor.pollWork. A single handle aggregates all budgets from work pulled for + * inline execution and releases the budget after the multi work bundle is complete. + */ + final class BoundedQueueExecutorWorkHandleImpl + implements BoundedQueueExecutorWorkHandle, AutoCloseable { + + @GuardedBy("this") + private int elements; + + @GuardedBy("this") + private long bytes; + + @GuardedBy("this") + private boolean closed = false; + + private BoundedQueueExecutorWorkHandleImpl(int elements, long bytes) { + checkArgument(elements >= 0 && bytes >= 0); + this.elements = elements; + this.bytes = bytes; + } + + /** + * Merges the budget from another handle into this handle. + * + *

    This transfers the budget (elements and bytes) from the {@code other} handle to this + * handle, and marks the {@code other} handle as closed to prevent it from releasing the budget + * again if it is closed. + */ + public void merge(BoundedQueueExecutorWorkHandleImpl other) { + checkArgumentNotNull(other); + synchronized (this) { + Preconditions.checkState(!closed, "Cannot merge into a closed handle"); + synchronized (other) { + Preconditions.checkState(!other.closed, "Cannot merge a closed handle"); + this.elements += other.elements; + this.bytes += other.bytes; + other.closed = true; + other.elements = 0; + other.bytes = 0; + } + } + } + + public synchronized boolean isClosed() { + return closed; + } + + @VisibleForTesting + synchronized int elements() { + return elements; + } + + @VisibleForTesting + synchronized long bytes() { + return bytes; + } + + @Override + public synchronized void close() { + if (closed) return; + closed = true; + decrementCounters(this.elements, this.bytes); + } + } + + private static final class QueuedWork implements Runnable { + + private final ExecutableWork work; + private final BoundedQueueExecutorWorkHandleImpl handle; + + public QueuedWork(ExecutableWork work, BoundedQueueExecutorWorkHandleImpl handle) { + this.work = work; + this.handle = handle; + } + + public ExecutableWork getWork() { + return work; + } + + public BoundedQueueExecutorWorkHandleImpl getHandle() { + return handle; + } + + @Override + public void run() { + checkArgument(!handle.isClosed()); + try (handle) { + work.run(handle); + } + } + } + + private void executeMonitorHeld(ExecutableWork work, long workBytes) { + ++elementsOutstanding; bytesOutstanding += workBytes; + monitor.leave(); + BoundedQueueExecutorWorkHandleImpl handle = + new BoundedQueueExecutorWorkHandleImpl(1, workBytes); + try { + executor.execute(new QueuedWork(work, handle)); + } catch (Throwable t) { + handle.close(); + throw ExceptionUtils.safeWrapThrowableAsException(t); + } + } + + private void executeMonitorHeld(Runnable work) { ++elementsOutstanding; monitor.leave(); @@ -232,21 +364,25 @@ private void executeMonitorHeld(Runnable work, long workBytes) { try { work.run(); } finally { - decrementCounters(workBytes); + decrementCounters(1, 0L); } }); - } catch (RuntimeException e) { - // If the execute() call threw an exception, decrement counters here. - decrementCounters(workBytes); - throw e; + } catch (Throwable t) { + decrementCounters(1, 0L); + throw ExceptionUtils.safeWrapThrowableAsException(t); } } - private void decrementCounters(long workBytes) { + @VisibleForTesting + BoundedQueueExecutorWorkHandleImpl createBudgetHandle(int elements, long bytes) { + return new BoundedQueueExecutorWorkHandleImpl(elements, bytes); + } + + private void decrementCounters(int elements, long bytes) { // All threads queue decrements and one thread grabs the monitor and updates // counters. We do this to reduce contention on monitor which is locked by // GetWork thread - decrementQueue.add(workBytes); + decrementQueue.add(new Budget(elements, bytes)); boolean submittedToExistingBatch = isDecrementBatchPending.getAndSet(true); if (submittedToExistingBatch) { // There is already a thread about to drain the decrement queue @@ -265,12 +401,12 @@ private void decrementCounters(long workBytes) { long bytesToDecrement = 0; int elementsToDecrement = 0; while (true) { - Long pollResult = decrementQueue.poll(); + Budget pollResult = decrementQueue.poll(); if (pollResult == null) { break; } - bytesToDecrement += pollResult; - ++elementsToDecrement; + bytesToDecrement += pollResult.bytes; + elementsToDecrement += pollResult.elements; } if (elementsToDecrement == 0) { return; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java new file mode 100644 index 000000000000..d04a385d6e6a --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +import javax.annotation.CheckReturnValue; +import org.apache.beam.sdk.annotations.Internal; + +/** Utility methods for simplifying work with exceptions and throwables. */ +@Internal +public final class ExceptionUtils { + + private ExceptionUtils() {} + + /** + * Returns the {@code throwable} as-is if it is an instance of {@link RuntimeException} throws if + * it is an {@link Error}, or returns the {@code throwable} wrapped in a {@code RuntimeException}. + */ + @CheckReturnValue + public static RuntimeException safeWrapThrowableAsException(Throwable throwable) { + if (throwable instanceof RuntimeException) { + return (RuntimeException) throwable; + } else if (throwable instanceof Error) { + throw (Error) throwable; + } else { + return new RuntimeException(throwable); + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java index 5a66545ab335..22573bf1ced2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizer.java @@ -156,7 +156,7 @@ public void finalizeCommits(Iterable finalizeIds) { } for (Runnable callback : callbacksToExecute) { try { - finalizationExecutor.forceExecute(callback, 0); + finalizationExecutor.forceExecute(callback); } catch (OutOfMemoryError oom) { throw oom; } catch (Throwable t) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java index 1428037d9ca0..364608be82ca 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java @@ -35,6 +35,7 @@ import org.apache.beam.runners.dataflow.worker.ReaderCache; import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException; import org.apache.beam.runners.dataflow.worker.logging.DataflowWorkerLoggingMDC; +import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle; import org.apache.beam.runners.dataflow.worker.streaming.ComputationState; import org.apache.beam.runners.dataflow.worker.streaming.ComputationWorkExecutor; import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; @@ -47,6 +48,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory; import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; +import org.apache.beam.runners.dataflow.worker.util.ExceptionUtils; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution; import org.apache.beam.runners.dataflow.worker.windmill.client.commits.Commit; @@ -218,7 +220,7 @@ public void scheduleWork( ExecutableWork.create( Work.create( workItem, serializedWorkItemSize, watermarks, processingContext, drainMode, clock), - work -> processWork(computationState, work, getWorkStreamLatencies))); + (work, handle) -> processWork(computationState, work, getWorkStreamLatencies, handle))); } /** Adds any applied finalize ids to the commit finalizer to have their callbacks executed. */ @@ -232,16 +234,19 @@ public void queueAppliedFinalizeIds(ImmutableList appliedFinalizeIds) { * internally if processing fails due to uncaught {@link Exception}(s). * * @implNote This will block the calling thread during execution of user DoFns. + * @param handle handled to pass to BoundedQueueExecutor.pollWork, currently unused */ private void processWork( ComputationState computationState, Work work, - ImmutableList getWorkStreamLatencies) { + ImmutableList getWorkStreamLatencies, + BoundedQueueExecutorWorkHandle handle) { work.recordGetWorkStreamLatencies(getWorkStreamLatencies); - processWork(computationState, work); + processWork(computationState, work, handle); } - private void processWork(ComputationState computationState, Work work) { + private void processWork( + ComputationState computationState, Work work, BoundedQueueExecutorWorkHandle unusedHandle) { Windmill.WorkItem workItem = work.getWorkItem(); String computationId = computationState.getComputationId(); ByteString key = workItem.getKey(); @@ -289,7 +294,7 @@ private void processWork(ComputationState computationState, Work work) { try { workFailureProcessor.logAndProcessFailure( computationId, - ExecutableWork.create(work, retry -> processWork(computationState, retry)), + ExecutableWork.create(work, (retry, h) -> processWork(computationState, retry, h)), t, invalidWork -> computationState.completeWorkAndScheduleNextWorkForKey( @@ -297,7 +302,8 @@ private void processWork(ComputationState computationState, Work work) { } catch (OutOfMemoryError oom) { throw oom; } catch (Throwable t2) { - throw new RuntimeException(t2); + LOG.warn("Failed to process work failure safely for work {}", work.id(), t2); + throw ExceptionUtils.safeWrapThrowableAsException(t2); } } finally { // Update total processing time counters. Updating in finally clause ensures that diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java index ff82a1ab5c4c..d58f20076994 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java @@ -373,7 +373,9 @@ private static ExecutableWork createMockWork( computationId, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), false, Instant::now), - processWorkFn); + (work, handle) -> { + processWorkFn.accept(work); + }); } private byte[] intervalWindowBytes(IntervalWindow window) throws Exception { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java index 865ae2612803..0f14efdd0c0b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java @@ -73,7 +73,7 @@ private static ExecutableWork createWork(Windmill.WorkItem workItem) { createWorkProcessingContext(), false, Instant::now), - ignored -> {}); + (work, handle) -> {}); } private static ExecutableWork expiredWork(Windmill.WorkItem workItem) { @@ -85,7 +85,7 @@ private static ExecutableWork expiredWork(Windmill.WorkItem workItem) { createWorkProcessingContext(), false, () -> Instant.EPOCH), - ignored -> {}); + (work, handle) -> {}); } private static Work.ProcessingContext createWorkProcessingContext() { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java index 1c8b8fca131d..30ad97140e1e 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java @@ -77,7 +77,7 @@ private static ExecutableWork createWork(ShardedKey shardedKey, long workToken, mock(HeartbeatSender.class)), false, Instant::now), - ignored -> {}); + (work, handle) -> {}); } @Before diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java index d7ea039bb809..55fe82c7163c 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java @@ -32,6 +32,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; import org.apache.beam.runners.dataflow.worker.streaming.Watermarks; import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.BoundedQueueExecutorWorkHandleImpl; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; @@ -85,18 +86,21 @@ private static ExecutableWork createWork(Consumer executeWorkFn) { mock(HeartbeatSender.class)), false, Instant::now), - executeWorkFn); + (work, handle) -> { + executeWorkFn.accept(work); + }); } - private Runnable createSleepProcessWorkFn(CountDownLatch start, CountDownLatch stop) { - return () -> { - start.countDown(); - try { - stop.await(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; + private ExecutableWork createSleepProcessWork(CountDownLatch start, CountDownLatch stop) { + return createWork( + ignored -> { + start.countDown(); + try { + stop.await(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } @Before @@ -123,9 +127,9 @@ public void testScheduleWorkWhenExceedMaximumPoolSize() throws Exception { CountDownLatch processStop2 = new CountDownLatch(1); CountDownLatch processStart3 = new CountDownLatch(1); CountDownLatch processStop3 = new CountDownLatch(1); - Runnable m1 = createSleepProcessWorkFn(processStart1, processStop1); - Runnable m2 = createSleepProcessWorkFn(processStart2, processStop2); - Runnable m3 = createSleepProcessWorkFn(processStart3, processStop3); + ExecutableWork m1 = createSleepProcessWork(processStart1, processStop1); + ExecutableWork m2 = createSleepProcessWork(processStart2, processStop2); + ExecutableWork m3 = createSleepProcessWork(processStart3, processStop3); executor.execute(m1, 1); processStart1.await(); @@ -152,8 +156,8 @@ public void testScheduleWorkWhenExceedMaximumBytesOutstanding() throws Exception CountDownLatch processStop1 = new CountDownLatch(1); CountDownLatch processStart2 = new CountDownLatch(1); CountDownLatch processStop2 = new CountDownLatch(1); - Runnable m1 = createSleepProcessWorkFn(processStart1, processStop1); - Runnable m2 = createSleepProcessWorkFn(processStart2, processStop2); + ExecutableWork m1 = createSleepProcessWork(processStart1, processStop1); + ExecutableWork m2 = createSleepProcessWork(processStart2, processStop2); executor.execute(m1, 10000000); processStart1.await(); @@ -187,9 +191,9 @@ public void testOverrideMaximumPoolSize() throws Exception { CountDownLatch processStart2 = new CountDownLatch(1); CountDownLatch processStart3 = new CountDownLatch(1); CountDownLatch stop = new CountDownLatch(1); - Runnable m1 = createSleepProcessWorkFn(processStart1, stop); - Runnable m2 = createSleepProcessWorkFn(processStart2, stop); - Runnable m3 = createSleepProcessWorkFn(processStart3, stop); + ExecutableWork m1 = createSleepProcessWork(processStart1, stop); + ExecutableWork m2 = createSleepProcessWork(processStart2, stop); + ExecutableWork m3 = createSleepProcessWork(processStart3, stop); // Initial state. assertEquals(0, executor.activeCount()); @@ -225,9 +229,9 @@ public void testRecordTotalTimeMaxActiveThreadsUsed() throws Exception { CountDownLatch processStart2 = new CountDownLatch(1); CountDownLatch processStart3 = new CountDownLatch(1); CountDownLatch stop = new CountDownLatch(1); - Runnable m1 = createSleepProcessWorkFn(processStart1, stop); - Runnable m2 = createSleepProcessWorkFn(processStart2, stop); - Runnable m3 = createSleepProcessWorkFn(processStart3, stop); + ExecutableWork m1 = createSleepProcessWork(processStart1, stop); + ExecutableWork m2 = createSleepProcessWork(processStart2, stop); + ExecutableWork m3 = createSleepProcessWork(processStart3, stop); // Initial state. assertEquals(0, executor.activeCount()); @@ -264,9 +268,9 @@ public void testRecordTotalTimeMaxActiveThreadsUsedWhenMaximumPoolSizeIsIncrease CountDownLatch processStart2 = new CountDownLatch(1); CountDownLatch processStart3 = new CountDownLatch(1); CountDownLatch stop = new CountDownLatch(1); - Runnable m1 = createSleepProcessWorkFn(processStart1, stop); - Runnable m2 = createSleepProcessWorkFn(processStart2, stop); - Runnable m3 = createSleepProcessWorkFn(processStart3, stop); + ExecutableWork m1 = createSleepProcessWork(processStart1, stop); + ExecutableWork m2 = createSleepProcessWork(processStart2, stop); + ExecutableWork m3 = createSleepProcessWork(processStart3, stop); // Initial state. assertEquals(0, executor.activeCount()); @@ -308,9 +312,9 @@ public void testRecordTotalTimeMaxActiveThreadsUsedWhenMaximumPoolSizeIsReduced( CountDownLatch processStop2 = new CountDownLatch(1); CountDownLatch processStart3 = new CountDownLatch(1); CountDownLatch processStop3 = new CountDownLatch(1); - Runnable m1 = createSleepProcessWorkFn(processStart1, processStop1); - Runnable m2 = createSleepProcessWorkFn(processStart2, processStop2); - Runnable m3 = createSleepProcessWorkFn(processStart3, processStop3); + ExecutableWork m1 = createSleepProcessWork(processStart1, processStop1); + ExecutableWork m2 = createSleepProcessWork(processStart2, processStop2); + ExecutableWork m3 = createSleepProcessWork(processStart3, processStop3); // Initial state. assertEquals(0, executor.activeCount()); @@ -351,6 +355,55 @@ public void testRecordTotalTimeMaxActiveThreadsUsedWhenMaximumPoolSizeIsReduced( executor.shutdown(); } + @Test + public void testRunnableExceptionPropagationDecrementsCounters() throws Exception { + CountDownLatch processStart = new CountDownLatch(1); + CountDownLatch processStop = new CountDownLatch(1); + + Runnable work = + () -> { + processStart.countDown(); + try { + processStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("Simulated finalizer processing exception"); + }; + + executor.forceExecute(work); + processStart.await(); + + assertEquals(1, executor.elementsOutstanding()); + + processStop.countDown(); + + // Wait until outstanding elements are released + while (executor.elementsOutstanding() != 0) { + Thread.sleep(10); + } + + assertEquals(0, executor.elementsOutstanding()); + } + + @Test + public void testHandleMerge() throws Exception { + BoundedQueueExecutorWorkHandleImpl handle1 = executor.createBudgetHandle(1, 100L); + BoundedQueueExecutorWorkHandleImpl handle2 = executor.createBudgetHandle(2, 200L); + + handle1.merge(handle2); + + // Verify that handle2 has 0 budget and is closed. + assertEquals(0, handle2.elements()); + assertEquals(0, handle2.bytes()); + assertTrue(handle2.isClosed()); + + // Verify that handle1 has the combined budget and is not closed. + assertEquals(3, handle1.elements()); + assertEquals(300L, handle1.bytes()); + assertFalse(handle1.isClosed()); + } + @Test public void testRenderSummaryHtml() { String expectedSummaryHtml = diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java index 68a11895fa12..51bd4816b031 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java @@ -98,7 +98,9 @@ private static ExecutableWork createWork(Supplier clock, Consumer mock(HeartbeatSender.class)), false, clock), - processWorkFn); + (work, handle) -> { + processWorkFn.accept(work); + }); } private static ExecutableWork createWork(Consumer processWorkFn) { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java index 054db878c869..88a82c6f76b6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java @@ -137,7 +137,9 @@ private ExecutableWork createOldWork( "computationId", new FakeGetDataClient(), ignored -> {}, heartbeatSender), false, ActiveWorkRefresherTest::aLongTimeAgo), - processWork); + (work, handle) -> { + processWork.accept(work); + }); } @Test From e2cb9902927f1d14bc24840191eb1e03ef9e07b1 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 2 Jun 2026 04:24:54 -0400 Subject: [PATCH 285/490] Fix truncating file handle (#38425) --- sdks/python/apache_beam/dataframe/io.py | 15 ++++++- sdks/python/apache_beam/dataframe/io_test.py | 45 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/sdks/python/apache_beam/dataframe/io.py b/sdks/python/apache_beam/dataframe/io.py index cabf6ccd5c88..21eab0b82faf 100644 --- a/sdks/python/apache_beam/dataframe/io.py +++ b/sdks/python/apache_beam/dataframe/io.py @@ -515,7 +515,7 @@ def seekable(self): @property def closed(self): - return False + return getattr(self._underlying, 'closed', False) def __iter__(self): # For pandas is_file_like. @@ -584,7 +584,18 @@ def _read(self, size=-1): return res def flush(self): - self._underlying.flush() + if not self.closed: + try: + self._underlying.flush() + except ValueError: + pass + + def close(self): + if not self.closed and hasattr(self._underlying, 'close'): + try: + self._underlying.close() + except (OSError, ValueError): + pass class _ReadFromPandasDoFn(beam.DoFn, beam.RestrictionProvider): diff --git a/sdks/python/apache_beam/dataframe/io_test.py b/sdks/python/apache_beam/dataframe/io_test.py index 313d955b4550..4cd502d1b8d7 100644 --- a/sdks/python/apache_beam/dataframe/io_test.py +++ b/sdks/python/apache_beam/dataframe/io_test.py @@ -296,6 +296,51 @@ def test_truncating_filehandle_iter(self): self._run_truncating_file_handle_iter_test('aaa b cccccccccccccccccccc') self._run_truncating_file_handle_iter_test('aaa b ccccccccccccccccc ') + def test_truncating_filehandle_flush_on_closed_stream(self): + class ClosedFlushingStream(StringIO): + def flush(self): + if self.closed: + raise ValueError("I/O operation on closed file.") + super().flush() + + s = 'a b c' + tracker = restriction_trackers.OffsetRestrictionTracker( + restriction_trackers.OffsetRange(0, len(s))) + underlying = ClosedFlushingStream(s) + handle = io._TruncatingFileHandle( + underlying, tracker, splitter=io._DelimSplitter(' ', 10)) + + # Verify that calling flush() when the underlying stream is closed + # succeeds without raising ValueError. + underlying.close() + handle.flush() + handle.close() + + def test_truncating_filehandle_exception_suppression(self): + class FaultyStream(StringIO): + @property + def closed(self): + return False + + def flush(self): + raise ValueError("Simulated flush error") + + def close(self): + raise OSError("Simulated close error") + + s = 'a b c' + tracker = restriction_trackers.OffsetRestrictionTracker( + restriction_trackers.OffsetRange(0, len(s))) + underlying = FaultyStream(s) + handle = io._TruncatingFileHandle( + underlying, tracker, splitter=io._DelimSplitter(' ', 10)) + + # Verify that ValueError raised during flush() is safely suppressed. + handle.flush() + + # Verify that OSError raised during close() is safely suppressed. + handle.close() + @parameterized.expand([ ('defaults', {}), ('header', dict(header=1)), From a3be67f5841d677aa3a656a733c6bc80f1cb0ede Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 2 Jun 2026 10:27:33 -0400 Subject: [PATCH 286/490] js2py to quickjs (#38473) * js2py to quickjs fix SomeTransform picklability in readme_test.py fix lint address gemini comments * address more gemini comments * use more quickjs.Function * lint * fix another lint * fix more gemini comments * first gemini fixes * address more gemini * gemini comments * remove comment --- sdks/python/apache_beam/yaml/readme_test.py | 16 +- sdks/python/apache_beam/yaml/yaml_mapping.py | 171 ++++++++++-------- sdks/python/apache_beam/yaml/yaml_udf_test.py | 119 +++++++++++- sdks/python/setup.py | 3 +- 4 files changed, 215 insertions(+), 94 deletions(-) diff --git a/sdks/python/apache_beam/yaml/readme_test.py b/sdks/python/apache_beam/yaml/readme_test.py index ad25111bd229..7596da15171c 100644 --- a/sdks/python/apache_beam/yaml/readme_test.py +++ b/sdks/python/apache_beam/yaml/readme_test.py @@ -132,15 +132,17 @@ def expand(self, pcoll): lambda _: 1, sum, 'count') -class _Fakes: - fn = str +class SomeTransform(beam.PTransform): + def __init__(self, *args, **kwargs): + super().__init__() - class SomeTransform(beam.PTransform): - def __init__(*args, **kwargs): - pass + def expand(self, pcoll): + return pcoll - def expand(self, pcoll): - return pcoll + +class _Fakes: + fn = str + SomeTransform = SomeTransform RENDER_DIR = None diff --git a/sdks/python/apache_beam/yaml/yaml_mapping.py b/sdks/python/apache_beam/yaml/yaml_mapping.py index a6b2b5704751..6bf2ad3fa13f 100644 --- a/sdks/python/apache_beam/yaml/yaml_mapping.py +++ b/sdks/python/apache_beam/yaml/yaml_mapping.py @@ -16,13 +16,17 @@ # """This module defines the basic MapToFields operation.""" +import datetime import itertools +import json import re +import threading from collections import abc from collections.abc import Callable from collections.abc import Collection from collections.abc import Iterable from collections.abc import Mapping +from decimal import Decimal from typing import Any from typing import NamedTuple from typing import Optional @@ -53,13 +57,11 @@ from apache_beam.yaml.yaml_errors import maybe_with_exception_handling_transform_fn from apache_beam.yaml.yaml_provider import dicts_to_rows -# Import js2py package if it exists +# Import quickjs package if it exists try: - import js2py - from js2py.base import JsObjectWrapper + import quickjs except ImportError: - js2py = None - JsObjectWrapper = object + quickjs = None _str_expression_fields = { 'AssignTimestamps': 'timestamp', @@ -178,18 +180,52 @@ def _check_mapping_arguments( raise ValueError(f'{transform_name} cannot specify "name" without "path"') -# js2py's JsObjectWrapper object has a self-referencing __dict__ property -# that cannot be pickled without implementing the __getstate__ and -# __setstate__ methods. -class _CustomJsObjectWrapper(JsObjectWrapper): - def __init__(self, js_obj): - super().__init__(js_obj.__dict__['_obj']) +_THREAD_LOCAL_JS_CACHE = threading.local() - def __getstate__(self): - return self.__dict__.copy() - def __setstate__(self, state): - self.__dict__.update(state) +class _JsFunctionWrapper: + def __init__(self, source_code, entrypoint_name): + self.source_code = source_code + self.entrypoint_name = entrypoint_name + + def _get_fn(self): + cache = _THREAD_LOCAL_JS_CACHE + if not hasattr(cache, 'functions'): + cache.functions = {} + + cache_key = (self.source_code, self.entrypoint_name) + if cache_key not in cache.functions: + context = quickjs.Context() + context.eval(self.source_code) + f = context.get(self.entrypoint_name) + + def call_fn(*args, run_gc=True): + def convert_arg(arg): + if isinstance(arg, (type(None), str, bool, float, int)): + return arg + else: + return context.parse_json(json.dumps(arg)) + + try: + result = f(*[convert_arg(a) for a in args]) + if isinstance(result, quickjs.Object): + result = json.loads(result.json()) + return result + finally: + if run_gc: + context.gc() + + cache.functions[cache_key] = call_fn + + return cache.functions[cache_key] + + def __call__(self, row): + fn = self._get_fn() + try: + return dicts_to_rows(fn(py_value_to_js_dict(row))) + except Exception as exn: + raise RuntimeError( + f"Error evaluating javascript expression: {exn}") from exn # TODO(yaml) Improve type inferencing for JS UDF's @@ -199,6 +235,12 @@ def py_value_to_js_dict(py_value): py_value = py_value._asdict() if isinstance(py_value, dict): return {key: py_value_to_js_dict(value) for key, value in py_value.items()} + elif isinstance(py_value, bytes): + return py_value.decode('utf-8', errors='replace') + elif isinstance(py_value, (datetime.datetime, datetime.date, datetime.time)): + return {'__date__': True, 'value': py_value.isoformat()} + elif isinstance(py_value, Decimal): + return float(py_value) elif not isinstance(py_value, str) and isinstance(py_value, abc.Iterable): return [py_value_to_js_dict(value) for value in list(py_value)] else: @@ -210,80 +252,53 @@ def py_value_to_js_dict(py_value): def _expand_javascript_mapping_func( original_fields, expression=None, callable=None, path=None, name=None): - # Check for installed js2py package - if js2py is None: + if quickjs is None: raise ValueError( - "Javascript mapping functions are not supported on" - " Python 3.12 or later.") - - # import remaining js2py objects - from js2py import base - from js2py.constructors import jsdate - from js2py.internals import simplex - - js_array_type = ( - base.PyJsArray, - base.PyJsArrayBuffer, - base.PyJsInt8Array, - base.PyJsUint8Array, - base.PyJsUint8ClampedArray, - base.PyJsInt16Array, - base.PyJsUint16Array, - base.PyJsInt32Array, - base.PyJsUint32Array, - base.PyJsFloat32Array, - base.PyJsFloat64Array) - - def _js_object_to_py_object(obj): - if isinstance(obj, (base.PyJsNumber, base.PyJsString, base.PyJsBoolean)): - return base.to_python(obj) - elif isinstance(obj, js_array_type): - return [_js_object_to_py_object(value) for value in obj.to_list()] - elif isinstance(obj, jsdate.PyJsDate): - return obj.to_utc_dt() - elif isinstance(obj, (base.PyJsNull, base.PyJsUndefined)): - return None - elif isinstance(obj, base.PyJsError): - raise RuntimeError(obj['message']) - elif isinstance(obj, base.PyJsObject): - return { - key: _js_object_to_py_object(value['value']) - for (key, value) in obj.own.items() - } - elif isinstance(obj, base.JsObjectWrapper): - return _js_object_to_py_object(obj._obj) - - return obj + "Javascript mapping functions are not supported because the " + "quickjs-ng library is not installed.") if expression: - source = '\n'.join(['function(__row__) {'] + [ - f' {name} = __row__.{name}' - for name in original_fields if name in expression - ] + [' return (' + expression + ')'] + ['}']) - js_func = _CustomJsObjectWrapper(js2py.eval_js(source)) + source_code = f""" + function udf(__row__) {{ + with (__row__) {{ + return ({expression}); + }} + }} + """ + user_entrypoint = 'udf' elif callable: - js_func = _CustomJsObjectWrapper(js2py.eval_js(callable)) + source_code = f"var __udf__ = ({callable});" + user_entrypoint = '__udf__' else: if not path.endswith('.js'): raise ValueError(f'File "{path}" is not a valid .js file.') udf_code = FileSystems.open(path).read().decode() - js = js2py.EvalJs() - js.eval(udf_code) - js_func = _CustomJsObjectWrapper(getattr(js, name)) - - def js_wrapper(row): - row_as_dict = py_value_to_js_dict(row) - try: - js_result = js_func(row_as_dict) - except simplex.JsException as exn: - raise RuntimeError( - f"Error evaluating javascript expression: " - f"{exn.mes['message']}") from exn - return dicts_to_rows(_js_object_to_py_object(js_result)) + source_code = udf_code + user_entrypoint = name + + source_code += f""" + function __convert_dates__(obj) {{ + if (obj && typeof obj === 'object') {{ + if (obj.__date__) {{ + return new Date(obj.value); + }} + for (var key in obj) {{ + if (obj.hasOwnProperty(key)) {{ + obj[key] = __convert_dates__(obj[key]); + }} + }} + }} + return obj; + }} + + function __wrapper__(row) {{ + return {user_entrypoint}(__convert_dates__(row)); + }} + """ - return js_wrapper + return _JsFunctionWrapper(source_code, '__wrapper__') def _expand_python_mapping_func( diff --git a/sdks/python/apache_beam/yaml/yaml_udf_test.py b/sdks/python/apache_beam/yaml/yaml_udf_test.py index 3d664ab9de41..81f033706afc 100644 --- a/sdks/python/apache_beam/yaml/yaml_udf_test.py +++ b/sdks/python/apache_beam/yaml/yaml_udf_test.py @@ -14,11 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import datetime import logging import os import shutil import tempfile import unittest +from decimal import Decimal import apache_beam as beam from apache_beam.io import localfilesystem @@ -32,10 +34,10 @@ from apache_beam.yaml.yaml_transform import YamlTransform try: - import js2py + import quickjs except ImportError: - js2py = None - logging.warning('js2py is not installed; some tests will be skipped.') + quickjs = None + logging.warning('quickjs-ng is not installed; some tests will be skipped.') def as_rows(): @@ -63,7 +65,7 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) - @unittest.skipIf(js2py is None, 'js2py not installed.') + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') def test_map_to_fields_filter_inline_js(self): with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( pickle_library='cloudpickle', yaml_experimental_features=['javascript' @@ -109,6 +111,46 @@ def test_map_to_fields_filter_inline_js(self): row=beam.Row(rank=2, values=[7, 8, 9, 12])), ])) + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') + def test_map_to_fields_js_callable_styles(self): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle', yaml_experimental_features=['javascript' + ])) as p: + elements = p | beam.Create(self.data) + result = elements | YamlTransform( + ''' + type: MapToFields + config: + language: javascript + fields: + label: + callable: "(x) => x.label + 'x'" + conductor: + callable: "function(x) { return x.conductor + 1 }" + row: + callable: | + (x) => { + x.row.values.push(x.row.rank + 10) + return x.row + } + ''') + assert_that( + result, + equal_to([ + beam.Row( + label='11ax', + conductor=12, + row=beam.Row(rank=0, values=[1, 2, 3, 10])), + beam.Row( + label='37ax', + conductor=38, + row=beam.Row(rank=1, values=[4, 5, 6, 11])), + beam.Row( + label='389ax', + conductor=390, + row=beam.Row(rank=2, values=[7, 8, 9, 12])), + ])) + def test_map_to_fields_filter_inline_py(self): with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( pickle_library='cloudpickle')) as p: @@ -197,7 +239,7 @@ def test_map_to_fields_sql_reserved_keyword_append(): beam.Row(label='389a', timestamp=2, label_copy="389a"), ])) - @unittest.skipIf(js2py is None, 'js2py not installed.') + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') def test_filter_inline_js(self): with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( pickle_library='cloudpickle', yaml_experimental_features=['javascript' @@ -252,7 +294,7 @@ def test_filter_inline_py(self): row=beam.Row(rank=2, values=[7, 8, 9])), ])) - @unittest.skipIf(js2py is None, 'js2py not installed.') + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') def test_filter_expression_js(self): with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( pickle_library='cloudpickle', yaml_experimental_features=['javascript' @@ -296,7 +338,7 @@ def test_filter_expression_py(self): row=beam.Row(rank=0, values=[1, 2, 3])), ])) - @unittest.skipIf(js2py is None, 'js2py not installed.') + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') def test_filter_inline_js_file(self): data = ''' function f(x) { @@ -374,6 +416,69 @@ def g(x): row=beam.Row(rank=2, values=[7, 8, 9])), ])) + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') + def test_map_to_fields_js_non_serializable_types(self): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle', yaml_experimental_features=['javascript' + ])) as p: + data = [ + beam.Row( + b=b'hello', + dt=datetime.datetime(2026, 5, 14, 12, 0, 0), + dec=Decimal('10.5')) + ] + elements = p | beam.Create(data) + result = elements | YamlTransform( + ''' + type: MapToFields + config: + language: javascript + fields: + b_out: + expression: "b + '_world'" + dt_out: + expression: "dt" + dt_year: + expression: "dt.getFullYear()" + dt_month: + expression: "dt.getMonth() + 1" + dt_date: + expression: "dt.getDate()" + dec_out: + expression: "dec * 2" + ''') + assert_that( + result, + equal_to([ + beam.Row( + b_out='hello_world', + dt_out='2026-05-14T12:00:00.000Z', + dt_year=2026, + dt_month=5, + dt_date=14, + dec_out=21.0), + ])) + + @unittest.skipIf(quickjs is None, 'quickjs-ng not installed.') + def test_map_to_fields_js_robustness(self): + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle', yaml_experimental_features=['javascript' + ])) as p: + data = [beam.Row(a=100, x=-42)] + elements = p | beam.Create(data) + result = elements | YamlTransform( + ''' + type: MapToFields + config: + language: javascript + fields: + abs_val: + expression: "Math.abs(x)" + ''') + assert_that(result, equal_to([ + beam.Row(abs_val=42), + ])) + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) diff --git a/sdks/python/setup.py b/sdks/python/setup.py index 961f890e1edf..a508b4c07fb3 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -639,8 +639,7 @@ def get_portability_package_data(): 'docstring-parser>=0.15,<1.0', 'jinja2>=3.0,<3.2', 'virtualenv-clone>=0.5,<1.0', - # https://github.com/PiotrDabkowski/Js2Py/issues/317 - 'js2py>=0.74,<1; python_version<"3.12"', + 'quickjs-ng>=0.14.0,<1.0.0', 'jsonschema>=4.0.0,<5.0.0', ] + dataframe_dependency, # Keep the following dependencies in line with what we test against From fd7af76025919d49dfdc5452feb21b69896d22f2 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:40:52 -0400 Subject: [PATCH 287/490] Migrate to Google Cloud Dataflow Client (#37639) * [WIP] Migrate to Google Cloud Dataflow Client rebase * Trigger relevant postcommits * base image update * fix camel case * update dataflow runner + tests * slide import to avoid triggering unit tests * yapf stuff * remove extra print * further spec structs, fix incorrect piplineUrl option, remove old client code * suppress line-too-longs * formatting * linting, tweak metrics tests * Proto-specific changes to metric processing tests * try to dump logging * handle more straightforward metrics values * add skips since the unit tests now depend on the proto library * testing if there's a disconnect between proto behavior locally and in tests * correct scalar access * clean up dist accesses * linting, various fixes * fix unit test setup for direct accesses * linting * more linting * re-enable histograms * Bump dataflow client version, restore pausing/paused concept * formatting * fix enum selection * handle the proto hash PR * remove unnecessary try/except block * re-delete old messsages * fix disk_provisioned_iops/throughput_mibps tests * code bot suggestions * revert pipeline options interaction * leftover update * yapf * swap credential loading * Fix message importance parsing * fix apiclient_test case w/ cloud deps and no auth * Local runner SSL fix * last local change * apitools test fix * yapf * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * gemini suggestions * fix message importance * fix local runner case * remove legacy translation maps * revert container requirements changes * missing import * fix bad trigger file merge * Update sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py Co-authored-by: Danny McCormick * fix merge problem with runnerv2 disabled tests * fix state breakage --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Danny McCormick --- .../trigger_files/beam_PostCommit_Python.json | 2 +- ...ommit_Python_ValidatesRunner_Dataflow.json | 2 +- .../runners/dataflow/dataflow_metrics.py | 63 +- .../runners/dataflow/dataflow_metrics_test.py | 314 +- .../runners/dataflow/dataflow_runner.py | 56 +- .../runners/dataflow/dataflow_runner_test.py | 39 +- .../runners/dataflow/internal/apiclient.py | 517 +- .../dataflow/internal/apiclient_test.py | 292 +- .../internal/clients/dataflow/__init__.py | 34 - .../clients/dataflow/dataflow_v1b3_client.py | 1318 --- .../dataflow/dataflow_v1b3_messages.py | 8077 ----------------- .../clients/dataflow/message_matchers.py | 118 - .../clients/dataflow/message_matchers_test.py | 74 - sdks/python/setup.py | 1 + 14 files changed, 506 insertions(+), 10401 deletions(-) delete mode 100644 sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/__init__.py delete mode 100644 sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py delete mode 100644 sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py delete mode 100644 sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers.py delete mode 100644 sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers_test.py diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 931ae76b69d5..4f79e635cc02 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "38069", - "modification": 42 + "modification": 43 } diff --git a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json index b26833333238..c537844dc84a 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 2 + "modification": 3 } diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py b/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py index 8f72d45060ca..cf86d6c8b58d 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_metrics.py @@ -57,7 +57,6 @@ def _get_match(proto, filter_fn): # V1b3 MetricStructuredName keys to accept and copy to the MetricKey labels. -STEP_LABEL = 'step' STRUCTURED_NAME_LABELS = set( ['execution_step', 'original_name', 'output_user_name']) @@ -112,9 +111,7 @@ def _translate_step_name(self, internal_name): try: step = _get_match( self._job_graph.proto.steps, lambda x: x.name == internal_name) - user_step_name = _get_match( - step.properties.additionalProperties, - lambda x: x.key == 'user_name').value.string_value + user_step_name = step.properties.get('user_name') except ValueError: pass # Exception is handled below. if not user_step_name: @@ -135,24 +132,17 @@ def _get_metric_key(self, metric): # step name (only happens for unstructured-named metrics). # 2. Unable to unpack [step] or [namespace]; which should only happen # for unstructured names. - step = _get_match( - metric.name.context.additionalProperties, - lambda x: x.key == STEP_LABEL).value + step = metric.name.context['step'] step = self._translate_step_name(step) - except ValueError: + except (KeyError, ValueError): pass namespace = "dataflow/v1b3" # Try to extract namespace or add a default. - try: - namespace = _get_match( - metric.name.context.additionalProperties, - lambda x: x.key == 'namespace').value - except ValueError: - pass + namespace = metric.name.context.get('namespace', 'dataflow/v1b3') - for kv in metric.name.context.additionalProperties: - if kv.key in STRUCTURED_NAME_LABELS: - labels[kv.key] = kv.value + for key in metric.name.context: + if key in STRUCTURED_NAME_LABELS: + labels[key] = metric.name.context[key] # Package everything besides namespace and name the labels as well, # including unmodified step names to assist in integration the exact # unmodified values which come from dataflow. @@ -185,10 +175,7 @@ def _populate_metrics(self, response, result, user_metrics=False): # in the service. # The second way is only useful for the UI, and should be ignored. continue - is_tentative = [ - prop for prop in metric.name.context.additionalProperties - if prop.key == 'tentative' and prop.value == 'true' - ] + is_tentative = metric.name.context.get('tentative') == 'true' tentative_or_committed = 'tentative' if is_tentative else 'committed' metric_key = self._get_metric_key(metric) @@ -209,32 +196,16 @@ def _get_metric_value(self, metric): return None if metric.scalar is not None: - return metric.scalar.integer_value + # This will always be a single value if there is any data in the field. + val = metric.scalar + if isinstance(val, float) and val.is_integer(): + return int(val) + return val elif metric.distribution is not None: - dist_count = _get_match( - metric.distribution.object_value.properties, - lambda x: x.key == 'count').value.integer_value - dist_min = _get_match( - metric.distribution.object_value.properties, - lambda x: x.key == 'min').value.integer_value - dist_max = _get_match( - metric.distribution.object_value.properties, - lambda x: x.key == 'max').value.integer_value - dist_sum = _get_match( - metric.distribution.object_value.properties, - lambda x: x.key == 'sum').value.integer_value - if dist_sum is None: - # distribution metric is not meant to use on large values, but in case - # it is, the value can overflow and become double_value, the correctness - # of the value may not be guaranteed. - _LOGGER.info( - "Distribution metric sum value seems to have " - "overflowed integer_value range, the correctness of sum or mean " - "value may not be guaranteed: %s" % metric.distribution) - dist_sum = int( - _get_match( - metric.distribution.object_value.properties, - lambda x: x.key == 'sum').value.double_value) + dist_count = int(metric.distribution['count']) + dist_min = int(metric.distribution['min']) + dist_max = int(metric.distribution['max']) + dist_sum = int(metric.distribution['sum']) return DistributionResult( DistributionData(dist_sum, dist_count, dist_min, dist_max)) #TODO(https://github.com/apache/beam/issues/31788) support StringSet after diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_metrics_test.py b/sdks/python/apache_beam/runners/dataflow/dataflow_metrics_test.py index ed1cd1500823..00ee324d1f3d 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_metrics_test.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_metrics_test.py @@ -27,6 +27,7 @@ import unittest import mock +from google.protobuf import json_format from apache_beam import DoFn from apache_beam import ParDo @@ -46,24 +47,15 @@ # Protect against environments where apitools library is not available. # pylint: disable=wrong-import-order, wrong-import-position try: + from google.cloud import dataflow + from apache_beam.runners.dataflow.internal import apiclient except ImportError: apiclient = None # type: ignore + dataflow = None # type: ignore # pylint: enable=wrong-import-order, wrong-import-position -class DictToObject(object): - """Translate from a dict(list()) structure to an object structure""" - def __init__(self, data): - for name, value in data.items(): - setattr(self, name, self._wrap(value)) - - def _wrap(self, value): - if isinstance(value, (tuple, list, set, frozenset)): - return type(value)([self._wrap(v) for v in value]) - return DictToObject(value) if isinstance(value, dict) else value - - class TestDataflowMetrics(unittest.TestCase): # TODO(https://github.com/apache/beam/issues/19258): Write a dump tool to @@ -73,85 +65,49 @@ class TestDataflowMetrics(unittest.TestCase): { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }, - { - "key": "tentative", - "value": "true" - }] + "namespace": "__main__.WordExtractingDoFn", + "step": "s2", + "tentative": "true" }, "name": "words", "origin": "user" }, - "scalar": { - "integer_value": 26185 - }, - "distribution": None, + "scalar": 26185, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }] + "namespace": "__main__.WordExtractingDoFn", "step": "s2" }, "name": "words", "origin": "user" }, - "scalar": { - "integer_value": 26181 - }, - "distribution": None, + "scalar": 26181, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }, - { - "key": "tentative", - "value": "true" - }] + "namespace": "__main__.WordExtractingDoFn", + "step": "s2", + "tentative": "true" }, "name": "empty_lines", "origin": "user" }, - "scalar": { - "integer_value": 1080 - }, - "distribution": None, + "scalar": 1080, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }] + "namespace": "__main__.WordExtractingDoFn", "step": "s2" }, "name": "empty_lines", "origin": "user" }, - "scalar": { - "integer_value": 1080 - }, - "distribution": None, + "scalar": 1080, "updateTime": "2017-03-22T18:47:06.402Z" }, ] @@ -161,140 +117,62 @@ class TestDataflowMetrics(unittest.TestCase): { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }, - { - "key": "tentative", - "value": "true" - }] + "namespace": "__main__.WordExtractingDoFn", + "step": "s2", + "tentative": "true" }, "name": "word_lengths", "origin": "user" }, - "scalar": { - "integer_value": 109475 - }, - "distribution": None, + "scalar": 109475, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }] + "namespace": "__main__.WordExtractingDoFn", "step": "s2" }, "name": "word_lengths", "origin": "user" }, - "scalar": { - "integer_value": 109475 - }, - "distribution": None, + "scalar": 109475, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }, - { - "key": "tentative", - "value": "true" - }] + "namespace": "__main__.WordExtractingDoFn", + "step": "s2", + "tentative": "true" }, "name": "word_length_dist", "origin": "user" }, "scalar": None, "distribution": { - "object_value": { - "properties": [ - { - "key": "min", "value": { - "integer_value": 2 - } - }, - { - "key": "max", "value": { - "integer_value": 16 - } - }, - { - "key": "count", "value": { - "integer_value": 2 - } - }, - { - "key": "mean", "value": { - "integer_value": 9 - } - }, - { - "key": "sum", "value": { - "integer_value": 18 - } - }, - ] - } + "min": 2, + "max": 16, + "count": 2, + "mean": 9, + "sum": 18, }, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "namespace", - "value": "__main__.WordExtractingDoFn" - }, { - "key": "step", "value": "s2" - }] + "namespace": "__main__.WordExtractingDoFn", "step": "s2" }, "name": "word_length_dist", "origin": "user" }, "scalar": None, "distribution": { - "object_value": { - "properties": [ - { - "key": "min", "value": { - "integer_value": 2 - } - }, - { - "key": "max", "value": { - "integer_value": 16 - } - }, - { - "key": "count", "value": { - "integer_value": 2 - } - }, - { - "key": "mean", "value": { - "integer_value": 9 - } - }, - { - "key": "sum", "value": { - "integer_value": 18 - } - }, - ] - } + "min": 2, + "max": 16, + "count": 2, + "mean": 9, + "sum": 18, }, "updateTime": "2017-03-22T18:47:06.402Z" }, @@ -306,138 +184,75 @@ class TestDataflowMetrics(unittest.TestCase): { "name": { "context": { - "additionalProperties": [ - { - "key": "original_name", - "value": "ToIsmRecordForMultimap-out0-ElementCount" - }, # yapf: disable - { - "key": "output_user_name", - "value": "ToIsmRecordForMultimap-out0" - } - ] + "original_name": "ToIsmRecordForMultimap-out0-ElementCount", + "output_user_name": "ToIsmRecordForMultimap-out0" }, "name": "ElementCount", "origin": "dataflow/v1b3" }, - "scalar": { - "integer_value": 42 - }, - "distribution": None, + "scalar": 42.0, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [ - { - "key": "original_name", - "value": "ToIsmRecordForMultimap-out0-ElementCount" - }, # yapf: disable - { - "key": "output_user_name", - "value": "ToIsmRecordForMultimap-out0" - }, - { - "key": "tentative", "value": "true" - } - ] + "original_name": "ToIsmRecordForMultimap-out0-ElementCount", + "output_user_name": "ToIsmRecordForMultimap-out0", + "tentative": "true" }, "name": "ElementCount", "origin": "dataflow/v1b3" }, - "scalar": { - "integer_value": 42 - }, - "distribution": None, + "scalar": 42.0, "updateTime": "2017-03-22T18:47:06.402Z" }, # MeanByteCount { "name": { "context": { - "additionalProperties": [ - { - "key": "original_name", - "value": "Read-out0-MeanByteCount" - }, - { - "key": "output_user_name", - "value": "GroupByKey/Read-out0" - } - ] + "original_name": "Read-out0-MeanByteCount", + "output_user_name": "GroupByKey/Read-out0" }, "name": "MeanByteCount", "origin": "dataflow/v1b3" }, - "scalar": { - "integer_value": 31 - }, - "distribution": None, + "scalar": 31.0, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [ - { - "key": "original_name", - "value": "Read-out0-MeanByteCount" - }, - { - "key": "output_user_name", - "value": "GroupByKey/Read-out0" - }, { - "key": "tentative", "value": "true" - } - ] + "original_name": "Read-out0-MeanByteCount", + "output_user_name": "GroupByKey/Read-out0", + "tentative": "true" }, "name": "MeanByteCount", "origin": "dataflow/v1b3" }, - "scalar": { - "integer_value": 31 - }, - "distribution": None, + "scalar": 31.0, "updateTime": "2017-03-22T18:47:06.402Z" }, # ExecutionTime { "name": { "context": { - "additionalProperties": [ - { - "key": "step", "value": "write/Write/Write" - }, - ] + "step": "write/Write/Write" }, "name": "ExecutionTime_ProcessElement", "origin": "dataflow/v1b3" }, - "scalar": { - "integer_value": 1000 - }, - "distribution": None, + "scalar": 1000.0, "updateTime": "2017-03-22T18:47:06.402Z" }, { "name": { "context": { - "additionalProperties": [{ - "key": "step", "value": "write/Write/Write" - }, - { - "key": "tentative", - "value": "true" - }] + "step": "write/Write/Write", "tentative": "true" }, "name": "ExecutionTime_ProcessElement", "origin": "dataflow/v1b3" }, - "scalar": { - "integer_value": 1000 - }, - "distribution": None, + "scalar": 1000.0, "updateTime": "2017-03-22T18:47:06.402Z" }, ] @@ -445,13 +260,15 @@ class TestDataflowMetrics(unittest.TestCase): def setup_mock_client_result(self, counter_list=None): mock_client = mock.Mock() - mock_query_result = DictToObject(counter_list) + mock_query_result = dataflow.JobMetrics() + json_format.ParseDict(counter_list, mock_query_result._pb) mock_client.get_job_metrics.return_value = mock_query_result mock_job_result = mock.Mock() mock_job_result.job_id.return_value = 1 mock_job_result.is_in_terminal_state.return_value = False return mock_client, mock_job_result + @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') def test_cache_functions(self): mock_client, mock_job_result = self.setup_mock_client_result( self.STRUCTURED_COUNTER_LIST) @@ -469,6 +286,7 @@ def test_cache_functions(self): dm.query() self.assertTrue(dm._cached_metrics) + @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') def test_query_structured_metrics(self): mock_client, mock_job_result = self.setup_mock_client_result( self.STRUCTURED_COUNTER_LIST) @@ -521,6 +339,7 @@ def test_translate_portable_job_step_name(self): 'MyTestParDo', dm._translate_step_name('ref_AppliedPTransform_MyTestParDo_14')) + @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') def test_query_counters(self): mock_client, mock_job_result = self.setup_mock_client_result( self.ONLY_COUNTERS_LIST) @@ -544,12 +363,15 @@ def test_query_counters(self): sorted(query_result['counters'], key=lambda x: x.key.metric.name), sorted(expected_counters, key=lambda x: x.key.metric.name)) + @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') def test_system_counters_set_labels_and_step_name(self): mock_client, mock_job_result = self.setup_mock_client_result( self.SYSTEM_COUNTERS_LIST) test_object = dataflow_metrics.DataflowMetrics(mock_client, mock_job_result) all_metrics = test_object.all_metrics() + print(all_metrics) + matchers = [ MetricResultMatcher( name='ElementCount', @@ -557,21 +379,21 @@ def test_system_counters_set_labels_and_step_name(self): 'original_name': 'ToIsmRecordForMultimap-out0-ElementCount', 'output_user_name': 'ToIsmRecordForMultimap-out0' }, - attempted=42, - committed=42), + attempted=42.0, + committed=42.0), MetricResultMatcher( name='MeanByteCount', labels={ 'original_name': 'Read-out0-MeanByteCount', 'output_user_name': 'GroupByKey/Read-out0' }, - attempted=31, - committed=31), + attempted=31.0, + committed=31.0), MetricResultMatcher( name='ExecutionTime_ProcessElement', step='write/Write/Write', - attempted=1000, - committed=1000) + attempted=1000.0, + committed=1000.0) ] errors = metric_result_matchers.verify_all(all_metrics, matchers) self.assertFalse(errors, errors) diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py b/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py index c6e2a7bec3d8..389e51b5728a 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py @@ -43,7 +43,6 @@ from apache_beam.options.pipeline_options import WorkerOptions from apache_beam.portability import common_urns from apache_beam.portability.api import beam_runner_api_pb2 -from apache_beam.runners.dataflow.internal.clients import dataflow as dataflow_api from apache_beam.runners.pipeline_utils import group_by_key_input_visitor from apache_beam.runners.pipeline_utils import merge_common_environments from apache_beam.runners.pipeline_utils import merge_superset_dep_environments @@ -56,6 +55,13 @@ from apache_beam.utils.interactive_utils import is_in_notebook from apache_beam.utils.plugin import BeamPlugin +# pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports +try: + from google.cloud import dataflow as dataflow_api +except ImportError: + dataflow_api = None # type: ignore +# pylint: enable=wrong-import-order, wrong-import-position + if TYPE_CHECKING: from apache_beam.pipeline import PTransformOverride @@ -149,29 +155,30 @@ def rank_error(msg): while True: response = runner.dataflow_client.get_job(job_id) # If get() is called very soon after Create() the response may not contain - # an initialized 'currentState' field. - if response.currentState is not None: - if response.currentState != last_job_state: + # an initialized 'current_state' field. + if response.current_state is not None: + current_state = response.current_state.name + if current_state != last_job_state: if state_update_callback: - state_update_callback(response.currentState) - _LOGGER.info('Job %s is in state %s', job_id, response.currentState) - last_job_state = response.currentState - if str(response.currentState) not in ('JOB_STATE_RUNNING', - 'JOB_STATE_PAUSED', - 'JOB_STATE_PAUSING'): + state_update_callback(current_state) + _LOGGER.info('Job %s is in state %s', job_id, current_state) + last_job_state = current_state + if str(current_state) not in ('JOB_STATE_RUNNING', + 'JOB_STATE_PAUSED', + 'JOB_STATE_PAUSING'): # Stop checking for new messages on timeout, explanatory # message received, success, or a terminal job state caused # by the user that therefore doesn't require explanation. if (final_countdown_timer_secs <= 0.0 or last_error_msg is not None or - str(response.currentState) == 'JOB_STATE_DONE' or - str(response.currentState) == 'JOB_STATE_CANCELLED' or - str(response.currentState) == 'JOB_STATE_UPDATED' or - str(response.currentState) == 'JOB_STATE_DRAINED'): + str(current_state) == 'JOB_STATE_DONE' or + str(current_state) == 'JOB_STATE_CANCELLED' or + str(current_state) == 'JOB_STATE_UPDATED' or + str(current_state) == 'JOB_STATE_DRAINED'): break # Check that job is in a post-preparation state before starting the # final countdown. - if (str(response.currentState) + if (str(current_state) not in ('JOB_STATE_PENDING', 'JOB_STATE_QUEUED')): # The job has failed; ensure we see any final error messages. sleep_secs = 1.0 # poll faster during the final countdown @@ -185,7 +192,11 @@ def rank_error(msg): messages, page_token = runner.dataflow_client.list_messages( job_id, page_token=page_token, start_time=last_message_time) for m in messages: - message = '%s: %s: %s' % (m.time, m.messageImportance, m.messageText) + message_importance = ( + dataflow_api.JobMessageImportance(m.message_importance).name + if m.message_importance is not None else + 'JOB_MESSAGE_IMPORTANCE_UNKNOWN') + message = '%s: %s: %s' % (m.time, message_importance, m.message_text) if not last_message_time or m.time > last_message_time: last_message_time = m.time @@ -199,9 +210,8 @@ def rank_error(msg): else: current_seen_messages.add(message) # Skip empty messages. - if m.messageImportance is None: + if m.message_importance is None: continue - message_importance = str(m.messageImportance) if (message_importance == 'JOB_MESSAGE_DEBUG' or message_importance == 'JOB_MESSAGE_DETAILED'): _LOGGER.debug(message) @@ -211,9 +221,9 @@ def rank_error(msg): _LOGGER.warning(message) elif message_importance == 'JOB_MESSAGE_ERROR': _LOGGER.error(message) - if rank_error(m.messageText) >= last_error_rank: - last_error_rank = rank_error(m.messageText) - last_error_msg = m.messageText + if rank_error(m.message_text) >= last_error_rank: + last_error_rank = rank_error(m.message_text) + last_error_msg = m.message_text else: _LOGGER.info(message) if not page_token: @@ -737,7 +747,7 @@ def has_job(self): @staticmethod def api_jobstate_to_pipeline_state(api_jobstate): - values_enum = dataflow_api.Job.CurrentStateValueValuesEnum + values_enum = dataflow_api.JobState # Ordered by the enum values. Values that may be introduced in # future versions of Dataflow API are considered UNRECOGNIZED by this SDK. @@ -766,7 +776,7 @@ def api_jobstate_to_pipeline_state(api_jobstate): if api_jobstate else PipelineState.UNKNOWN) def _get_job_state(self): - return self.api_jobstate_to_pipeline_state(self._job.currentState) + return self.api_jobstate_to_pipeline_state(self._job.current_state) @property def state(self): diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py b/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py index d848b5ffe595..c10902cf1fc6 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_runner_test.py @@ -42,7 +42,6 @@ from apache_beam.runners.dataflow.dataflow_runner import _check_and_add_missing_options from apache_beam.runners.dataflow.dataflow_runner import _check_and_add_missing_streaming_options from apache_beam.runners.dataflow.dataflow_runner import _is_runner_v2_disabled -from apache_beam.runners.dataflow.internal.clients import dataflow as dataflow_api from apache_beam.runners.internal import names from apache_beam.runners.runner import PipelineState from apache_beam.testing.extra_assertions import ExtraAssertionsMixin @@ -52,12 +51,15 @@ from apache_beam.typehints import typehints # Protect against environments where apitools library is not available. -# pylint: disable=wrong-import-order, wrong-import-position +# pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports try: + from google.cloud import dataflow as dataflow_api + from apache_beam.runners.dataflow.internal import apiclient except ImportError: apiclient = None # type: ignore -# pylint: enable=wrong-import-order, wrong-import-position + dataflow_api = None # type: ignore +# pylint: enable=wrong-import-order, wrong-import-position, ungrouped-imports # SpecialParDo and SpecialDoFn are used in test_remote_runner_display_data. @@ -102,7 +104,7 @@ def setUp(self): @mock.patch('time.sleep', return_value=None) def test_wait_until_finish_unrecognized(self, patched_time_sleep): - values_enum = dataflow_api.Job.CurrentStateValueValuesEnum + values_enum = dataflow_api.JobState options = PipelineOptions(self.default_properties) class MockDataflowRunner(object): @@ -110,12 +112,12 @@ def __init__(self, states): self.dataflow_client = mock.MagicMock() self.job = mock.MagicMock() self.job.id = "test-job-id" - self.job.currentState = values_enum.JOB_STATE_UNKNOWN + self.job.current_state = values_enum.JOB_STATE_UNKNOWN self._states = states self._next_state_index = 0 def get_job_side_effect(*args, **kwargs): - self.job.currentState = self._states[self._next_state_index] + self.job.current_state = self._states[self._next_state_index] if self._next_state_index < (len(self._states) - 1): self._next_state_index += 1 return mock.DEFAULT @@ -138,19 +140,19 @@ def get_job_side_effect(*args, **kwargs): @mock.patch('time.sleep', return_value=None) def test_wait_until_finish(self, patched_time_sleep): - values_enum = dataflow_api.Job.CurrentStateValueValuesEnum + values_enum = dataflow_api.JobState options = PipelineOptions(self.default_properties) class MockDataflowRunner(object): def __init__(self, states): self.dataflow_client = mock.MagicMock() self.job = mock.MagicMock() - self.job.currentState = values_enum.JOB_STATE_UNKNOWN + self.job.current_state = values_enum.JOB_STATE_UNKNOWN self._states = states self._next_state_index = 0 def get_job_side_effect(*args, **kwargs): - self.job.currentState = self._states[self._next_state_index] + self.job.current_state = self._states[self._next_state_index] if self._next_state_index < (len(self._states) - 1): self._next_state_index += 1 return mock.DEFAULT @@ -223,7 +225,7 @@ def get_job_side_effect(*args, **kwargs): @mock.patch('time.sleep', return_value=None) def test_cancel(self, patched_time_sleep): - values_enum = dataflow_api.Job.CurrentStateValueValuesEnum + values_enum = dataflow_api.JobState options = PipelineOptions( self.default_properties).view_as(GoogleCloudOptions) @@ -231,7 +233,7 @@ class MockDataflowRunner(object): def __init__(self, state, cancel_result): self.dataflow_client = mock.MagicMock() self.job = mock.MagicMock() - self.job.currentState = state + self.job.current_state = state self.dataflow_client.get_job = mock.MagicMock(return_value=self.job) self.dataflow_client.modify_job_state = mock.MagicMock( @@ -257,7 +259,7 @@ def __init__(self, state, cancel_result): terminal_result.cancel() def test_api_jobstate_to_pipeline_state(self): - values_enum = dataflow_api.Job.CurrentStateValueValuesEnum + values_enum = dataflow_api.JobState expected_mappings = [ (values_enum.JOB_STATE_UNKNOWN, PipelineState.UNKNOWN), (values_enum.JOB_STATE_STOPPED, PipelineState.STOPPED), @@ -270,11 +272,11 @@ def test_api_jobstate_to_pipeline_state(self): (values_enum.JOB_STATE_DRAINED, PipelineState.DRAINED), (values_enum.JOB_STATE_PENDING, PipelineState.PENDING), (values_enum.JOB_STATE_CANCELLING, PipelineState.CANCELLING), + (values_enum.JOB_STATE_PAUSING, PipelineState.PAUSING), + (values_enum.JOB_STATE_PAUSED, PipelineState.PAUSED), ( values_enum.JOB_STATE_RESOURCE_CLEANING_UP, PipelineState.RESOURCE_CLEANING_UP), - (values_enum.JOB_STATE_PAUSING, PipelineState.PAUSING), - (values_enum.JOB_STATE_PAUSED, PipelineState.PAUSED), ] for api_state, pipeline_state in expected_mappings: @@ -692,8 +694,7 @@ def test_auto_streaming_with_unbounded(self): with beam.Pipeline(options=options) as p: _ = p | beam.io.ReadFromPubSub('projects/some-project/topics/some-topic') self.assertEqual( - p.result.job.proto.type, - apiclient.dataflow.Job.TypeValueValuesEnum.JOB_TYPE_STREAMING) + p.result.job.proto.type, apiclient.dataflow.JobType.JOB_TYPE_STREAMING) @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') @mock.patch( @@ -711,8 +712,7 @@ def test_auto_streaming_no_unbounded(self): with beam.Pipeline(options=options) as p: _ = p | beam.Create([1, 2, 3]) self.assertEqual( - p.result.job.proto.type, - apiclient.dataflow.Job.TypeValueValuesEnum.JOB_TYPE_BATCH) + p.result.job.proto.type, apiclient.dataflow.JobType.JOB_TYPE_BATCH) @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') @mock.patch( @@ -731,8 +731,7 @@ def test_explicit_streaming_no_unbounded(self): with beam.Pipeline(options=options) as p: _ = p | beam.Create([1, 2, 3]) self.assertEqual( - p.result.job.proto.type, - apiclient.dataflow.Job.TypeValueValuesEnum.JOB_TYPE_STREAMING) + p.result.job.proto.type, apiclient.dataflow.JobType.JOB_TYPE_STREAMING) def test_runner_v2_disabled_experiments_raise(self): disable_experiments = [ diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 2cf0e79731ac..531c076ffdf0 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -48,16 +48,20 @@ from datetime import datetime from datetime import timezone -from apitools.base.py import encoding -from apitools.base.py import exceptions +import google.auth.credentials +from google.api_core import exceptions +from google.api_core import client_options as client_options_lib +from google.cloud import dataflow +from google.protobuf import json_format +from google.protobuf.struct_pb2 import Struct +from google.protobuf.struct_pb2 import Value from apache_beam import version as beam_version from apache_beam.internal.gcp.auth import get_service_credentials -from apache_beam.internal.gcp.json_value import to_json_value -from apache_beam.internal.http_client import get_new_http from apache_beam.io.filesystems import FileSystems from apache_beam.io.gcp.gcsfilesystem import GCSFileSystem from apache_beam.io.gcp.gcsio import create_storage_client +from apache_beam.options import value_provider from apache_beam.options.pipeline_options import DebugOptions from apache_beam.options.pipeline_options import GoogleCloudOptions from apache_beam.options.pipeline_options import StandardOptions @@ -65,7 +69,6 @@ from apache_beam.portability import common_urns from apache_beam.portability.api import beam_runner_api_pb2 from apache_beam.runners.dataflow.internal import names -from apache_beam.runners.dataflow.internal.clients import dataflow from apache_beam.runners.internal import names as shared_names from apache_beam.runners.pipeline_utils import validate_pipeline_graph from apache_beam.runners.portability.stager import Stager @@ -105,46 +108,45 @@ def __init__( self.debug_options = options.view_as(DebugOptions) self.pipeline_url = proto_pipeline_staged_url self.proto = dataflow.Environment() - self.proto.clusterManagerApiService = GoogleCloudOptions.COMPUTE_API_SERVICE + self.proto.cluster_manager_api_service = GoogleCloudOptions.COMPUTE_API_SERVICE # pylint: disable=line-too-long self.proto.dataset = '{}/cloud_dataflow'.format( GoogleCloudOptions.BIGQUERY_API_SERVICE) - self.proto.tempStoragePrefix = ( + self.proto.temp_storage_prefix = ( self.google_cloud_options.temp_location.replace( 'gs:/', GoogleCloudOptions.STORAGE_API_SERVICE)) if self.worker_options.worker_region: - self.proto.workerRegion = self.worker_options.worker_region + self.proto.worker_region = self.worker_options.worker_region if self.worker_options.worker_zone: - self.proto.workerZone = self.worker_options.worker_zone + self.proto.worker_zone = self.worker_options.worker_zone # User agent information. - self.proto.userAgent = dataflow.Environment.UserAgentValue() + user_agent = Struct( + fields={ + 'name': Value(string_value=self._get_python_sdk_name()), + 'version': Value(string_value=beam_version.__version__) + }) self.local = 'localhost' in self.google_cloud_options.dataflow_endpoint self._proto_pipeline = proto_pipeline if self.google_cloud_options.service_account_email: - self.proto.serviceAccountEmail = ( + self.proto.service_account_email = ( self.google_cloud_options.service_account_email) if self.google_cloud_options.dataflow_kms_key: - self.proto.serviceKmsKeyName = self.google_cloud_options.dataflow_kms_key - - self.proto.userAgent.additionalProperties.extend([ - dataflow.Environment.UserAgentValue.AdditionalProperty( - key='name', value=to_json_value(self._get_python_sdk_name())), - dataflow.Environment.UserAgentValue.AdditionalProperty( - key='version', value=to_json_value(beam_version.__version__)) - ]) + self.proto.service_kms_key_name = self.google_cloud_options.dataflow_kms_key # pylint: disable=line-too-long + + self.proto.user_agent = user_agent + # Version information. - self.proto.version = dataflow.Environment.VersionValue() _verify_interpreter_version_is_supported(options) if self.standard_options.streaming: job_type = 'FNAPI_STREAMING' else: job_type = 'FNAPI_BATCH' - self.proto.version.additionalProperties.extend([ - dataflow.Environment.VersionValue.AdditionalProperty( - key='job_type', value=to_json_value(job_type)), - dataflow.Environment.VersionValue.AdditionalProperty( - key='major', value=to_json_value(environment_version)) - ]) + version = Struct( + fields={ + 'job_type': Value(string_value=job_type), + 'major': Value(string_value=environment_version) + }) + self.proto.version = version # TODO: Use enumerated type instead of strings for job types. if job_type.startswith('FNAPI_'): self.debug_options.experiments = self.debug_options.experiments or [] @@ -159,13 +161,11 @@ def __init__( debug_options_experiments.append('use_multiple_sdk_containers') # FlexRS if self.google_cloud_options.flexrs_goal == 'COST_OPTIMIZED': - self.proto.flexResourceSchedulingGoal = ( - dataflow.Environment.FlexResourceSchedulingGoalValueValuesEnum. - FLEXRS_COST_OPTIMIZED) + self.proto.flex_resource_scheduling_goal = ( + dataflow.FlexResourceSchedulingGoal.FLEXRS_COST_OPTIMIZED) elif self.google_cloud_options.flexrs_goal == 'SPEED_OPTIMIZED': - self.proto.flexResourceSchedulingGoal = ( - dataflow.Environment.FlexResourceSchedulingGoalValueValuesEnum. - FLEXRS_SPEED_OPTIMIZED) + self.proto.flex_resource_scheduling_goal = ( + dataflow.FlexResourceSchedulingGoal.FLEXRS_SPEED_OPTIMIZED) # Experiments if self.debug_options.experiments: for experiment in self.debug_options.experiments: @@ -184,34 +184,34 @@ def __init__( pool = dataflow.WorkerPool( kind='local' if self.local else 'harness', packages=package_descriptors, - taskrunnerSettings=dataflow.TaskRunnerSettings( - parallelWorkerSettings=dataflow.WorkerSettings( - baseUrl=GoogleCloudOptions.DATAFLOW_ENDPOINT, - servicePath=self.google_cloud_options.dataflow_endpoint))) + taskrunner_settings=dataflow.TaskRunnerSettings( + parallel_worker_settings=dataflow.WorkerSettings( + base_url=GoogleCloudOptions.DATAFLOW_ENDPOINT, + service_path=self.google_cloud_options.dataflow_endpoint))) - pool.autoscalingSettings = dataflow.AutoscalingSettings() + pool.autoscaling_settings = dataflow.AutoscalingSettings() # Set worker pool options received through command line. if self.worker_options.num_workers: - pool.numWorkers = self.worker_options.num_workers + pool.num_workers = self.worker_options.num_workers if self.worker_options.max_num_workers: - pool.autoscalingSettings.maxNumWorkers = ( + pool.autoscaling_settings.max_num_workers = ( self.worker_options.max_num_workers) if self.worker_options.autoscaling_algorithm: - values_enum = dataflow.AutoscalingSettings.AlgorithmValueValuesEnum - pool.autoscalingSettings.algorithm = { + values_enum = dataflow.AutoscalingAlgorithm + pool.autoscaling_settings.algorithm = { 'NONE': values_enum.AUTOSCALING_ALGORITHM_NONE, 'THROUGHPUT_BASED': values_enum.AUTOSCALING_ALGORITHM_BASIC, }.get(self.worker_options.autoscaling_algorithm) if self.worker_options.machine_type: - pool.machineType = self.worker_options.machine_type + pool.machine_type = self.worker_options.machine_type if self.worker_options.disk_size_gb: - pool.diskSizeGb = self.worker_options.disk_size_gb + pool.disk_size_gb = self.worker_options.disk_size_gb if self.worker_options.disk_type: - pool.diskType = self.worker_options.disk_type + pool.disk_type = self.worker_options.disk_type if self.worker_options.disk_provisioned_iops is not None: - pool.diskProvisionedIops = self.worker_options.disk_provisioned_iops + pool.disk_provisioned_iops = self.worker_options.disk_provisioned_iops if self.worker_options.disk_provisioned_throughput_mibps is not None: - pool.diskProvisionedThroughputMibps = ( + pool.disk_provisioned_throughput_mibps = ( self.worker_options.disk_provisioned_throughput_mibps) if self.worker_options.zone: pool.zone = self.worker_options.zone @@ -236,54 +236,52 @@ def __init__( container_image_url = environment_payload.container_image container_image = dataflow.SdkHarnessContainerImage() - container_image.containerImage = container_image_url - container_image.useSingleCorePerContainer = ( + container_image.container_image = container_image_url + container_image.use_single_core_per_container = ( common_urns.protocols.MULTI_CORE_BUNDLE_PROCESSING.urn not in environment.capabilities) - container_image.environmentId = id + container_image.environment_id = id for capability in environment.capabilities: container_image.capabilities.append(capability) - pool.sdkHarnessContainerImages.append(container_image) + pool.sdk_harness_container_images.append(container_image) - if not pool.sdkHarnessContainerImages: - pool.workerHarnessContainerImage = ( + if not pool.sdk_harness_container_images: + pool.worker_harness_container_image = ( get_container_image_from_options(options)) - elif len(pool.sdkHarnessContainerImages) == 1: + elif len(pool.sdk_harness_container_images) == 1: # Dataflow expects a value here when there is only one environment. - pool.workerHarnessContainerImage = ( - pool.sdkHarnessContainerImages[0].containerImage) + pool.worker_harness_container_image = ( + pool.sdk_harness_container_images[0].container_image) if self.debug_options.number_of_worker_harness_threads: - pool.numThreadsPerWorker = ( + pool.num_threads_per_worker = ( self.debug_options.number_of_worker_harness_threads) if self.worker_options.use_public_ips is not None: if self.worker_options.use_public_ips: - pool.ipConfiguration = ( - dataflow.WorkerPool.IpConfigurationValueValuesEnum.WORKER_IP_PUBLIC) + pool.ip_configuration = ( + dataflow.WorkerIPAddressConfiguration.WORKER_IP_PUBLIC) else: - pool.ipConfiguration = ( - dataflow.WorkerPool.IpConfigurationValueValuesEnum.WORKER_IP_PRIVATE - ) + pool.ip_configuration = ( + dataflow.WorkerIPAddressConfiguration.WORKER_IP_PRIVATE) if self.standard_options.streaming: # Use separate data disk for streaming. disk = dataflow.Disk() if self.local: - disk.diskType = 'local' + disk.disk_type = 'local' if self.worker_options.disk_type: - disk.diskType = self.worker_options.disk_type - pool.dataDisks.append(disk) - self.proto.workerPools.append(pool) + disk.disk_type = self.worker_options.disk_type + pool.data_disks.append(disk) + self.proto.worker_pools.append(pool) sdk_pipeline_options = options.get_all_options(retain_unknown_options=True) if sdk_pipeline_options: - self.proto.sdkPipelineOptions = ( - dataflow.Environment.SdkPipelineOptionsValue()) - - options_dict = { - k: v - for k, v in sdk_pipeline_options.items() if v is not None - } + options_dict = {} + for k, v in sdk_pipeline_options.items(): + if v is None: + continue + options_dict[k] = str(v) if isinstance( + v, value_provider.ValueProvider) else v options_dict["pipelineUrl"] = proto_pipeline_staged_url if pipeline_proto_hash: options_dict["pipelineProtoHash"] = pipeline_proto_hash @@ -291,22 +289,25 @@ def __init__( # Though impersonation should start a job, the workers should # not try to modify their credentials. options_dict.pop('impersonate_service_account', None) - self.proto.sdkPipelineOptions.additionalProperties.append( - dataflow.Environment.SdkPipelineOptionsValue.AdditionalProperty( - key='options', value=to_json_value(options_dict))) dd = DisplayData.create_from_options(options) items = [item.get_dict() for item in dd.items] - self.proto.sdkPipelineOptions.additionalProperties.append( - dataflow.Environment.SdkPipelineOptionsValue.AdditionalProperty( - key='display_data', value=to_json_value(items))) + + _LOGGER.info('sdk_pipeline_options: %s', options_dict) + + sdk_pipeline_options_struct = Struct() + sdk_pipeline_options_struct.update({ + 'options': options_dict, 'display_data': items + }) + self.proto.sdk_pipeline_options = sdk_pipeline_options_struct if self.google_cloud_options.dataflow_service_options: for option in self.google_cloud_options.dataflow_service_options: - self.proto.serviceOptions.append(option) + self.proto.service_options.append(option) if self.google_cloud_options.enable_hot_key_logging: - self.proto.debugOptions = dataflow.DebugOptions(enableHotKeyLogging=True) + self.proto.debug_options = dataflow.DebugOptions( + enable_hot_key_logging=True) def _get_environments_from_tranforms(self): if not self._proto_pipeline: @@ -362,7 +363,7 @@ def shortstrings_registerer(encoding_name): # further modify it to not output too-long strings, aimed at the # 10,000+ character hex-encoded "serialized_fn" values. return json.dumps( - json.loads(encoding.MessageToJson(self.proto)), + json.loads(json_format.MessageToJson(self.proto._pb)), indent=2, sort_keys=True) @@ -438,25 +439,22 @@ def __init__(self, options, proto_pipeline): self.proto = dataflow.Job(name=self.google_cloud_options.job_name) if self.options.view_as(StandardOptions).streaming: - self.proto.type = dataflow.Job.TypeValueValuesEnum.JOB_TYPE_STREAMING + self.proto.type_ = dataflow.JobType.JOB_TYPE_STREAMING else: - self.proto.type = dataflow.Job.TypeValueValuesEnum.JOB_TYPE_BATCH + self.proto.type_ = dataflow.JobType.JOB_TYPE_BATCH if self.google_cloud_options.update: - self.proto.replaceJobId = self.job_id_for_name(self.proto.name) + self.proto.replace_job_id = self.job_id_for_name(self.proto.name) if self.google_cloud_options.transform_name_mapping: - self.proto.transformNameMapping = ( - dataflow.Job.TransformNameMappingValue()) + self.proto.transform_name_mapping = Struct() for _, (key, value) in enumerate( self.google_cloud_options.transform_name_mapping.items()): - self.proto.transformNameMapping.additionalProperties.append( - dataflow.Job.TransformNameMappingValue.AdditionalProperty( - key=key, value=value)) + self.proto.transform_name_mapping.update({key: value}) if self.google_cloud_options.create_from_snapshot: - self.proto.createdFromSnapshotId = ( + self.proto.created_from_snapshot_id = ( self.google_cloud_options.create_from_snapshot) # Labels. if self.google_cloud_options.labels: - self.proto.labels = dataflow.Job.LabelsValue() + self.proto.labels = Struct() labels = self.google_cloud_options.labels if isinstance(labels, str): labels = [labels] @@ -466,18 +464,15 @@ def __init__(self, options, proto_pipeline): if '{' in label: label = ast.literal_eval(label) for key, value in label.items(): - self.proto.labels.additionalProperties.append( - dataflow.Job.LabelsValue.AdditionalProperty( - key=key, value=value)) + self.proto.labels.update({key: value}) else: parts = label.split('=', 1) key = parts[0] value = parts[1] if len(parts) > 1 else '' - self.proto.labels.additionalProperties.append( - dataflow.Job.LabelsValue.AdditionalProperty(key=key, value=value)) + self.proto.labels.update({key: value}) # Client Request ID - self.proto.clientRequestId = '{}-{}'.format( + self.proto.client_request_id = '{}-{}'.format( datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S%f'), random.randrange(9000) + 1000) @@ -489,7 +484,7 @@ def job_id_for_name(self, job_name): self.google_cloud_options).job_id_for_name(job_name) def json(self): - return encoding.MessageToJson(self.proto) + return json_format.MessageToJson(self.proto._pb) def __reduce__(self): """Reduce hook for pickling the Job class more easily.""" @@ -518,13 +513,31 @@ def __init__(self, options, root_staging_location=None): else: credentials = get_service_credentials(options) - http_client = get_new_http() - self._client = dataflow.DataflowV1b3( - url=self.google_cloud_options.dataflow_endpoint, - credentials=credentials, - get_credentials=(not self.google_cloud_options.no_auth), - http=http_client, - response_encoding=get_response_encoding()) + gapic_credentials = ( + credentials or google.auth.credentials.AnonymousCredentials()) + + client_options = None + transport = None + if self.google_cloud_options.dataflow_endpoint: + endpoint = self.google_cloud_options.dataflow_endpoint + if 'localhost' in endpoint: + transport = 'rest' + else: + endpoint = re.sub('^https?://', '', endpoint) + client_options = client_options_lib.ClientOptions(api_endpoint=endpoint) + + self._jobs_client = dataflow.JobsV1Beta3Client( + credentials=gapic_credentials, + client_options=client_options, + transport=transport) + self._messages_client = dataflow.MessagesV1Beta3Client( + credentials=gapic_credentials, + client_options=client_options, + transport=transport) + self._metrics_client = dataflow.MetricsV1Beta3Client( + credentials=gapic_credentials, + client_options=client_options, + transport=transport) self._storage_client = create_storage_client( options, not self.google_cloud_options.no_auth) self._sdk_image_overrides = self._get_sdk_image_overrides(options) @@ -743,7 +756,7 @@ def stage_file_with_retry( "stream_or_path is unsupported.") @retry.no_retries # Using no_retries marks this as an integration point. - def create_job(self, job): + def create_job(self, job: Job): """Creates job description. May stage and/or submit for remote execution.""" self.create_job_description(job) @@ -758,7 +771,7 @@ def create_job(self, job): "dataflow_graph.json", io.BytesIO(job.json().encode('utf-8'))) del job.proto.steps[:] - job.proto.stepsLocation = FileSystems.join( + job.proto.steps_location = FileSystems.join( job.options.view_as(GoogleCloudOptions).staging_location, "dataflow_graph.json") @@ -831,7 +844,7 @@ def _apply_sdk_environment_overrides( new_payload.container_image = new_container_image environment.payload = new_payload.SerializeToString() - def create_job_description(self, job): + def create_job_description(self, job: Job): """Creates a job described by the workflow proto.""" DataflowApplicationClient._apply_sdk_environment_overrides( job.proto_pipeline, self._sdk_image_overrides, job.options) @@ -860,45 +873,44 @@ def create_job_description(self, job): _LOGGER.debug('JOB: %s', job) @retry.with_exponential_backoff(num_retries=3, initial_delay_secs=3) - def get_job_metrics(self, job_id): - request = dataflow.DataflowProjectsLocationsJobsGetMetricsRequest() - request.jobId = job_id + def get_job_metrics(self, job_id) -> dataflow.JobMetrics: + request = dataflow.GetJobMetricsRequest() + request.job_id = job_id request.location = self.google_cloud_options.region - request.projectId = self.google_cloud_options.project + request.project_id = self.google_cloud_options.project try: - response = self._client.projects_locations_jobs.GetMetrics(request) - except exceptions.BadStatusCodeError as e: - _LOGGER.error( - 'HTTP status %d. Unable to query metrics', e.response.status) + response = self._metrics_client.get_job_metrics(request) + except exceptions.GoogleAPICallError as e: + _LOGGER.error('HTTP status %d. Unable to query metrics', e.code) raise return response @retry.with_exponential_backoff(num_retries=3) - def submit_job_description(self, job): + def submit_job_description(self, job: Job) -> dataflow.Job: """Creates and excutes a job request.""" - request = dataflow.DataflowProjectsLocationsJobsCreateRequest() - request.projectId = self.google_cloud_options.project + request = dataflow.CreateJobRequest() + request.project_id = self.google_cloud_options.project request.location = self.google_cloud_options.region request.job = job.proto try: - response = self._client.projects_locations_jobs.Create(request) - except exceptions.BadStatusCodeError as e: + response = self._jobs_client.create_job(request=request) + except exceptions.GoogleAPICallError as e: _LOGGER.error( 'HTTP status %d trying to create job' ' at dataflow service endpoint %s', - e.response.status, + e.code, self.google_cloud_options.dataflow_endpoint) _LOGGER.fatal('details of server error: %s', e) raise - if response.clientRequestId and \ - response.clientRequestId != job.proto.clientRequestId: + if response.client_request_id and \ + response.client_request_id != job.proto.client_request_id: if self.google_cloud_options.update: raise DataflowJobAlreadyExistsError( "The job named %s with id: %s has already been updated into job " "id: %s and cannot be updated again." % - (response.name, job.proto.replaceJobId, response.id)) + (response.name, job.proto.replace_job_id, response.id)) else: raise DataflowJobAlreadyExistsError( 'There is already active job named %s with id: %s. If you want to ' @@ -919,7 +931,7 @@ def submit_job_description(self, job): return response @retry.with_exponential_backoff() # Using retry defaults from utils/retry.py - def modify_job_state(self, job_id, new_state): + def modify_job_state(self, job_id, new_state) -> bool: """Modify the run state of the job. Args: @@ -931,27 +943,27 @@ def modify_job_state(self, job_id, new_state): True if the job was modified successfully. """ if new_state == 'JOB_STATE_DONE': - new_state = dataflow.Job.RequestedStateValueValuesEnum.JOB_STATE_DONE + new_state = dataflow.JobState.JOB_STATE_DONE elif new_state == 'JOB_STATE_CANCELLED': - new_state = dataflow.Job.RequestedStateValueValuesEnum.JOB_STATE_CANCELLED + new_state = dataflow.JobState.JOB_STATE_CANCELLED elif new_state == 'JOB_STATE_DRAINING': - new_state = dataflow.Job.RequestedStateValueValuesEnum.JOB_STATE_DRAINING + new_state = dataflow.JobState.JOB_STATE_DRAINING else: # Other states could only be set by the service. return False - request = dataflow.DataflowProjectsLocationsJobsUpdateRequest() - request.jobId = job_id - request.projectId = self.google_cloud_options.project + request = dataflow.UpdateJobRequest() + request.job_id = job_id + request.project_id = self.google_cloud_options.project request.location = self.google_cloud_options.region - request.job = dataflow.Job(requestedState=new_state) + request.job = dataflow.Job(requested_state=new_state) - self._client.projects_locations_jobs.Update(request) + self._jobs_client.update_job(request=request) return True @retry.with_exponential_backoff( retry_filter=retry.retry_on_server_errors_and_notfound_filter) - def get_job(self, job_id): + def get_job(self, job_id) -> dataflow.Job: """Gets the job status for a submitted job. Args: @@ -972,11 +984,11 @@ def get_job(self, job_id): (e.g. '2015-03-10T00:01:53.074Z') currentStateTime: UTC time for the current state of the job. """ - request = dataflow.DataflowProjectsLocationsJobsGetRequest() - request.jobId = job_id - request.projectId = self.google_cloud_options.project + request = dataflow.GetJobRequest() + request.job_id = job_id + request.project_id = self.google_cloud_options.project request.location = self.google_cloud_options.region - response = self._client.projects_locations_jobs.Get(request) + response = self._jobs_client.get_job(request) return response @retry.with_exponential_backoff( @@ -1027,58 +1039,53 @@ def list_messages( JOB_MESSAGE_WARNING, JOB_MESSAGE_ERROR. messageText: A message string. """ - request = dataflow.DataflowProjectsLocationsJobsMessagesListRequest( - jobId=job_id, + request = dataflow.ListJobMessagesRequest( + job_id=job_id, location=self.google_cloud_options.region, - projectId=self.google_cloud_options.project) + project_id=self.google_cloud_options.project) if page_token is not None: - request.pageToken = page_token + request.page_token = page_token if start_time is not None: - request.startTime = start_time + request.start_time = start_time if end_time is not None: - request.endTime = end_time + request.end_time = end_time if minimum_importance is not None: if minimum_importance == 'JOB_MESSAGE_DEBUG': - request.minimumImportance = ( - dataflow.DataflowProjectsLocationsJobsMessagesListRequest. - MinimumImportanceValueValuesEnum.JOB_MESSAGE_DEBUG) + request.minimum_importance = ( + dataflow.JobMessageImportance.JOB_MESSAGE_DEBUG) elif minimum_importance == 'JOB_MESSAGE_DETAILED': - request.minimumImportance = ( - dataflow.DataflowProjectsLocationsJobsMessagesListRequest. - MinimumImportanceValueValuesEnum.JOB_MESSAGE_DETAILED) + request.minimum_importance = ( + dataflow.JobMessageImportance.JOB_MESSAGE_DETAILED) elif minimum_importance == 'JOB_MESSAGE_BASIC': - request.minimumImportance = ( - dataflow.DataflowProjectsLocationsJobsMessagesListRequest. - MinimumImportanceValueValuesEnum.JOB_MESSAGE_BASIC) + request.minimum_importance = ( + dataflow.JobMessageImportance.JOB_MESSAGE_BASIC) elif minimum_importance == 'JOB_MESSAGE_WARNING': - request.minimumImportance = ( - dataflow.DataflowProjectsLocationsJobsMessagesListRequest. - MinimumImportanceValueValuesEnum.JOB_MESSAGE_WARNING) + request.minimum_importance = ( + dataflow.JobMessageImportance.JOB_MESSAGE_WARNING) elif minimum_importance == 'JOB_MESSAGE_ERROR': - request.minimumImportance = ( - dataflow.DataflowProjectsLocationsJobsMessagesListRequest. - MinimumImportanceValueValuesEnum.JOB_MESSAGE_ERROR) + request.minimum_importance = ( + dataflow.JobMessageImportance.JOB_MESSAGE_ERROR) else: raise RuntimeError( 'Unexpected value for minimum_importance argument: %r' % minimum_importance) - response = self._client.projects_locations_jobs_messages.List(request) - return response.jobMessages, response.nextPageToken + response = self._messages_client.list_job_messages(request=request) + return response.job_messages, response.next_page_token def job_id_for_name(self, job_name): token = None while True: - request = dataflow.DataflowProjectsLocationsJobsListRequest( - projectId=self.google_cloud_options.project, + request = dataflow.ListJobsRequest( + project_id=self.google_cloud_options.project, location=self.google_cloud_options.region, - pageToken=token) - response = self._client.projects_locations_jobs.List(request) - for job in response.jobs: - if (job.name == job_name and job.currentState - in [dataflow.Job.CurrentStateValueValuesEnum.JOB_STATE_RUNNING, - dataflow.Job.CurrentStateValueValuesEnum.JOB_STATE_DRAINING]): + page_token=token) + response = self._jobs_client.list_jobs(request) + for job in response: + if (job.name == job_name and + job.current_state in [dataflow.JobState.JOB_STATE_RUNNING, + dataflow.JobState.JOB_STATE_DRAINING]): return job.id - token = response.nextPageToken + token = response.next_page_token if token is None: raise ValueError("No running job found with name '%s'" % job_name) @@ -1086,39 +1093,43 @@ def job_id_for_name(self, job_name): class MetricUpdateTranslators(object): """Translators between accumulators and dataflow metric updates.""" @staticmethod - def translate_boolean(accumulator, metric_update_proto): - metric_update_proto.boolean = accumulator.value + def translate_boolean( + accumulator, metric_update_proto: dataflow.MetricUpdate): + metric_update_proto.scalar = accumulator.value @staticmethod - def translate_scalar_mean_int(accumulator, metric_update_proto): + def translate_scalar_mean_int( + accumulator, metric_update_proto: dataflow.MetricUpdate): if accumulator.count: - metric_update_proto.integerMean = dataflow.IntegerMean() - metric_update_proto.integerMean.sum = to_split_int(accumulator.sum) - metric_update_proto.integerMean.count = to_split_int(accumulator.count) + metric_update_proto.kind = 'Mean' + metric_update_proto.mean_sum = accumulator.sum + metric_update_proto.mean_count = accumulator.count else: - metric_update_proto.nameAndKind.kind = None + metric_update_proto.kind = None # type: ignore @staticmethod - def translate_scalar_mean_float(accumulator, metric_update_proto): + def translate_scalar_mean_float( + accumulator, metric_update_proto: dataflow.MetricUpdate): if accumulator.count: - metric_update_proto.floatingPointMean = dataflow.FloatingPointMean() - metric_update_proto.floatingPointMean.sum = accumulator.sum - metric_update_proto.floatingPointMean.count = to_split_int( - accumulator.count) + metric_update_proto.kind = 'Mean' + metric_update_proto.mean_sum = accumulator.sum + metric_update_proto.mean_count = accumulator.count else: - metric_update_proto.nameAndKind.kind = None + metric_update_proto.kind = None # type: ignore @staticmethod - def translate_scalar_counter_int(accumulator, metric_update_proto): - metric_update_proto.integer = to_split_int(accumulator.value) + def translate_scalar_counter_int( + accumulator, metric_update_proto: dataflow.MetricUpdate): + metric_update_proto.scalar = accumulator.value @staticmethod - def translate_scalar_counter_float(accumulator, metric_update_proto): - metric_update_proto.floatingPoint = accumulator.value + def translate_scalar_counter_float( + accumulator, metric_update_proto: dataflow.MetricUpdate): + metric_update_proto.scalar = accumulator.value class _LegacyDataflowStager(Stager): - def __init__(self, dataflow_application_client): + def __init__(self, dataflow_application_client: DataflowApplicationClient): super().__init__() self._dataflow_application_client = dataflow_application_client @@ -1138,6 +1149,12 @@ def get_sdk_package_name(): return shared_names.BEAM_PACKAGE_NAME +class Histogram(object): + def __init__(self): + self.firstBucketOffset = None + self.bucketCounts = None + + class DataflowJobAlreadyExistsError(retry.PermanentException): """A non-retryable exception that a job with the given name already exists.""" # Inherits retry.PermanentException to avoid retry in @@ -1145,16 +1162,10 @@ class DataflowJobAlreadyExistsError(retry.PermanentException): pass -def to_split_int(n): - res = dataflow.SplitInt64() - res.lowBits = n & 0xffffffff - res.highBits = n >> 32 - return res - - # TODO: Used in legacy batch worker. Move under MetricUpdateTranslators # after Dataflow Portable Runner transition. -def translate_distribution(distribution_update, metric_update_proto): +def translate_distribution( + distribution_update, metric_update_proto: dataflow.MetricUpdate): """Translate metrics DistributionUpdate to dataflow distribution update. Args: @@ -1162,21 +1173,23 @@ def translate_distribution(distribution_update, metric_update_proto): DistributionInt64Accumulator or DataflowDistributionCounter. metric_update_proto: Used for report metrics. """ - dist_update_proto = dataflow.DistributionUpdate() - dist_update_proto.min = to_split_int(distribution_update.min) - dist_update_proto.max = to_split_int(distribution_update.max) - dist_update_proto.count = to_split_int(distribution_update.count) - dist_update_proto.sum = to_split_int(distribution_update.sum) + dist_update_proto = Struct() + dist_update_proto.update({"min": distribution_update.min}) + dist_update_proto.update({"max": distribution_update.max}) + dist_update_proto.update({"count": distribution_update.count}) + dist_update_proto.update({"sum": distribution_update.sum}) # DataflowDistributionCounter needs to translate histogram if isinstance(distribution_update, DataflowDistributionCounter): - dist_update_proto.histogram = dataflow.Histogram() - distribution_update.translate_to_histogram(dist_update_proto.histogram) + histogram = Histogram() + distribution_update.translate_to_histogram(histogram) + dist_update_proto.update({"firstBucketOffset": histogram.firstBucketOffset}) + dist_update_proto.update({"bucketCounts": histogram.bucketCounts}) metric_update_proto.distribution = dist_update_proto # TODO: Used in legacy batch worker. Delete after Dataflow Portable Runner transition. -def translate_value(value, metric_update_proto): - metric_update_proto.integer = to_split_int(value) +def translate_value(value, metric_update_proto: dataflow.MetricUpdate): + metric_update_proto.scalar = value def _get_container_image_tag(): @@ -1252,95 +1265,3 @@ def _verify_interpreter_version_is_supported(pipeline_options): 'using an unsupported version of Python interpreter, pass ' '--experiment use_unsupported_python_version pipeline option.' % (_PYTHON_VERSIONS_SUPPORTED_BY_DATAFLOW, sys.version)) - - -# To enable a counter on the service, add it to this dictionary. -# This is required for the legacy python dataflow runner, as portability -# does not communicate to the service via python code, but instead via a -# a runner harness (in C++ or Java). -# TODO(https://github.com/apache/beam/issues/19433) : Remove this antipattern, -# legacy dataflow python pipelines will break whenever a new cy_combiner type -# is used. -structured_counter_translations = { - cy_combiners.CountCombineFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.SUM, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.SumInt64Fn: ( - dataflow.CounterMetadata.KindValueValuesEnum.SUM, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.MinInt64Fn: ( - dataflow.CounterMetadata.KindValueValuesEnum.MIN, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.MaxInt64Fn: ( - dataflow.CounterMetadata.KindValueValuesEnum.MAX, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.MeanInt64Fn: ( - dataflow.CounterMetadata.KindValueValuesEnum.MEAN, - MetricUpdateTranslators.translate_scalar_mean_int), - cy_combiners.SumFloatFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.SUM, - MetricUpdateTranslators.translate_scalar_counter_float), - cy_combiners.MinFloatFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.MIN, - MetricUpdateTranslators.translate_scalar_counter_float), - cy_combiners.MaxFloatFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.MAX, - MetricUpdateTranslators.translate_scalar_counter_float), - cy_combiners.MeanFloatFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.MEAN, - MetricUpdateTranslators.translate_scalar_mean_float), - cy_combiners.AllCombineFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.AND, - MetricUpdateTranslators.translate_boolean), - cy_combiners.AnyCombineFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.OR, - MetricUpdateTranslators.translate_boolean), - cy_combiners.DataflowDistributionCounterFn: ( - dataflow.CounterMetadata.KindValueValuesEnum.DISTRIBUTION, - translate_distribution), - cy_combiners.DistributionInt64Fn: ( - dataflow.CounterMetadata.KindValueValuesEnum.DISTRIBUTION, - translate_distribution), -} - -counter_translations = { - cy_combiners.CountCombineFn: ( - dataflow.NameAndKind.KindValueValuesEnum.SUM, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.SumInt64Fn: ( - dataflow.NameAndKind.KindValueValuesEnum.SUM, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.MinInt64Fn: ( - dataflow.NameAndKind.KindValueValuesEnum.MIN, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.MaxInt64Fn: ( - dataflow.NameAndKind.KindValueValuesEnum.MAX, - MetricUpdateTranslators.translate_scalar_counter_int), - cy_combiners.MeanInt64Fn: ( - dataflow.NameAndKind.KindValueValuesEnum.MEAN, - MetricUpdateTranslators.translate_scalar_mean_int), - cy_combiners.SumFloatFn: ( - dataflow.NameAndKind.KindValueValuesEnum.SUM, - MetricUpdateTranslators.translate_scalar_counter_float), - cy_combiners.MinFloatFn: ( - dataflow.NameAndKind.KindValueValuesEnum.MIN, - MetricUpdateTranslators.translate_scalar_counter_float), - cy_combiners.MaxFloatFn: ( - dataflow.NameAndKind.KindValueValuesEnum.MAX, - MetricUpdateTranslators.translate_scalar_counter_float), - cy_combiners.MeanFloatFn: ( - dataflow.NameAndKind.KindValueValuesEnum.MEAN, - MetricUpdateTranslators.translate_scalar_mean_float), - cy_combiners.AllCombineFn: ( - dataflow.NameAndKind.KindValueValuesEnum.AND, - MetricUpdateTranslators.translate_boolean), - cy_combiners.AnyCombineFn: ( - dataflow.NameAndKind.KindValueValuesEnum.OR, - MetricUpdateTranslators.translate_boolean), - cy_combiners.DataflowDistributionCounterFn: ( - dataflow.NameAndKind.KindValueValuesEnum.DISTRIBUTION, - translate_distribution), - cy_combiners.DistributionInt64Fn: ( - dataflow.CounterMetadata.KindValueValuesEnum.DISTRIBUTION, - translate_distribution), -} diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index 909ce9fd117e..b34f06b64df4 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -39,7 +39,6 @@ from apache_beam.portability import common_urns from apache_beam.portability.api import beam_runner_api_pb2 from apache_beam.runners.dataflow.internal import names -from apache_beam.runners.dataflow.internal.clients import dataflow from apache_beam.transforms import Create from apache_beam.transforms import DataflowDistributionCounter from apache_beam.transforms import DoFn @@ -50,9 +49,13 @@ # Protect against environments where apitools library is not available. # pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports try: + import google.auth + from google.cloud import dataflow + from apache_beam.runners.dataflow.internal import apiclient except ImportError: apiclient = None # type: ignore + dataflow = None # type: ignore # pylint: enable=wrong-import-order, wrong-import-position FAKE_PIPELINE_URL = "gs://invalid-bucket/anywhere" @@ -61,6 +64,18 @@ @unittest.skipIf(apiclient is None, 'GCP dependencies are not installed') class UtilTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.patcher = mock.patch( + 'google.auth.default', return_value=(None, 'test_project')) + cls.patcher.start() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.patcher.stop() + @unittest.skip("Enable once BEAM-1080 is fixed.") def test_create_application_client(self): pipeline_options = PipelineOptions() @@ -80,23 +95,23 @@ def test_pipeline_url(self): FAKE_PIPELINE_URL) recovered_options = None - for additionalProperty in env.proto.sdkPipelineOptions.additionalProperties: - if additionalProperty.key == 'options': - recovered_options = additionalProperty.value + for key in env.proto.sdk_pipeline_options.keys(): + if key == 'options': + recovered_options = env.proto.sdk_pipeline_options[key] break else: self.fail( - 'No pipeline options found in %s' % env.proto.sdkPipelineOptions) + 'No pipeline options found in %s' % env.proto.sdk_pipeline_options) pipeline_url = None - for property in recovered_options.object_value.properties: - if property.key == 'pipelineUrl': - pipeline_url = property.value + for key in recovered_options.keys(): + if key == 'pipelineUrl': + pipeline_url = recovered_options[key] break else: - self.fail('No pipeline_url found in %s' % recovered_options) + self.fail('No pipelineUrl found in %s' % recovered_options) - self.assertEqual(pipeline_url.string_value, FAKE_PIPELINE_URL) + self.assertEqual(pipeline_url, FAKE_PIPELINE_URL) def test_pipeline_proto_hash(self): pipeline_options = PipelineOptions( @@ -115,22 +130,22 @@ def test_pipeline_proto_hash(self): pipeline_proto_hash=expected_hash) recovered_options = None - for additionalProperty in env.proto.sdkPipelineOptions.additionalProperties: - if additionalProperty.key == 'options': - recovered_options = additionalProperty.value + for key in env.proto.sdk_pipeline_options.keys(): + if key == 'options': + recovered_options = env.proto.sdk_pipeline_options[key] break else: self.fail('No pipeline options found') pipeline_proto_hash = None - for property in recovered_options.object_value.properties: - if property.key == 'pipelineProtoHash': - pipeline_proto_hash = property.value + for key in recovered_options.keys(): + if key == 'pipelineProtoHash': + pipeline_proto_hash = recovered_options[key] break else: self.fail('No pipelineProtoHash found') - self.assertEqual(pipeline_proto_hash.string_value, expected_hash) + self.assertEqual(pipeline_proto_hash, expected_hash) def test_set_network(self): pipeline_options = PipelineOptions([ @@ -144,7 +159,7 @@ def test_set_network(self): pipeline_options, '2.0.0', #any environment version FAKE_PIPELINE_URL) - self.assertEqual(env.proto.workerPools[0].network, 'anetworkname') + self.assertEqual(env.proto.worker_pools[0].network, 'anetworkname') def test_set_subnetwork(self): pipeline_options = PipelineOptions([ @@ -160,7 +175,7 @@ def test_set_subnetwork(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].subnetwork, + env.proto.worker_pools[0].subnetwork, '/regions/MY/subnetworks/SUBNETWORK') def test_flexrs_blank(self): @@ -172,7 +187,9 @@ def test_flexrs_blank(self): pipeline_options, '2.0.0', #any environment version FAKE_PIPELINE_URL) - self.assertEqual(env.proto.flexResourceSchedulingGoal, None) + self.assertEqual( + env.proto.flex_resource_scheduling_goal, + dataflow.FlexResourceSchedulingGoal.FLEXRS_UNSPECIFIED) def test_flexrs_cost(self): pipeline_options = PipelineOptions([ @@ -188,10 +205,8 @@ def test_flexrs_cost(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.flexResourceSchedulingGoal, - ( - dataflow.Environment.FlexResourceSchedulingGoalValueValuesEnum. - FLEXRS_COST_OPTIMIZED)) + env.proto.flex_resource_scheduling_goal, + (dataflow.FlexResourceSchedulingGoal.FLEXRS_COST_OPTIMIZED)) def test_flexrs_speed(self): pipeline_options = PipelineOptions([ @@ -207,10 +222,8 @@ def test_flexrs_speed(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.flexResourceSchedulingGoal, - ( - dataflow.Environment.FlexResourceSchedulingGoalValueValuesEnum. - FLEXRS_SPEED_OPTIMIZED)) + env.proto.flex_resource_scheduling_goal, + (dataflow.FlexResourceSchedulingGoal.FLEXRS_SPEED_OPTIMIZED)) def _verify_sdk_harness_container_images_get_set(self, pipeline_options): pipeline = Pipeline(options=pipeline_options) @@ -240,18 +253,18 @@ def _verify_sdk_harness_container_images_get_set(self, pipeline_options): '2.0.0', # any environment version FAKE_PIPELINE_URL, proto_pipeline) - worker_pool = env.proto.workerPools[0] + worker_pool = env.proto.worker_pools[0] - self.assertEqual(2, len(worker_pool.sdkHarnessContainerImages)) + self.assertEqual(2, len(worker_pool.sdk_harness_container_images)) # Only one of the environments is missing MULTI_CORE_BUNDLE_PROCESSING. self.assertEqual( 1, sum( - c.useSingleCorePerContainer - for c in worker_pool.sdkHarnessContainerImages)) + c.use_single_core_per_container + for c in worker_pool.sdk_harness_container_images)) - env_and_image = [(item.environmentId, item.containerImage) - for item in worker_pool.sdkHarnessContainerImages] + env_and_image = [(item.environment_id, item.container_image) + for item in worker_pool.sdk_harness_container_images] self.assertIn(('dummy_env_id', 'dummy_image'), env_and_image) self.assertIn((mock.ANY, 'test_default_image'), env_and_image) @@ -504,84 +517,69 @@ def test_default_job_name(self): regexp = 'beamapp-.*-[0-9]{10}-[0-9]{6}-[a-z0-9]{8}$' self.assertRegex(job_name, regexp) - def test_split_int(self): - number = 12345 - split_number = apiclient.to_split_int(number) - self.assertEqual((split_number.lowBits, split_number.highBits), (number, 0)) - shift_number = number << 32 - split_number = apiclient.to_split_int(shift_number) - self.assertEqual((split_number.lowBits, split_number.highBits), (0, number)) - def test_translate_distribution_using_accumulator(self): - metric_update = dataflow.CounterUpdate() + metric_update = dataflow.MetricUpdate() accumulator = mock.Mock() accumulator.min = 1 accumulator.max = 15 accumulator.sum = 16 accumulator.count = 2 apiclient.translate_distribution(accumulator, metric_update) - self.assertEqual(metric_update.distribution.min.lowBits, accumulator.min) - self.assertEqual(metric_update.distribution.max.lowBits, accumulator.max) - self.assertEqual(metric_update.distribution.sum.lowBits, accumulator.sum) - self.assertEqual( - metric_update.distribution.count.lowBits, accumulator.count) + self.assertEqual(metric_update.distribution['min'], accumulator.min) + self.assertEqual(metric_update.distribution['max'], accumulator.max) + self.assertEqual(metric_update.distribution['sum'], accumulator.sum) + self.assertEqual(metric_update.distribution['count'], accumulator.count) def test_translate_distribution_using_distribution_data(self): - metric_update = dataflow.CounterUpdate() + metric_update = dataflow.MetricUpdate() distribution_update = DistributionData(16, 2, 1, 15) apiclient.translate_distribution(distribution_update, metric_update) + self.assertEqual(metric_update.distribution['min'], distribution_update.min) + self.assertEqual(metric_update.distribution['max'], distribution_update.max) + self.assertEqual(metric_update.distribution['sum'], distribution_update.sum) self.assertEqual( - metric_update.distribution.min.lowBits, distribution_update.min) - self.assertEqual( - metric_update.distribution.max.lowBits, distribution_update.max) - self.assertEqual( - metric_update.distribution.sum.lowBits, distribution_update.sum) - self.assertEqual( - metric_update.distribution.count.lowBits, distribution_update.count) + metric_update.distribution['count'], distribution_update.count) def test_translate_distribution_using_dataflow_distribution_counter(self): counter_update = DataflowDistributionCounter() counter_update.add_input(1) counter_update.add_input(3) - metric_proto = dataflow.CounterUpdate() + metric_proto = dataflow.MetricUpdate() apiclient.translate_distribution(counter_update, metric_proto) histogram = mock.Mock(firstBucketOffset=None, bucketCounts=None) counter_update.translate_to_histogram(histogram) - self.assertEqual(metric_proto.distribution.min.lowBits, counter_update.min) - self.assertEqual(metric_proto.distribution.max.lowBits, counter_update.max) - self.assertEqual(metric_proto.distribution.sum.lowBits, counter_update.sum) - self.assertEqual( - metric_proto.distribution.count.lowBits, counter_update.count) - self.assertEqual( - metric_proto.distribution.histogram.bucketCounts, - histogram.bucketCounts) + self.assertEqual(metric_proto.distribution['min'], counter_update.min) + self.assertEqual(metric_proto.distribution['max'], counter_update.max) + self.assertEqual(metric_proto.distribution['sum'], counter_update.sum) + self.assertEqual(metric_proto.distribution['count'], counter_update.count) self.assertEqual( - metric_proto.distribution.histogram.firstBucketOffset, + metric_proto.distribution['firstBucketOffset'], histogram.firstBucketOffset) + self.assertEqual( + metric_proto.distribution['bucketCounts'], histogram.bucketCounts) def test_translate_means(self): - metric_update = dataflow.CounterUpdate() + metric_update = dataflow.MetricUpdate() accumulator = mock.Mock() accumulator.sum = 16 accumulator.count = 2 apiclient.MetricUpdateTranslators.translate_scalar_mean_int( accumulator, metric_update) - self.assertEqual(metric_update.integerMean.sum.lowBits, accumulator.sum) - self.assertEqual(metric_update.integerMean.count.lowBits, accumulator.count) + self.assertEqual(metric_update.mean_sum, accumulator.sum) + self.assertEqual(metric_update.mean_count, accumulator.count) accumulator.sum = 16.0 accumulator.count = 2 apiclient.MetricUpdateTranslators.translate_scalar_mean_float( accumulator, metric_update) - self.assertEqual(metric_update.floatingPointMean.sum, accumulator.sum) - self.assertEqual( - metric_update.floatingPointMean.count.lowBits, accumulator.count) + self.assertEqual(metric_update.mean_sum, accumulator.sum) + self.assertEqual(metric_update.mean_count, accumulator.count) def test_translate_means_using_distribution_accumulator(self): # This is the special case for MeanByteCount. # Which is reported over the FnAPI as a beam distribution, # and to the service as a MetricUpdate IntegerMean. - metric_update = dataflow.CounterUpdate() + metric_update = dataflow.MetricUpdate() accumulator = mock.Mock() accumulator.min = 7 accumulator.max = 9 @@ -589,16 +587,15 @@ def test_translate_means_using_distribution_accumulator(self): accumulator.count = 2 apiclient.MetricUpdateTranslators.translate_scalar_mean_int( accumulator, metric_update) - self.assertEqual(metric_update.integerMean.sum.lowBits, accumulator.sum) - self.assertEqual(metric_update.integerMean.count.lowBits, accumulator.count) + self.assertEqual(metric_update.mean_sum, accumulator.sum) + self.assertEqual(metric_update.mean_count, accumulator.count) accumulator.sum = 16.0 accumulator.count = 2 apiclient.MetricUpdateTranslators.translate_scalar_mean_float( accumulator, metric_update) - self.assertEqual(metric_update.floatingPointMean.sum, accumulator.sum) - self.assertEqual( - metric_update.floatingPointMean.count.lowBits, accumulator.count) + self.assertEqual(metric_update.mean_sum, accumulator.sum) + self.assertEqual(metric_update.mean_count, accumulator.count) def test_default_ip_configuration(self): pipeline_options = PipelineOptions( @@ -607,7 +604,9 @@ def test_default_ip_configuration(self): pipeline_options, '2.0.0', FAKE_PIPELINE_URL) - self.assertEqual(env.proto.workerPools[0].ipConfiguration, None) + self.assertEqual( + env.proto.worker_pools[0].ip_configuration, + dataflow.WorkerIPAddressConfiguration.WORKER_IP_UNSPECIFIED) def test_public_ip_configuration(self): pipeline_options = PipelineOptions( @@ -617,8 +616,8 @@ def test_public_ip_configuration(self): '2.0.0', FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].ipConfiguration, - dataflow.WorkerPool.IpConfigurationValueValuesEnum.WORKER_IP_PUBLIC) + env.proto.worker_pools[0].ip_configuration, + dataflow.WorkerIPAddressConfiguration.WORKER_IP_PUBLIC) def test_private_ip_configuration(self): pipeline_options = PipelineOptions( @@ -628,8 +627,8 @@ def test_private_ip_configuration(self): '2.0.0', FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].ipConfiguration, - dataflow.WorkerPool.IpConfigurationValueValuesEnum.WORKER_IP_PRIVATE) + env.proto.worker_pools[0].ip_configuration, + dataflow.WorkerIPAddressConfiguration.WORKER_IP_PRIVATE) def test_number_of_worker_harness_threads(self): pipeline_options = PipelineOptions([ @@ -642,7 +641,7 @@ def test_number_of_worker_harness_threads(self): pipeline_options, '2.0.0', FAKE_PIPELINE_URL) - self.assertEqual(env.proto.workerPools[0].numThreadsPerWorker, 2) + self.assertEqual(env.proto.worker_pools[0].num_threads_per_worker, 2) def test_disk_provisioning_options(self): pipeline_options = PipelineOptions([ @@ -657,9 +656,9 @@ def test_disk_provisioning_options(self): pipeline_options, '2.0.0', FAKE_PIPELINE_URL) - self.assertEqual(env.proto.workerPools[0].diskProvisionedIops, 4000) + self.assertEqual(env.proto.worker_pools[0].disk_provisioned_iops, 4000) self.assertEqual( - env.proto.workerPools[0].diskProvisionedThroughputMibps, 200) + env.proto.worker_pools[0].disk_provisioned_throughput_mibps, 200) @mock.patch( 'apache_beam.runners.dataflow.internal.apiclient.' @@ -721,7 +720,7 @@ def test_pinned_worker_harness_image_tag_used_in_dev_sdk(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, + env.proto.worker_pools[0].worker_harness_container_image, ( names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY + '/beam_python%d.%d_sdk:%s' % ( @@ -737,7 +736,7 @@ def test_pinned_worker_harness_image_tag_used_in_dev_sdk(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, + env.proto.worker_pools[0].worker_harness_container_image, ( names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY + '/beam_python%d.%d_sdk:%s' % ( @@ -759,7 +758,7 @@ def test_worker_harness_image_tag_matches_released_sdk_version(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, + env.proto.worker_pools[0].worker_harness_container_image, ( names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY + '/beam_python%d.%d_sdk:2.2.0' % @@ -773,7 +772,7 @@ def test_worker_harness_image_tag_matches_released_sdk_version(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, + env.proto.worker_pools[0].worker_harness_container_image, ( names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY + '/beam_python%d.%d_sdk:2.2.0' % @@ -793,7 +792,7 @@ def test_worker_harness_image_tag_matches_base_sdk_version_of_an_rc(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, + env.proto.worker_pools[0].worker_harness_container_image, ( names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY + '/beam_python%d.%d_sdk:2.2.0' % @@ -807,7 +806,7 @@ def test_worker_harness_image_tag_matches_base_sdk_version_of_an_rc(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, + env.proto.worker_pools[0].worker_harness_container_image, ( names.DATAFLOW_CONTAINER_IMAGE_REPOSITORY + '/beam_python%d.%d_sdk:2.2.0' % @@ -827,7 +826,7 @@ def test_worker_harness_override_takes_precedence_over_sdk_defaults(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, 'some:image') + env.proto.worker_pools[0].worker_harness_container_image, 'some:image') # batch, legacy pipeline. pipeline_options = PipelineOptions([ '--temp_location', @@ -840,7 +839,7 @@ def test_worker_harness_override_takes_precedence_over_sdk_defaults(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.workerPools[0].workerHarnessContainerImage, 'some:image') + env.proto.worker_pools[0].worker_harness_container_image, 'some:image') @mock.patch( 'apache_beam.runners.dataflow.internal.apiclient.Job.' @@ -859,7 +858,7 @@ def test_transform_name_mapping(self, mock_job): '{\"from\":\"to\"}' ]) job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertIsNotNone(job.proto.transformNameMapping) + self.assertIsNotNone(job.proto.transform_name_mapping) def test_created_from_snapshot_id(self): pipeline_options = PipelineOptions([ @@ -873,7 +872,7 @@ def test_created_from_snapshot_id(self): 'test_snapshot_id' ]) job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertEqual('test_snapshot_id', job.proto.createdFromSnapshotId) + self.assertEqual('test_snapshot_id', job.proto.created_from_snapshot_id) def test_labels(self): pipeline_options = PipelineOptions([ @@ -885,7 +884,7 @@ def test_labels(self): 'gs://test-location/temp' ]) job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertIsNone(job.proto.labels) + self.assertEqual(job.proto.labels, {}) pipeline_options = PipelineOptions([ '--project', @@ -906,17 +905,17 @@ def test_labels(self): 'key5' ]) job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertEqual(5, len(job.proto.labels.additionalProperties)) - self.assertEqual('key1', job.proto.labels.additionalProperties[0].key) - self.assertEqual('value1', job.proto.labels.additionalProperties[0].value) - self.assertEqual('key2', job.proto.labels.additionalProperties[1].key) - self.assertEqual('', job.proto.labels.additionalProperties[1].value) - self.assertEqual('key3', job.proto.labels.additionalProperties[2].key) - self.assertEqual('value3', job.proto.labels.additionalProperties[2].value) - self.assertEqual('key4', job.proto.labels.additionalProperties[3].key) - self.assertEqual('value4', job.proto.labels.additionalProperties[3].value) - self.assertEqual('key5', job.proto.labels.additionalProperties[4].key) - self.assertEqual('', job.proto.labels.additionalProperties[4].value) + self.assertEqual(5, len(job.proto.labels)) + self.assertTrue('key1' in job.proto.labels.keys()) + self.assertEqual('value1', job.proto.labels['key1']) + self.assertTrue('key2' in job.proto.labels.keys()) + self.assertEqual('', job.proto.labels['key2']) + self.assertTrue('key3' in job.proto.labels.keys()) + self.assertEqual('value3', job.proto.labels['key3']) + self.assertTrue('key4' in job.proto.labels.keys()) + self.assertEqual('value4', job.proto.labels['key4']) + self.assertTrue('key5' in job.proto.labels.keys()) + self.assertEqual('', job.proto.labels['key5']) pipeline_options = PipelineOptions([ '--project', @@ -929,13 +928,13 @@ def test_labels(self): '{ "name": "wrench", "mass": "1_3kg", "count": "3" }' ]) job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertEqual(3, len(job.proto.labels.additionalProperties)) - self.assertEqual('name', job.proto.labels.additionalProperties[0].key) - self.assertEqual('wrench', job.proto.labels.additionalProperties[0].value) - self.assertEqual('mass', job.proto.labels.additionalProperties[1].key) - self.assertEqual('1_3kg', job.proto.labels.additionalProperties[1].value) - self.assertEqual('count', job.proto.labels.additionalProperties[2].key) - self.assertEqual('3', job.proto.labels.additionalProperties[2].value) + self.assertEqual(3, len(job.proto.labels)) + self.assertTrue('name' in job.proto.labels.keys()) + self.assertEqual('wrench', job.proto.labels['name']) + self.assertTrue('mass' in job.proto.labels.keys()) + self.assertEqual('1_3kg', job.proto.labels['mass']) + self.assertTrue('count' in job.proto.labels.keys()) + self.assertEqual('3', job.proto.labels['count']) pipeline_options = PipelineOptions([ '--project', @@ -949,13 +948,13 @@ def test_labels(self): "name": "wrench", "mass": "1_3kg", "count": "3" } job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertEqual(3, len(job.proto.labels.additionalProperties)) - self.assertEqual('name', job.proto.labels.additionalProperties[0].key) - self.assertEqual('wrench', job.proto.labels.additionalProperties[0].value) - self.assertEqual('mass', job.proto.labels.additionalProperties[1].key) - self.assertEqual('1_3kg', job.proto.labels.additionalProperties[1].value) - self.assertEqual('count', job.proto.labels.additionalProperties[2].key) - self.assertEqual('3', job.proto.labels.additionalProperties[2].value) + self.assertEqual(3, len(job.proto.labels)) + self.assertTrue('name' in job.proto.labels.keys()) + self.assertEqual('wrench', job.proto.labels['name']) + self.assertTrue('mass' in job.proto.labels.keys()) + self.assertEqual('1_3kg', job.proto.labels['mass']) + self.assertTrue('count' in job.proto.labels.keys()) + self.assertEqual('3', job.proto.labels['count']) pipeline_options = PipelineOptions([ '--project', @@ -969,13 +968,13 @@ def test_labels(self): GoogleCloudOptions ).labels = '{ "name": "wrench", "mass": "1_3kg", "count": "3" }' job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertEqual(3, len(job.proto.labels.additionalProperties)) - self.assertEqual('name', job.proto.labels.additionalProperties[0].key) - self.assertEqual('wrench', job.proto.labels.additionalProperties[0].value) - self.assertEqual('mass', job.proto.labels.additionalProperties[1].key) - self.assertEqual('1_3kg', job.proto.labels.additionalProperties[1].value) - self.assertEqual('count', job.proto.labels.additionalProperties[2].key) - self.assertEqual('3', job.proto.labels.additionalProperties[2].value) + self.assertEqual(3, len(job.proto.labels)) + self.assertTrue('name' in job.proto.labels.keys()) + self.assertEqual('wrench', job.proto.labels['name']) + self.assertTrue('mass' in job.proto.labels.keys()) + self.assertEqual('1_3kg', job.proto.labels['mass']) + self.assertTrue('count' in job.proto.labels.keys()) + self.assertEqual('3', job.proto.labels['count']) def test_experiment_use_multiple_sdk_containers(self): pipeline_options = PipelineOptions([ @@ -990,7 +989,7 @@ def test_experiment_use_multiple_sdk_containers(self): ]) environment = apiclient.Environment([], pipeline_options, - 1, + '1', FAKE_PIPELINE_URL) self.assertIn('use_multiple_sdk_containers', environment.proto.experiments) @@ -1008,7 +1007,7 @@ def test_experiment_use_multiple_sdk_containers(self): ]) environment = apiclient.Environment([], pipeline_options, - 1, + '1', FAKE_PIPELINE_URL) self.assertIn('use_multiple_sdk_containers', environment.proto.experiments) @@ -1026,7 +1025,7 @@ def test_experiment_use_multiple_sdk_containers(self): ]) environment = apiclient.Environment([], pipeline_options, - 1, + '1', FAKE_PIPELINE_URL) self.assertNotIn( 'use_multiple_sdk_containers', environment.proto.experiments) @@ -1049,7 +1048,7 @@ def test_get_python_sdk_name(self): ]) environment = apiclient.Environment([], pipeline_options, - 1, + '1', FAKE_PIPELINE_URL) self.assertEqual( 'Apache Beam Python 3.9 SDK', environment._get_python_sdk_name()) @@ -1176,18 +1175,18 @@ def test_create_job_returns_existing_job(self): 'gs://test-location/temp', ]) job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) - self.assertTrue(job.proto.clientRequestId) # asserts non-empty string + self.assertTrue(job.proto.client_request_id) # asserts non-empty string pipeline_options.view_as(GoogleCloudOptions).no_auth = True client = apiclient.DataflowApplicationClient(pipeline_options) response = dataflow.Job() - # different clientRequestId from `job` - response.clientRequestId = "20210821081910123456-1234" + # different client_id_request from `job` + response.client_request_id = "20210821081910123456-1234" response.name = 'test_job_name' response.id = '2021-08-19_21_18_43-9756917246311111021' - with mock.patch.object(client._client.projects_locations_jobs, - 'Create', + with mock.patch.object(client._jobs_client, + 'create_job', side_effect=[response]): with mock.patch.object(client, 'create_job_description', side_effect=None): @@ -1220,20 +1219,20 @@ def test_update_job_returns_existing_job(self): job = apiclient.Job(pipeline_options, beam_runner_api_pb2.Pipeline()) job_id_for_name_mock.assert_called_once() - self.assertTrue(job.proto.clientRequestId) # asserts non-empty string + self.assertTrue(job.proto.client_request_id) # asserts non-empty string pipeline_options.view_as(GoogleCloudOptions).no_auth = True client = apiclient.DataflowApplicationClient(pipeline_options) response = dataflow.Job() # different clientRequestId from `job` - response.clientRequestId = "20210821083254123456-1234" + response.client_request_id = "20210821083254123456-1234" response.name = 'test_job_name' response.id = '2021-08-19_21_29_07-5725551945600207770' with mock.patch.object(client, 'create_job_description', side_effect=None): - with mock.patch.object(client._client.projects_locations_jobs, - 'Create', + with mock.patch.object(client._jobs_client, + 'create_job', side_effect=[response]): with self.assertRaises( @@ -1503,7 +1502,7 @@ def test_set_dataflow_service_option(self): pipeline_options, '2.0.0', #any environment version FAKE_PIPELINE_URL) - self.assertEqual(env.proto.serviceOptions, ['whizz=bang']) + self.assertEqual(env.proto.service_options, ['whizz=bang']) def test_enable_hot_key_logging(self): # Tests that the enable_hot_key_logging is not set by default. @@ -1514,7 +1513,9 @@ def test_enable_hot_key_logging(self): pipeline_options, '2.0.0', #any environment version FAKE_PIPELINE_URL) - self.assertIsNone(env.proto.debugOptions) + self.assertEqual( + env.proto.debug_options, + dataflow.DebugOptions(enable_hot_key_logging=False)) # Now test that it is set when given. pipeline_options = PipelineOptions([ @@ -1526,7 +1527,8 @@ def test_enable_hot_key_logging(self): '2.0.0', #any environment version FAKE_PIPELINE_URL) self.assertEqual( - env.proto.debugOptions, dataflow.DebugOptions(enableHotKeyLogging=True)) + env.proto.debug_options, + dataflow.DebugOptions(enable_hot_key_logging=True)) def _mock_uncached_copy(self, staging_root, src, sha256, dst_name=None): sha_prefix = sha256[0:2] diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/__init__.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/__init__.py deleted file mode 100644 index 8e69c725830a..000000000000 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""Common imports for generated dataflow client library.""" -# pylint:disable=wildcard-import - -import pkgutil - -# Protect against environments where apitools library is not available. -# pylint: disable=wrong-import-order, wrong-import-position -try: - from apitools.base.py import * - - from apache_beam.runners.dataflow.internal.clients.dataflow.dataflow_v1b3_client import * - from apache_beam.runners.dataflow.internal.clients.dataflow.dataflow_v1b3_messages import * -except ImportError: - pass -# pylint: enable=wrong-import-order, wrong-import-position - -__path__ = pkgutil.extend_path(__path__, __name__) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py deleted file mode 100644 index 179e51eb95e8..000000000000 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_client.py +++ /dev/null @@ -1,1318 +0,0 @@ -"""Generated client library for dataflow version v1b3.""" -# NOTE: This file is autogenerated and should not be edited by hand. - -from apitools.base.py import base_api -from . import dataflow_v1b3_messages as messages - - -class DataflowV1b3(base_api.BaseApiClient): - """Generated client library for service dataflow version v1b3.""" - - MESSAGES_MODULE = messages - BASE_URL = 'https://dataflow.googleapis.com/' - MTLS_BASE_URL = 'https://dataflow.mtls.googleapis.com/' - - _PACKAGE = 'dataflow' - _SCOPES = ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute'] - _VERSION = 'v1b3' - _CLIENT_ID = 'CLIENT_ID' - _CLIENT_SECRET = 'CLIENT_SECRET' - _USER_AGENT = 'x_Tw5K8nnjoRAqULM9PFAC2b' - _CLIENT_CLASS_NAME = 'DataflowV1b3' - _URL_VERSION = 'v1b3' - _API_KEY = None - - def __init__(self, url='', credentials=None, - get_credentials=True, http=None, model=None, - log_request=False, log_response=False, - credentials_args=None, default_global_params=None, - additional_http_headers=None, response_encoding=None): - """Create a new dataflow handle.""" - url = url or self.BASE_URL - super(DataflowV1b3, self).__init__( - url, credentials=credentials, - get_credentials=get_credentials, http=http, model=model, - log_request=log_request, log_response=log_response, - credentials_args=credentials_args, - default_global_params=default_global_params, - additional_http_headers=additional_http_headers, - response_encoding=response_encoding) - self.projects_jobs_debug = self.ProjectsJobsDebugService(self) - self.projects_jobs_messages = self.ProjectsJobsMessagesService(self) - self.projects_jobs_workItems = self.ProjectsJobsWorkItemsService(self) - self.projects_jobs = self.ProjectsJobsService(self) - self.projects_locations_flexTemplates = self.ProjectsLocationsFlexTemplatesService(self) - self.projects_locations_jobs_debug = self.ProjectsLocationsJobsDebugService(self) - self.projects_locations_jobs_messages = self.ProjectsLocationsJobsMessagesService(self) - self.projects_locations_jobs_snapshots = self.ProjectsLocationsJobsSnapshotsService(self) - self.projects_locations_jobs_stages = self.ProjectsLocationsJobsStagesService(self) - self.projects_locations_jobs_workItems = self.ProjectsLocationsJobsWorkItemsService(self) - self.projects_locations_jobs = self.ProjectsLocationsJobsService(self) - self.projects_locations_snapshots = self.ProjectsLocationsSnapshotsService(self) - self.projects_locations_templates = self.ProjectsLocationsTemplatesService(self) - self.projects_locations = self.ProjectsLocationsService(self) - self.projects_snapshots = self.ProjectsSnapshotsService(self) - self.projects_templates = self.ProjectsTemplatesService(self) - self.projects = self.ProjectsService(self) - - class ProjectsJobsDebugService(base_api.BaseApiService): - """Service class for the projects_jobs_debug resource.""" - - _NAME = 'projects_jobs_debug' - - def __init__(self, client): - super(DataflowV1b3.ProjectsJobsDebugService, self).__init__(client) - self._upload_configs = { - } - - def GetConfig(self, request, global_params=None): - r"""Get encoded debug configuration for component. Not cacheable. - - Args: - request: (DataflowProjectsJobsDebugGetConfigRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (GetDebugConfigResponse) The response message. - """ - config = self.GetMethodConfig('GetConfig') - return self._RunMethod( - config, request, global_params=global_params) - - GetConfig.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.jobs.debug.getConfig', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}/debug/getConfig', - request_field='getDebugConfigRequest', - request_type_name='DataflowProjectsJobsDebugGetConfigRequest', - response_type_name='GetDebugConfigResponse', - supports_download=False, - ) - - def SendCapture(self, request, global_params=None): - r"""Send encoded debug capture data for component. - - Args: - request: (DataflowProjectsJobsDebugSendCaptureRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (SendDebugCaptureResponse) The response message. - """ - config = self.GetMethodConfig('SendCapture') - return self._RunMethod( - config, request, global_params=global_params) - - SendCapture.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.jobs.debug.sendCapture', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}/debug/sendCapture', - request_field='sendDebugCaptureRequest', - request_type_name='DataflowProjectsJobsDebugSendCaptureRequest', - response_type_name='SendDebugCaptureResponse', - supports_download=False, - ) - - class ProjectsJobsMessagesService(base_api.BaseApiService): - """Service class for the projects_jobs_messages resource.""" - - _NAME = 'projects_jobs_messages' - - def __init__(self, client): - super(DataflowV1b3.ProjectsJobsMessagesService, self).__init__(client) - self._upload_configs = { - } - - def List(self, request, global_params=None): - r"""Request the job status. To request the status of a job, we recommend using `projects.locations.jobs.messages.list` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.messages.list` is not recommended, as you can only request the status of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsJobsMessagesListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListJobMessagesResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.jobs.messages.list', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=['endTime', 'location', 'minimumImportance', 'pageSize', 'pageToken', 'startTime'], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}/messages', - request_field='', - request_type_name='DataflowProjectsJobsMessagesListRequest', - response_type_name='ListJobMessagesResponse', - supports_download=False, - ) - - class ProjectsJobsWorkItemsService(base_api.BaseApiService): - """Service class for the projects_jobs_workItems resource.""" - - _NAME = 'projects_jobs_workItems' - - def __init__(self, client): - super(DataflowV1b3.ProjectsJobsWorkItemsService, self).__init__(client) - self._upload_configs = { - } - - def Lease(self, request, global_params=None): - r"""Leases a dataflow WorkItem to run. - - Args: - request: (DataflowProjectsJobsWorkItemsLeaseRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (LeaseWorkItemResponse) The response message. - """ - config = self.GetMethodConfig('Lease') - return self._RunMethod( - config, request, global_params=global_params) - - Lease.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.jobs.workItems.lease', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}/workItems:lease', - request_field='leaseWorkItemRequest', - request_type_name='DataflowProjectsJobsWorkItemsLeaseRequest', - response_type_name='LeaseWorkItemResponse', - supports_download=False, - ) - - def ReportStatus(self, request, global_params=None): - r"""Reports the status of dataflow WorkItems leased by a worker. - - Args: - request: (DataflowProjectsJobsWorkItemsReportStatusRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ReportWorkItemStatusResponse) The response message. - """ - config = self.GetMethodConfig('ReportStatus') - return self._RunMethod( - config, request, global_params=global_params) - - ReportStatus.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.jobs.workItems.reportStatus', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}/workItems:reportStatus', - request_field='reportWorkItemStatusRequest', - request_type_name='DataflowProjectsJobsWorkItemsReportStatusRequest', - response_type_name='ReportWorkItemStatusResponse', - supports_download=False, - ) - - class ProjectsJobsService(base_api.BaseApiService): - """Service class for the projects_jobs resource.""" - - _NAME = 'projects_jobs' - - def __init__(self, client): - super(DataflowV1b3.ProjectsJobsService, self).__init__(client) - self._upload_configs = { - } - - def Aggregated(self, request, global_params=None): - r"""List the jobs of a project across all regions. **Note:** This method doesn't support filtering the list of jobs by name. - - Args: - request: (DataflowProjectsJobsAggregatedRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListJobsResponse) The response message. - """ - config = self.GetMethodConfig('Aggregated') - return self._RunMethod( - config, request, global_params=global_params) - - Aggregated.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.jobs.aggregated', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['filter', 'location', 'name', 'pageSize', 'pageToken', 'view'], - relative_path='v1b3/projects/{projectId}/jobs:aggregated', - request_field='', - request_type_name='DataflowProjectsJobsAggregatedRequest', - response_type_name='ListJobsResponse', - supports_download=False, - ) - - def Create(self, request, global_params=None): - r"""Creates a Dataflow job. To create a job, we recommend using `projects.locations.jobs.create` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.create` is not recommended, as your job will always start in `us-central1`. Do not enter confidential information when you supply string values using the API. - - Args: - request: (DataflowProjectsJobsCreateRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Create') - return self._RunMethod( - config, request, global_params=global_params) - - Create.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.jobs.create', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['location', 'replaceJobId', 'view'], - relative_path='v1b3/projects/{projectId}/jobs', - request_field='job', - request_type_name='DataflowProjectsJobsCreateRequest', - response_type_name='Job', - supports_download=False, - ) - - def Get(self, request, global_params=None): - r"""Gets the state of the specified Cloud Dataflow job. To get the state of a job, we recommend using `projects.locations.jobs.get` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.get` is not recommended, as you can only get the state of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsJobsGetRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Get') - return self._RunMethod( - config, request, global_params=global_params) - - Get.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.jobs.get', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=['location', 'view'], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}', - request_field='', - request_type_name='DataflowProjectsJobsGetRequest', - response_type_name='Job', - supports_download=False, - ) - - def GetMetrics(self, request, global_params=None): - r"""Request the job status. To request the status of a job, we recommend using `projects.locations.jobs.getMetrics` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.getMetrics` is not recommended, as you can only request the status of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsJobsGetMetricsRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (JobMetrics) The response message. - """ - config = self.GetMethodConfig('GetMetrics') - return self._RunMethod( - config, request, global_params=global_params) - - GetMetrics.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.jobs.getMetrics', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=['location', 'startTime'], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}/metrics', - request_field='', - request_type_name='DataflowProjectsJobsGetMetricsRequest', - response_type_name='JobMetrics', - supports_download=False, - ) - - def List(self, request, global_params=None): - r"""List the jobs of a project. To list the jobs of a project in a region, we recommend using `projects.locations.jobs.list` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). To list the all jobs across all regions, use `projects.jobs.aggregated`. Using `projects.jobs.list` is not recommended, because you can only get the list of jobs that are running in `us-central1`. `projects.locations.jobs.list` and `projects.jobs.list` support filtering the list of jobs by name. Filtering by name isn't supported by `projects.jobs.aggregated`. - - Args: - request: (DataflowProjectsJobsListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListJobsResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.jobs.list', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['filter', 'location', 'name', 'pageSize', 'pageToken', 'view'], - relative_path='v1b3/projects/{projectId}/jobs', - request_field='', - request_type_name='DataflowProjectsJobsListRequest', - response_type_name='ListJobsResponse', - supports_download=False, - ) - - def Snapshot(self, request, global_params=None): - r"""Snapshot the state of a streaming job. - - Args: - request: (DataflowProjectsJobsSnapshotRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Snapshot) The response message. - """ - config = self.GetMethodConfig('Snapshot') - return self._RunMethod( - config, request, global_params=global_params) - - Snapshot.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.jobs.snapshot', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}:snapshot', - request_field='snapshotJobRequest', - request_type_name='DataflowProjectsJobsSnapshotRequest', - response_type_name='Snapshot', - supports_download=False, - ) - - def Update(self, request, global_params=None): - r"""Updates the state of an existing Cloud Dataflow job. To update the state of an existing job, we recommend using `projects.locations.jobs.update` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.update` is not recommended, as you can only update the state of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsJobsUpdateRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Update') - return self._RunMethod( - config, request, global_params=global_params) - - Update.method_config = lambda: base_api.ApiMethodInfo( - http_method='PUT', - method_id='dataflow.projects.jobs.update', - ordered_params=['projectId', 'jobId'], - path_params=['jobId', 'projectId'], - query_params=['location', 'updateMask'], - relative_path='v1b3/projects/{projectId}/jobs/{jobId}', - request_field='job', - request_type_name='DataflowProjectsJobsUpdateRequest', - response_type_name='Job', - supports_download=False, - ) - - class ProjectsLocationsFlexTemplatesService(base_api.BaseApiService): - """Service class for the projects_locations_flexTemplates resource.""" - - _NAME = 'projects_locations_flexTemplates' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsFlexTemplatesService, self).__init__(client) - self._upload_configs = { - } - - def Launch(self, request, global_params=None): - r"""Launch a job with a FlexTemplate. - - Args: - request: (DataflowProjectsLocationsFlexTemplatesLaunchRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (LaunchFlexTemplateResponse) The response message. - """ - config = self.GetMethodConfig('Launch') - return self._RunMethod( - config, request, global_params=global_params) - - Launch.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.flexTemplates.launch', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/flexTemplates:launch', - request_field='launchFlexTemplateRequest', - request_type_name='DataflowProjectsLocationsFlexTemplatesLaunchRequest', - response_type_name='LaunchFlexTemplateResponse', - supports_download=False, - ) - - class ProjectsLocationsJobsDebugService(base_api.BaseApiService): - """Service class for the projects_locations_jobs_debug resource.""" - - _NAME = 'projects_locations_jobs_debug' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsJobsDebugService, self).__init__(client) - self._upload_configs = { - } - - def GetConfig(self, request, global_params=None): - r"""Get encoded debug configuration for component. Not cacheable. - - Args: - request: (DataflowProjectsLocationsJobsDebugGetConfigRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (GetDebugConfigResponse) The response message. - """ - config = self.GetMethodConfig('GetConfig') - return self._RunMethod( - config, request, global_params=global_params) - - GetConfig.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.debug.getConfig', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/debug/getConfig', - request_field='getDebugConfigRequest', - request_type_name='DataflowProjectsLocationsJobsDebugGetConfigRequest', - response_type_name='GetDebugConfigResponse', - supports_download=False, - ) - - def GetWorkerStacktraces(self, request, global_params=None): - r"""Get worker stacktraces from debug capture. - - Args: - request: (DataflowProjectsLocationsJobsDebugGetWorkerStacktracesRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (GetWorkerStacktracesResponse) The response message. - """ - config = self.GetMethodConfig('GetWorkerStacktraces') - return self._RunMethod( - config, request, global_params=global_params) - - GetWorkerStacktraces.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.debug.getWorkerStacktraces', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/debug/getWorkerStacktraces', - request_field='getWorkerStacktracesRequest', - request_type_name='DataflowProjectsLocationsJobsDebugGetWorkerStacktracesRequest', - response_type_name='GetWorkerStacktracesResponse', - supports_download=False, - ) - - def SendCapture(self, request, global_params=None): - r"""Send encoded debug capture data for component. - - Args: - request: (DataflowProjectsLocationsJobsDebugSendCaptureRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (SendDebugCaptureResponse) The response message. - """ - config = self.GetMethodConfig('SendCapture') - return self._RunMethod( - config, request, global_params=global_params) - - SendCapture.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.debug.sendCapture', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/debug/sendCapture', - request_field='sendDebugCaptureRequest', - request_type_name='DataflowProjectsLocationsJobsDebugSendCaptureRequest', - response_type_name='SendDebugCaptureResponse', - supports_download=False, - ) - - class ProjectsLocationsJobsMessagesService(base_api.BaseApiService): - """Service class for the projects_locations_jobs_messages resource.""" - - _NAME = 'projects_locations_jobs_messages' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsJobsMessagesService, self).__init__(client) - self._upload_configs = { - } - - def List(self, request, global_params=None): - r"""Request the job status. To request the status of a job, we recommend using `projects.locations.jobs.messages.list` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.messages.list` is not recommended, as you can only request the status of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsLocationsJobsMessagesListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListJobMessagesResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.messages.list', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=['endTime', 'minimumImportance', 'pageSize', 'pageToken', 'startTime'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/messages', - request_field='', - request_type_name='DataflowProjectsLocationsJobsMessagesListRequest', - response_type_name='ListJobMessagesResponse', - supports_download=False, - ) - - class ProjectsLocationsJobsSnapshotsService(base_api.BaseApiService): - """Service class for the projects_locations_jobs_snapshots resource.""" - - _NAME = 'projects_locations_jobs_snapshots' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsJobsSnapshotsService, self).__init__(client) - self._upload_configs = { - } - - def List(self, request, global_params=None): - r"""Lists snapshots. - - Args: - request: (DataflowProjectsLocationsJobsSnapshotsListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListSnapshotsResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.snapshots.list', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/snapshots', - request_field='', - request_type_name='DataflowProjectsLocationsJobsSnapshotsListRequest', - response_type_name='ListSnapshotsResponse', - supports_download=False, - ) - - class ProjectsLocationsJobsStagesService(base_api.BaseApiService): - """Service class for the projects_locations_jobs_stages resource.""" - - _NAME = 'projects_locations_jobs_stages' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsJobsStagesService, self).__init__(client) - self._upload_configs = { - } - - def GetExecutionDetails(self, request, global_params=None): - r"""Request detailed information about the execution status of a stage of the job. EXPERIMENTAL. This API is subject to change or removal without notice. - - Args: - request: (DataflowProjectsLocationsJobsStagesGetExecutionDetailsRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (StageExecutionDetails) The response message. - """ - config = self.GetMethodConfig('GetExecutionDetails') - return self._RunMethod( - config, request, global_params=global_params) - - GetExecutionDetails.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.stages.getExecutionDetails', - ordered_params=['projectId', 'location', 'jobId', 'stageId'], - path_params=['jobId', 'location', 'projectId', 'stageId'], - query_params=['endTime', 'pageSize', 'pageToken', 'startTime'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/stages/{stageId}/executionDetails', - request_field='', - request_type_name='DataflowProjectsLocationsJobsStagesGetExecutionDetailsRequest', - response_type_name='StageExecutionDetails', - supports_download=False, - ) - - class ProjectsLocationsJobsWorkItemsService(base_api.BaseApiService): - """Service class for the projects_locations_jobs_workItems resource.""" - - _NAME = 'projects_locations_jobs_workItems' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsJobsWorkItemsService, self).__init__(client) - self._upload_configs = { - } - - def Lease(self, request, global_params=None): - r"""Leases a dataflow WorkItem to run. - - Args: - request: (DataflowProjectsLocationsJobsWorkItemsLeaseRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (LeaseWorkItemResponse) The response message. - """ - config = self.GetMethodConfig('Lease') - return self._RunMethod( - config, request, global_params=global_params) - - Lease.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.workItems.lease', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/workItems:lease', - request_field='leaseWorkItemRequest', - request_type_name='DataflowProjectsLocationsJobsWorkItemsLeaseRequest', - response_type_name='LeaseWorkItemResponse', - supports_download=False, - ) - - def ReportStatus(self, request, global_params=None): - r"""Reports the status of dataflow WorkItems leased by a worker. - - Args: - request: (DataflowProjectsLocationsJobsWorkItemsReportStatusRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ReportWorkItemStatusResponse) The response message. - """ - config = self.GetMethodConfig('ReportStatus') - return self._RunMethod( - config, request, global_params=global_params) - - ReportStatus.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.workItems.reportStatus', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/workItems:reportStatus', - request_field='reportWorkItemStatusRequest', - request_type_name='DataflowProjectsLocationsJobsWorkItemsReportStatusRequest', - response_type_name='ReportWorkItemStatusResponse', - supports_download=False, - ) - - class ProjectsLocationsJobsService(base_api.BaseApiService): - """Service class for the projects_locations_jobs resource.""" - - _NAME = 'projects_locations_jobs' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsJobsService, self).__init__(client) - self._upload_configs = { - } - - def Create(self, request, global_params=None): - r"""Creates a Dataflow job. To create a job, we recommend using `projects.locations.jobs.create` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.create` is not recommended, as your job will always start in `us-central1`. Do not enter confidential information when you supply string values using the API. - - Args: - request: (DataflowProjectsLocationsJobsCreateRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Create') - return self._RunMethod( - config, request, global_params=global_params) - - Create.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.create', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=['replaceJobId', 'view'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs', - request_field='job', - request_type_name='DataflowProjectsLocationsJobsCreateRequest', - response_type_name='Job', - supports_download=False, - ) - - def Get(self, request, global_params=None): - r"""Gets the state of the specified Cloud Dataflow job. To get the state of a job, we recommend using `projects.locations.jobs.get` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.get` is not recommended, as you can only get the state of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsLocationsJobsGetRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Get') - return self._RunMethod( - config, request, global_params=global_params) - - Get.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.get', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=['view'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}', - request_field='', - request_type_name='DataflowProjectsLocationsJobsGetRequest', - response_type_name='Job', - supports_download=False, - ) - - def GetExecutionDetails(self, request, global_params=None): - r"""Request detailed information about the execution status of the job. EXPERIMENTAL. This API is subject to change or removal without notice. - - Args: - request: (DataflowProjectsLocationsJobsGetExecutionDetailsRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (JobExecutionDetails) The response message. - """ - config = self.GetMethodConfig('GetExecutionDetails') - return self._RunMethod( - config, request, global_params=global_params) - - GetExecutionDetails.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.getExecutionDetails', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=['pageSize', 'pageToken'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/executionDetails', - request_field='', - request_type_name='DataflowProjectsLocationsJobsGetExecutionDetailsRequest', - response_type_name='JobExecutionDetails', - supports_download=False, - ) - - def GetMetrics(self, request, global_params=None): - r"""Request the job status. To request the status of a job, we recommend using `projects.locations.jobs.getMetrics` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.getMetrics` is not recommended, as you can only request the status of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsLocationsJobsGetMetricsRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (JobMetrics) The response message. - """ - config = self.GetMethodConfig('GetMetrics') - return self._RunMethod( - config, request, global_params=global_params) - - GetMetrics.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.getMetrics', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=['startTime'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}/metrics', - request_field='', - request_type_name='DataflowProjectsLocationsJobsGetMetricsRequest', - response_type_name='JobMetrics', - supports_download=False, - ) - - def List(self, request, global_params=None): - r"""List the jobs of a project. To list the jobs of a project in a region, we recommend using `projects.locations.jobs.list` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). To list the all jobs across all regions, use `projects.jobs.aggregated`. Using `projects.jobs.list` is not recommended, because you can only get the list of jobs that are running in `us-central1`. `projects.locations.jobs.list` and `projects.jobs.list` support filtering the list of jobs by name. Filtering by name isn't supported by `projects.jobs.aggregated`. - - Args: - request: (DataflowProjectsLocationsJobsListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListJobsResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.jobs.list', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=['filter', 'name', 'pageSize', 'pageToken', 'view'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs', - request_field='', - request_type_name='DataflowProjectsLocationsJobsListRequest', - response_type_name='ListJobsResponse', - supports_download=False, - ) - - def Snapshot(self, request, global_params=None): - r"""Snapshot the state of a streaming job. - - Args: - request: (DataflowProjectsLocationsJobsSnapshotRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Snapshot) The response message. - """ - config = self.GetMethodConfig('Snapshot') - return self._RunMethod( - config, request, global_params=global_params) - - Snapshot.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.jobs.snapshot', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}:snapshot', - request_field='snapshotJobRequest', - request_type_name='DataflowProjectsLocationsJobsSnapshotRequest', - response_type_name='Snapshot', - supports_download=False, - ) - - def Update(self, request, global_params=None): - r"""Updates the state of an existing Cloud Dataflow job. To update the state of an existing job, we recommend using `projects.locations.jobs.update` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.jobs.update` is not recommended, as you can only update the state of jobs that are running in `us-central1`. - - Args: - request: (DataflowProjectsLocationsJobsUpdateRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Update') - return self._RunMethod( - config, request, global_params=global_params) - - Update.method_config = lambda: base_api.ApiMethodInfo( - http_method='PUT', - method_id='dataflow.projects.locations.jobs.update', - ordered_params=['projectId', 'location', 'jobId'], - path_params=['jobId', 'location', 'projectId'], - query_params=['updateMask'], - relative_path='v1b3/projects/{projectId}/locations/{location}/jobs/{jobId}', - request_field='job', - request_type_name='DataflowProjectsLocationsJobsUpdateRequest', - response_type_name='Job', - supports_download=False, - ) - - class ProjectsLocationsSnapshotsService(base_api.BaseApiService): - """Service class for the projects_locations_snapshots resource.""" - - _NAME = 'projects_locations_snapshots' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsSnapshotsService, self).__init__(client) - self._upload_configs = { - } - - def Delete(self, request, global_params=None): - r"""Deletes a snapshot. - - Args: - request: (DataflowProjectsLocationsSnapshotsDeleteRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (DeleteSnapshotResponse) The response message. - """ - config = self.GetMethodConfig('Delete') - return self._RunMethod( - config, request, global_params=global_params) - - Delete.method_config = lambda: base_api.ApiMethodInfo( - http_method='DELETE', - method_id='dataflow.projects.locations.snapshots.delete', - ordered_params=['projectId', 'location', 'snapshotId'], - path_params=['location', 'projectId', 'snapshotId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/snapshots/{snapshotId}', - request_field='', - request_type_name='DataflowProjectsLocationsSnapshotsDeleteRequest', - response_type_name='DeleteSnapshotResponse', - supports_download=False, - ) - - def Get(self, request, global_params=None): - r"""Gets information about a snapshot. - - Args: - request: (DataflowProjectsLocationsSnapshotsGetRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Snapshot) The response message. - """ - config = self.GetMethodConfig('Get') - return self._RunMethod( - config, request, global_params=global_params) - - Get.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.snapshots.get', - ordered_params=['projectId', 'location', 'snapshotId'], - path_params=['location', 'projectId', 'snapshotId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/snapshots/{snapshotId}', - request_field='', - request_type_name='DataflowProjectsLocationsSnapshotsGetRequest', - response_type_name='Snapshot', - supports_download=False, - ) - - def List(self, request, global_params=None): - r"""Lists snapshots. - - Args: - request: (DataflowProjectsLocationsSnapshotsListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListSnapshotsResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.snapshots.list', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=['jobId'], - relative_path='v1b3/projects/{projectId}/locations/{location}/snapshots', - request_field='', - request_type_name='DataflowProjectsLocationsSnapshotsListRequest', - response_type_name='ListSnapshotsResponse', - supports_download=False, - ) - - class ProjectsLocationsTemplatesService(base_api.BaseApiService): - """Service class for the projects_locations_templates resource.""" - - _NAME = 'projects_locations_templates' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsTemplatesService, self).__init__(client) - self._upload_configs = { - } - - def Create(self, request, global_params=None): - r"""Creates a Cloud Dataflow job from a template. Do not enter confidential information when you supply string values using the API. To create a job, we recommend using `projects.locations.templates.create` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.templates.create` is not recommended, because your job will always start in `us-central1`. - - Args: - request: (DataflowProjectsLocationsTemplatesCreateRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Create') - return self._RunMethod( - config, request, global_params=global_params) - - Create.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.templates.create', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/templates', - request_field='createJobFromTemplateRequest', - request_type_name='DataflowProjectsLocationsTemplatesCreateRequest', - response_type_name='Job', - supports_download=False, - ) - - def Get(self, request, global_params=None): - r"""Get the template associated with a template. To get the template, we recommend using `projects.locations.templates.get` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.templates.get` is not recommended, because only templates that are running in `us-central1` are retrieved. - - Args: - request: (DataflowProjectsLocationsTemplatesGetRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (GetTemplateResponse) The response message. - """ - config = self.GetMethodConfig('Get') - return self._RunMethod( - config, request, global_params=global_params) - - Get.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.locations.templates.get', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=['gcsPath', 'view'], - relative_path='v1b3/projects/{projectId}/locations/{location}/templates:get', - request_field='', - request_type_name='DataflowProjectsLocationsTemplatesGetRequest', - response_type_name='GetTemplateResponse', - supports_download=False, - ) - - def Launch(self, request, global_params=None): - r"""Launches a template. To launch a template, we recommend using `projects.locations.templates.launch` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.templates.launch` is not recommended, because jobs launched from the template will always start in `us-central1`. - - Args: - request: (DataflowProjectsLocationsTemplatesLaunchRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (LaunchTemplateResponse) The response message. - """ - config = self.GetMethodConfig('Launch') - return self._RunMethod( - config, request, global_params=global_params) - - Launch.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.templates.launch', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=['dynamicTemplate_gcsPath', 'dynamicTemplate_stagingLocation', 'gcsPath', 'validateOnly'], - relative_path='v1b3/projects/{projectId}/locations/{location}/templates:launch', - request_field='launchTemplateParameters', - request_type_name='DataflowProjectsLocationsTemplatesLaunchRequest', - response_type_name='LaunchTemplateResponse', - supports_download=False, - ) - - class ProjectsLocationsService(base_api.BaseApiService): - """Service class for the projects_locations resource.""" - - _NAME = 'projects_locations' - - def __init__(self, client): - super(DataflowV1b3.ProjectsLocationsService, self).__init__(client) - self._upload_configs = { - } - - def WorkerMessages(self, request, global_params=None): - r"""Send a worker_message to the service. - - Args: - request: (DataflowProjectsLocationsWorkerMessagesRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (SendWorkerMessagesResponse) The response message. - """ - config = self.GetMethodConfig('WorkerMessages') - return self._RunMethod( - config, request, global_params=global_params) - - WorkerMessages.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.locations.workerMessages', - ordered_params=['projectId', 'location'], - path_params=['location', 'projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/locations/{location}/WorkerMessages', - request_field='sendWorkerMessagesRequest', - request_type_name='DataflowProjectsLocationsWorkerMessagesRequest', - response_type_name='SendWorkerMessagesResponse', - supports_download=False, - ) - - class ProjectsSnapshotsService(base_api.BaseApiService): - """Service class for the projects_snapshots resource.""" - - _NAME = 'projects_snapshots' - - def __init__(self, client): - super(DataflowV1b3.ProjectsSnapshotsService, self).__init__(client) - self._upload_configs = { - } - - def Get(self, request, global_params=None): - r"""Gets information about a snapshot. - - Args: - request: (DataflowProjectsSnapshotsGetRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Snapshot) The response message. - """ - config = self.GetMethodConfig('Get') - return self._RunMethod( - config, request, global_params=global_params) - - Get.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.snapshots.get', - ordered_params=['projectId', 'snapshotId'], - path_params=['projectId', 'snapshotId'], - query_params=['location'], - relative_path='v1b3/projects/{projectId}/snapshots/{snapshotId}', - request_field='', - request_type_name='DataflowProjectsSnapshotsGetRequest', - response_type_name='Snapshot', - supports_download=False, - ) - - def List(self, request, global_params=None): - r"""Lists snapshots. - - Args: - request: (DataflowProjectsSnapshotsListRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (ListSnapshotsResponse) The response message. - """ - config = self.GetMethodConfig('List') - return self._RunMethod( - config, request, global_params=global_params) - - List.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.snapshots.list', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['jobId', 'location'], - relative_path='v1b3/projects/{projectId}/snapshots', - request_field='', - request_type_name='DataflowProjectsSnapshotsListRequest', - response_type_name='ListSnapshotsResponse', - supports_download=False, - ) - - class ProjectsTemplatesService(base_api.BaseApiService): - """Service class for the projects_templates resource.""" - - _NAME = 'projects_templates' - - def __init__(self, client): - super(DataflowV1b3.ProjectsTemplatesService, self).__init__(client) - self._upload_configs = { - } - - def Create(self, request, global_params=None): - r"""Creates a Cloud Dataflow job from a template. Do not enter confidential information when you supply string values using the API. To create a job, we recommend using `projects.locations.templates.create` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.templates.create` is not recommended, because your job will always start in `us-central1`. - - Args: - request: (DataflowProjectsTemplatesCreateRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (Job) The response message. - """ - config = self.GetMethodConfig('Create') - return self._RunMethod( - config, request, global_params=global_params) - - Create.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.templates.create', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/templates', - request_field='createJobFromTemplateRequest', - request_type_name='DataflowProjectsTemplatesCreateRequest', - response_type_name='Job', - supports_download=False, - ) - - def Get(self, request, global_params=None): - r"""Get the template associated with a template. To get the template, we recommend using `projects.locations.templates.get` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.templates.get` is not recommended, because only templates that are running in `us-central1` are retrieved. - - Args: - request: (DataflowProjectsTemplatesGetRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (GetTemplateResponse) The response message. - """ - config = self.GetMethodConfig('Get') - return self._RunMethod( - config, request, global_params=global_params) - - Get.method_config = lambda: base_api.ApiMethodInfo( - http_method='GET', - method_id='dataflow.projects.templates.get', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['gcsPath', 'location', 'view'], - relative_path='v1b3/projects/{projectId}/templates:get', - request_field='', - request_type_name='DataflowProjectsTemplatesGetRequest', - response_type_name='GetTemplateResponse', - supports_download=False, - ) - - def Launch(self, request, global_params=None): - r"""Launches a template. To launch a template, we recommend using `projects.locations.templates.launch` with a [regional endpoint] (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints). Using `projects.templates.launch` is not recommended, because jobs launched from the template will always start in `us-central1`. - - Args: - request: (DataflowProjectsTemplatesLaunchRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (LaunchTemplateResponse) The response message. - """ - config = self.GetMethodConfig('Launch') - return self._RunMethod( - config, request, global_params=global_params) - - Launch.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.templates.launch', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['dynamicTemplate_gcsPath', 'dynamicTemplate_stagingLocation', 'gcsPath', 'location', 'validateOnly'], - relative_path='v1b3/projects/{projectId}/templates:launch', - request_field='launchTemplateParameters', - request_type_name='DataflowProjectsTemplatesLaunchRequest', - response_type_name='LaunchTemplateResponse', - supports_download=False, - ) - - class ProjectsService(base_api.BaseApiService): - """Service class for the projects resource.""" - - _NAME = 'projects' - - def __init__(self, client): - super(DataflowV1b3.ProjectsService, self).__init__(client) - self._upload_configs = { - } - - def DeleteSnapshots(self, request, global_params=None): - r"""Deletes a snapshot. - - Args: - request: (DataflowProjectsDeleteSnapshotsRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (DeleteSnapshotResponse) The response message. - """ - config = self.GetMethodConfig('DeleteSnapshots') - return self._RunMethod( - config, request, global_params=global_params) - - DeleteSnapshots.method_config = lambda: base_api.ApiMethodInfo( - http_method='DELETE', - method_id='dataflow.projects.deleteSnapshots', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=['location', 'snapshotId'], - relative_path='v1b3/projects/{projectId}/snapshots', - request_field='', - request_type_name='DataflowProjectsDeleteSnapshotsRequest', - response_type_name='DeleteSnapshotResponse', - supports_download=False, - ) - - def WorkerMessages(self, request, global_params=None): - r"""Send a worker_message to the service. - - Args: - request: (DataflowProjectsWorkerMessagesRequest) input message - global_params: (StandardQueryParameters, default: None) global arguments - Returns: - (SendWorkerMessagesResponse) The response message. - """ - config = self.GetMethodConfig('WorkerMessages') - return self._RunMethod( - config, request, global_params=global_params) - - WorkerMessages.method_config = lambda: base_api.ApiMethodInfo( - http_method='POST', - method_id='dataflow.projects.workerMessages', - ordered_params=['projectId'], - path_params=['projectId'], - query_params=[], - relative_path='v1b3/projects/{projectId}/WorkerMessages', - request_field='sendWorkerMessagesRequest', - request_type_name='DataflowProjectsWorkerMessagesRequest', - response_type_name='SendWorkerMessagesResponse', - supports_download=False, - ) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py deleted file mode 100644 index 19822d4428b0..000000000000 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/dataflow_v1b3_messages.py +++ /dev/null @@ -1,8077 +0,0 @@ -"""Generated message classes for dataflow version v1b3. - -Manages Google Cloud Dataflow projects on Google Cloud Platform. -""" -# NOTE: This file is autogenerated and should not be edited by hand. - -from apitools.base.protorpclite import messages as _messages -from apitools.base.py import encoding -from apitools.base.py import extra_types - - -package = 'dataflow' - - -class ApproximateProgress(_messages.Message): - r"""Obsolete in favor of ApproximateReportedProgress and - ApproximateSplitRequest. - - Fields: - percentComplete: Obsolete. - position: Obsolete. - remainingTime: Obsolete. - """ - - percentComplete = _messages.FloatField(1, variant=_messages.Variant.FLOAT) - position = _messages.MessageField('Position', 2) - remainingTime = _messages.StringField(3) - - -class ApproximateReportedProgress(_messages.Message): - r"""A progress measurement of a WorkItem by a worker. - - Fields: - consumedParallelism: Total amount of parallelism in the portion of input - of this task that has already been consumed and is no longer active. In - the first two examples above (see remaining_parallelism), the value - should be 29 or 2 respectively. The sum of remaining_parallelism and - consumed_parallelism should equal the total amount of parallelism in - this work item. If specified, must be finite. - fractionConsumed: Completion as fraction of the input consumed, from 0.0 - (beginning, nothing consumed), to 1.0 (end of the input, entire input - consumed). - position: A Position within the work to represent a progress. - remainingParallelism: Total amount of parallelism in the input of this - task that remains, (i.e. can be delegated to this task and any new tasks - via dynamic splitting). Always at least 1 for non-finished work items - and 0 for finished. "Amount of parallelism" refers to how many non-empty - parts of the input can be read in parallel. This does not necessarily - equal number of records. An input that can be read in parallel down to - the individual records is called "perfectly splittable". An example of - non-perfectly parallelizable input is a block-compressed file format - where a block of records has to be read as a whole, but different blocks - can be read in parallel. Examples: * If we are processing record #30 - (starting at 1) out of 50 in a perfectly splittable 50-record input, - this value should be 21 (20 remaining + 1 current). * If we are reading - through block 3 in a block-compressed file consisting of 5 blocks, this - value should be 3 (since blocks 4 and 5 can be processed in parallel by - new tasks via dynamic splitting and the current task remains processing - block 3). * If we are reading through the last block in a block- - compressed file, or reading or processing the last record in a perfectly - splittable input, this value should be 1, because apart from the current - task, no additional remainder can be split off. - """ - - consumedParallelism = _messages.MessageField('ReportedParallelism', 1) - fractionConsumed = _messages.FloatField(2) - position = _messages.MessageField('Position', 3) - remainingParallelism = _messages.MessageField('ReportedParallelism', 4) - - -class ApproximateSplitRequest(_messages.Message): - r"""A suggestion by the service to the worker to dynamically split the - WorkItem. - - Fields: - fractionConsumed: A fraction at which to split the work item, from 0.0 - (beginning of the input) to 1.0 (end of the input). - fractionOfRemainder: The fraction of the remainder of work to split the - work item at, from 0.0 (split at the current position) to 1.0 (end of - the input). - position: A Position at which to split the work item. - """ - - fractionConsumed = _messages.FloatField(1) - fractionOfRemainder = _messages.FloatField(2) - position = _messages.MessageField('Position', 3) - - -class AutoscalingEvent(_messages.Message): - r"""A structured message reporting an autoscaling decision made by the - Dataflow service. - - Enums: - EventTypeValueValuesEnum: The type of autoscaling event to report. - - Fields: - currentNumWorkers: The current number of workers the job has. - description: A message describing why the system decided to adjust the - current number of workers, why it failed, or why the system decided to - not make any changes to the number of workers. - eventType: The type of autoscaling event to report. - targetNumWorkers: The target number of workers the worker pool wants to - resize to use. - time: The time this event was emitted to indicate a new target or current - num_workers value. - workerPool: A short and friendly name for the worker pool this event - refers to. - """ - - class EventTypeValueValuesEnum(_messages.Enum): - r"""The type of autoscaling event to report. - - Values: - TYPE_UNKNOWN: Default type for the enum. Value should never be returned. - TARGET_NUM_WORKERS_CHANGED: The TARGET_NUM_WORKERS_CHANGED type should - be used when the target worker pool size has changed at the start of - an actuation. An event should always be specified as - TARGET_NUM_WORKERS_CHANGED if it reflects a change in the - target_num_workers. - CURRENT_NUM_WORKERS_CHANGED: The CURRENT_NUM_WORKERS_CHANGED type should - be used when actual worker pool size has been changed, but the - target_num_workers has not changed. - ACTUATION_FAILURE: The ACTUATION_FAILURE type should be used when we - want to report an error to the user indicating why the current number - of workers in the pool could not be changed. Displayed in the current - status and history widgets. - NO_CHANGE: Used when we want to report to the user a reason why we are - not currently adjusting the number of workers. Should specify both - target_num_workers, current_num_workers and a decision_message. - """ - TYPE_UNKNOWN = 0 - TARGET_NUM_WORKERS_CHANGED = 1 - CURRENT_NUM_WORKERS_CHANGED = 2 - ACTUATION_FAILURE = 3 - NO_CHANGE = 4 - - currentNumWorkers = _messages.IntegerField(1) - description = _messages.MessageField('StructuredMessage', 2) - eventType = _messages.EnumField('EventTypeValueValuesEnum', 3) - targetNumWorkers = _messages.IntegerField(4) - time = _messages.StringField(5) - workerPool = _messages.StringField(6) - - -class AutoscalingSettings(_messages.Message): - r"""Settings for WorkerPool autoscaling. - - Enums: - AlgorithmValueValuesEnum: The algorithm to use for autoscaling. - - Fields: - algorithm: The algorithm to use for autoscaling. - maxNumWorkers: The maximum number of workers to cap scaling at. - """ - - class AlgorithmValueValuesEnum(_messages.Enum): - r"""The algorithm to use for autoscaling. - - Values: - AUTOSCALING_ALGORITHM_UNKNOWN: The algorithm is unknown, or unspecified. - AUTOSCALING_ALGORITHM_NONE: Disable autoscaling. - AUTOSCALING_ALGORITHM_BASIC: Increase worker count over time to reduce - job execution time. - """ - AUTOSCALING_ALGORITHM_UNKNOWN = 0 - AUTOSCALING_ALGORITHM_NONE = 1 - AUTOSCALING_ALGORITHM_BASIC = 2 - - algorithm = _messages.EnumField('AlgorithmValueValuesEnum', 1) - maxNumWorkers = _messages.IntegerField(2, variant=_messages.Variant.INT32) - - -class Base2Exponent(_messages.Message): - r"""Exponential buckets where the growth factor between buckets is - `2**(2**-scale)`. e.g. for `scale=1` growth factor is - `2**(2**(-1))=sqrt(2)`. `n` buckets will have the following boundaries. - - 0th: [0, gf) - i in [1, n-1]: [gf^(i), gf^(i+1)) - - Fields: - numberOfBuckets: Must be greater than 0. - scale: Must be between -3 and 3. This forces the growth factor of the - bucket boundaries to be between `2^(1/8)` and `256`. - """ - - numberOfBuckets = _messages.IntegerField(1, variant=_messages.Variant.INT32) - scale = _messages.IntegerField(2, variant=_messages.Variant.INT32) - - -class BigQueryIODetails(_messages.Message): - r"""Metadata for a BigQuery connector used by the job. - - Fields: - dataset: Dataset accessed in the connection. - projectId: Project accessed in the connection. - query: Query used to access data in the connection. - table: Table accessed in the connection. - """ - - dataset = _messages.StringField(1) - projectId = _messages.StringField(2) - query = _messages.StringField(3) - table = _messages.StringField(4) - - -class BigTableIODetails(_messages.Message): - r"""Metadata for a Cloud Bigtable connector used by the job. - - Fields: - instanceId: InstanceId accessed in the connection. - projectId: ProjectId accessed in the connection. - tableId: TableId accessed in the connection. - """ - - instanceId = _messages.StringField(1) - projectId = _messages.StringField(2) - tableId = _messages.StringField(3) - - -class BoundedTrie(_messages.Message): - r"""The message type used for encoding metrics of type bounded trie. - - Fields: - bound: The maximum number of elements to store before truncation. - root: A compact representation of all the elements in this trie. - singleton: A more efficient representation for metrics consisting of a - single value. - """ - - bound = _messages.IntegerField(1, variant=_messages.Variant.INT32) - root = _messages.MessageField('BoundedTrieNode', 2) - singleton = _messages.StringField(3, repeated=True) - - -class BoundedTrieNode(_messages.Message): - r"""A single node in a BoundedTrie. - - Messages: - ChildrenValue: Children of this node. Must be empty if truncated is true. - - Fields: - children: Children of this node. Must be empty if truncated is true. - truncated: Whether this node has been truncated. A truncated leaf - represents possibly many children with the same prefix. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class ChildrenValue(_messages.Message): - r"""Children of this node. Must be empty if truncated is true. - - Messages: - AdditionalProperty: An additional property for a ChildrenValue object. - - Fields: - additionalProperties: Additional properties of type ChildrenValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ChildrenValue object. - - Fields: - key: Name of the additional property. - value: A BoundedTrieNode attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('BoundedTrieNode', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - children = _messages.MessageField('ChildrenValue', 1) - truncated = _messages.BooleanField(2) - - -class BucketOptions(_messages.Message): - r"""`BucketOptions` describes the bucket boundaries used in the histogram. - - Fields: - exponential: Bucket boundaries grow exponentially. - linear: Bucket boundaries grow linearly. - """ - - exponential = _messages.MessageField('Base2Exponent', 1) - linear = _messages.MessageField('Linear', 2) - - -class CPUTime(_messages.Message): - r"""Modeled after information exposed by /proc/stat. - - Fields: - rate: Average CPU utilization rate (% non-idle cpu / second) since - previous sample. - timestamp: Timestamp of the measurement. - totalMs: Total active CPU time across all cores (ie., non-idle) in - milliseconds since start-up. - """ - - rate = _messages.FloatField(1) - timestamp = _messages.StringField(2) - totalMs = _messages.IntegerField(3, variant=_messages.Variant.UINT64) - - -class ComponentSource(_messages.Message): - r"""Description of an interstitial value between transforms in an execution - stage. - - Fields: - name: Dataflow service generated name for this source. - originalTransformOrCollection: User name for the original user transform - or collection with which this source is most closely associated. - userName: Human-readable name for this transform; may be user or system - generated. - """ - - name = _messages.StringField(1) - originalTransformOrCollection = _messages.StringField(2) - userName = _messages.StringField(3) - - -class ComponentTransform(_messages.Message): - r"""Description of a transform executed as part of an execution stage. - - Fields: - name: Dataflow service generated name for this source. - originalTransform: User name for the original user transform with which - this transform is most closely associated. - userName: Human-readable name for this transform; may be user or system - generated. - """ - - name = _messages.StringField(1) - originalTransform = _messages.StringField(2) - userName = _messages.StringField(3) - - -class ComputationTopology(_messages.Message): - r"""All configuration data for a particular Computation. - - Fields: - computationId: The ID of the computation. - inputs: The inputs to the computation. - keyRanges: The key ranges processed by the computation. - outputs: The outputs from the computation. - stateFamilies: The state family values. - systemStageName: The system stage name. - """ - - computationId = _messages.StringField(1) - inputs = _messages.MessageField('StreamLocation', 2, repeated=True) - keyRanges = _messages.MessageField('KeyRangeLocation', 3, repeated=True) - outputs = _messages.MessageField('StreamLocation', 4, repeated=True) - stateFamilies = _messages.MessageField('StateFamilyConfig', 5, repeated=True) - systemStageName = _messages.StringField(6) - - -class ConcatPosition(_messages.Message): - r"""A position that encapsulates an inner position and an index for the - inner position. A ConcatPosition can be used by a reader of a source that - encapsulates a set of other sources. - - Fields: - index: Index of the inner source. - position: Position within the inner source. - """ - - index = _messages.IntegerField(1, variant=_messages.Variant.INT32) - position = _messages.MessageField('Position', 2) - - -class ContainerSpec(_messages.Message): - r"""Container Spec. - - Fields: - defaultEnvironment: Default runtime environment for the job. - image: Name of the docker container image. E.g., gcr.io/project/some-image - imageRepositoryCertPath: Cloud Storage path to self-signed certificate of - private registry. - imageRepositoryPasswordSecretId: Secret Manager secret id for password to - authenticate to private registry. - imageRepositoryUsernameSecretId: Secret Manager secret id for username to - authenticate to private registry. - metadata: Metadata describing a template including description and - validation rules. - sdkInfo: Required. SDK info of the Flex Template. - """ - - defaultEnvironment = _messages.MessageField('FlexTemplateRuntimeEnvironment', 1) - image = _messages.StringField(2) - imageRepositoryCertPath = _messages.StringField(3) - imageRepositoryPasswordSecretId = _messages.StringField(4) - imageRepositoryUsernameSecretId = _messages.StringField(5) - metadata = _messages.MessageField('TemplateMetadata', 6) - sdkInfo = _messages.MessageField('SDKInfo', 7) - - -class CounterMetadata(_messages.Message): - r"""CounterMetadata includes all static non-name non-value counter - attributes. - - Enums: - KindValueValuesEnum: Counter aggregation kind. - StandardUnitsValueValuesEnum: System defined Units, see above enum. - - Fields: - description: Human-readable description of the counter semantics. - kind: Counter aggregation kind. - otherUnits: A string referring to the unit type. - standardUnits: System defined Units, see above enum. - """ - - class KindValueValuesEnum(_messages.Enum): - r"""Counter aggregation kind. - - Values: - INVALID: Counter aggregation kind was not set. - SUM: Aggregated value is the sum of all contributed values. - MAX: Aggregated value is the max of all contributed values. - MIN: Aggregated value is the min of all contributed values. - MEAN: Aggregated value is the mean of all contributed values. - OR: Aggregated value represents the logical 'or' of all contributed - values. - AND: Aggregated value represents the logical 'and' of all contributed - values. - SET: Aggregated value is a set of unique contributed values. - DISTRIBUTION: Aggregated value captures statistics about a distribution. - LATEST_VALUE: Aggregated value tracks the latest value of a variable. - """ - INVALID = 0 - SUM = 1 - MAX = 2 - MIN = 3 - MEAN = 4 - OR = 5 - AND = 6 - SET = 7 - DISTRIBUTION = 8 - LATEST_VALUE = 9 - - class StandardUnitsValueValuesEnum(_messages.Enum): - r"""System defined Units, see above enum. - - Values: - BYTES: Counter returns a value in bytes. - BYTES_PER_SEC: Counter returns a value in bytes per second. - MILLISECONDS: Counter returns a value in milliseconds. - MICROSECONDS: Counter returns a value in microseconds. - NANOSECONDS: Counter returns a value in nanoseconds. - TIMESTAMP_MSEC: Counter returns a timestamp in milliseconds. - TIMESTAMP_USEC: Counter returns a timestamp in microseconds. - TIMESTAMP_NSEC: Counter returns a timestamp in nanoseconds. - """ - BYTES = 0 - BYTES_PER_SEC = 1 - MILLISECONDS = 2 - MICROSECONDS = 3 - NANOSECONDS = 4 - TIMESTAMP_MSEC = 5 - TIMESTAMP_USEC = 6 - TIMESTAMP_NSEC = 7 - - description = _messages.StringField(1) - kind = _messages.EnumField('KindValueValuesEnum', 2) - otherUnits = _messages.StringField(3) - standardUnits = _messages.EnumField('StandardUnitsValueValuesEnum', 4) - - -class CounterStructuredName(_messages.Message): - r"""Identifies a counter within a per-job namespace. Counters whose - structured names are the same get merged into a single value for the job. - - Enums: - OriginValueValuesEnum: One of the standard Origins defined above. - PortionValueValuesEnum: Portion of this counter, either key or value. - - Fields: - componentStepName: Name of the optimized step being executed by the - workers. - executionStepName: Name of the stage. An execution step contains multiple - component steps. - inputIndex: Index of an input collection that's being read from/written to - as a side input. The index identifies a step's side inputs starting by 1 - (e.g. the first side input has input_index 1, the third has input_index - 3). Side inputs are identified by a pair of (original_step_name, - input_index). This field helps uniquely identify them. - name: Counter name. Not necessarily globally-unique, but unique within the - context of the other fields. Required. - origin: One of the standard Origins defined above. - originNamespace: A string containing a more specific namespace of the - counter's origin. - originalRequestingStepName: The step name requesting an operation, such as - GBK. I.e. the ParDo causing a read/write from shuffle to occur, or a - read from side inputs. - originalStepName: System generated name of the original step in the user's - graph, before optimization. - portion: Portion of this counter, either key or value. - workerId: ID of a particular worker. - """ - - class OriginValueValuesEnum(_messages.Enum): - r"""One of the standard Origins defined above. - - Values: - SYSTEM: Counter was created by the Dataflow system. - USER: Counter was created by the user. - """ - SYSTEM = 0 - USER = 1 - - class PortionValueValuesEnum(_messages.Enum): - r"""Portion of this counter, either key or value. - - Values: - ALL: Counter portion has not been set. - KEY: Counter reports a key. - VALUE: Counter reports a value. - """ - ALL = 0 - KEY = 1 - VALUE = 2 - - componentStepName = _messages.StringField(1) - executionStepName = _messages.StringField(2) - inputIndex = _messages.IntegerField(3, variant=_messages.Variant.INT32) - name = _messages.StringField(4) - origin = _messages.EnumField('OriginValueValuesEnum', 5) - originNamespace = _messages.StringField(6) - originalRequestingStepName = _messages.StringField(7) - originalStepName = _messages.StringField(8) - portion = _messages.EnumField('PortionValueValuesEnum', 9) - workerId = _messages.StringField(10) - - -class CounterStructuredNameAndMetadata(_messages.Message): - r"""A single message which encapsulates structured name and metadata for a - given counter. - - Fields: - metadata: Metadata associated with a counter - name: Structured name of the counter. - """ - - metadata = _messages.MessageField('CounterMetadata', 1) - name = _messages.MessageField('CounterStructuredName', 2) - - -class CounterUpdate(_messages.Message): - r"""An update to a Counter sent from a worker. Next ID: 17 - - Fields: - boolean: Boolean value for And, Or. - boundedTrie: Bounded trie data - cumulative: True if this counter is reported as the total cumulative - aggregate value accumulated since the worker started working on this - WorkItem. By default this is false, indicating that this counter is - reported as a delta. - distribution: Distribution data - floatingPoint: Floating point value for Sum, Max, Min. - floatingPointList: List of floating point numbers, for Set. - floatingPointMean: Floating point mean aggregation value for Mean. - integer: Integer value for Sum, Max, Min. - integerGauge: Gauge data - integerList: List of integers, for Set. - integerMean: Integer mean aggregation value for Mean. - internal: Value for internally-defined counters used by the Dataflow - service. - nameAndKind: Counter name and aggregation type. - shortId: The service-generated short identifier for this counter. The - short_id -> (name, metadata) mapping is constant for the lifetime of a - job. - stringList: List of strings, for Set. - structuredNameAndMetadata: Counter structured name and metadata. - """ - - boolean = _messages.BooleanField(1) - boundedTrie = _messages.MessageField('BoundedTrie', 2) - cumulative = _messages.BooleanField(3) - distribution = _messages.MessageField('DistributionUpdate', 4) - floatingPoint = _messages.FloatField(5) - floatingPointList = _messages.MessageField('FloatingPointList', 6) - floatingPointMean = _messages.MessageField('FloatingPointMean', 7) - integer = _messages.MessageField('SplitInt64', 8) - integerGauge = _messages.MessageField('IntegerGauge', 9) - integerList = _messages.MessageField('IntegerList', 10) - integerMean = _messages.MessageField('IntegerMean', 11) - internal = _messages.MessageField('extra_types.JsonValue', 12) - nameAndKind = _messages.MessageField('NameAndKind', 13) - shortId = _messages.IntegerField(14) - stringList = _messages.MessageField('StringList', 15) - structuredNameAndMetadata = _messages.MessageField('CounterStructuredNameAndMetadata', 16) - - -class CreateJobFromTemplateRequest(_messages.Message): - r"""A request to create a Cloud Dataflow job from a template. - - Messages: - ParametersValue: The runtime parameters to pass to the job. - - Fields: - environment: The runtime environment for the job. - gcsPath: Required. A Cloud Storage path to the template from which to - create the job. Must be a valid Cloud Storage URL, beginning with - `gs://`. - jobName: Required. The job name to use for the created job. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. - parameters: The runtime parameters to pass to the job. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class ParametersValue(_messages.Message): - r"""The runtime parameters to pass to the job. - - Messages: - AdditionalProperty: An additional property for a ParametersValue object. - - Fields: - additionalProperties: Additional properties of type ParametersValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ParametersValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - environment = _messages.MessageField('RuntimeEnvironment', 1) - gcsPath = _messages.StringField(2) - jobName = _messages.StringField(3) - location = _messages.StringField(4) - parameters = _messages.MessageField('ParametersValue', 5) - - -class CustomSourceLocation(_messages.Message): - r"""Identifies the location of a custom souce. - - Fields: - stateful: Whether this source is stateful. - """ - - stateful = _messages.BooleanField(1) - - -class DataDiskAssignment(_messages.Message): - r"""Data disk assignment for a given VM instance. - - Fields: - dataDisks: Mounted data disks. The order is important a data disk's - 0-based index in this list defines which persistent directory the disk - is mounted to, for example the list of { - "myproject-1014-104817-4c2-harness-0-disk-0" }, { - "myproject-1014-104817-4c2-harness-0-disk-1" }. - vmInstance: VM instance name the data disks mounted to, for example - "myproject-1014-104817-4c2-harness-0". - """ - - dataDisks = _messages.StringField(1, repeated=True) - vmInstance = _messages.StringField(2) - - -class DataSamplingConfig(_messages.Message): - r"""Configuration options for sampling elements. - - Enums: - BehaviorsValueListEntryValuesEnum: - - Fields: - behaviors: List of given sampling behaviors to enable. For example, - specifying behaviors = [ALWAYS_ON] samples in-flight elements but does - not sample exceptions. Can be used to specify multiple behaviors like, - behaviors = [ALWAYS_ON, EXCEPTIONS] for specifying periodic sampling and - exception sampling. If DISABLED is in the list, then sampling will be - disabled and ignore the other given behaviors. Ordering does not matter. - """ - - class BehaviorsValueListEntryValuesEnum(_messages.Enum): - r"""BehaviorsValueListEntryValuesEnum enum type. - - Values: - DATA_SAMPLING_BEHAVIOR_UNSPECIFIED: If given, has no effect on sampling - behavior. Used as an unknown or unset sentinel value. - DISABLED: When given, disables element sampling. Has same behavior as - not setting the behavior. - ALWAYS_ON: When given, enables sampling in-flight from all PCollections. - EXCEPTIONS: When given, enables sampling input elements when a user- - defined DoFn causes an exception. - """ - DATA_SAMPLING_BEHAVIOR_UNSPECIFIED = 0 - DISABLED = 1 - ALWAYS_ON = 2 - EXCEPTIONS = 3 - - behaviors = _messages.EnumField('BehaviorsValueListEntryValuesEnum', 1, repeated=True) - - -class DataSamplingReport(_messages.Message): - r"""Contains per-worker telemetry about the data sampling feature. - - Fields: - bytesWrittenDelta: Optional. Delta of bytes written to file from previous - report. - elementsSampledBytes: Optional. Delta of bytes sampled from previous - report. - elementsSampledCount: Optional. Delta of number of elements sampled from - previous report. - exceptionsSampledCount: Optional. Delta of number of samples taken from - user code exceptions from previous report. - pcollectionsSampledCount: Optional. Delta of number of PCollections - sampled from previous report. - persistenceErrorsCount: Optional. Delta of errors counts from persisting - the samples from previous report. - translationErrorsCount: Optional. Delta of errors counts from retrieving, - or translating the samples from previous report. - """ - - bytesWrittenDelta = _messages.IntegerField(1) - elementsSampledBytes = _messages.IntegerField(2) - elementsSampledCount = _messages.IntegerField(3) - exceptionsSampledCount = _messages.IntegerField(4) - pcollectionsSampledCount = _messages.IntegerField(5) - persistenceErrorsCount = _messages.IntegerField(6) - translationErrorsCount = _messages.IntegerField(7) - - -class DataflowGaugeValue(_messages.Message): - r"""The gauge value of a metric. - - Fields: - measuredTime: The timestamp when the gauge was recorded. - value: The value of the gauge. - """ - - measuredTime = _messages.StringField(1) - value = _messages.IntegerField(2) - - -class DataflowHistogramValue(_messages.Message): - r"""Summary statistics for a population of values. HistogramValue contains a - sequence of buckets and gives a count of values that fall into each bucket. - Bucket boundares are defined by a formula and bucket widths are either fixed - or exponentially increasing. - - Fields: - bucketCounts: Optional. The number of values in each bucket of the - histogram, as described in `bucket_options`. `bucket_counts` should - contain N values, where N is the number of buckets specified in - `bucket_options`. If `bucket_counts` has fewer than N values, the - remaining values are assumed to be 0. - bucketOptions: Describes the bucket boundaries used in the histogram. - count: Number of values recorded in this histogram. - outlierStats: Statistics on the values recorded in the histogram that fall - out of the bucket boundaries. - """ - - bucketCounts = _messages.IntegerField(1, repeated=True) - bucketOptions = _messages.MessageField('BucketOptions', 2) - count = _messages.IntegerField(3) - outlierStats = _messages.MessageField('OutlierStats', 4) - - -class DataflowProjectsDeleteSnapshotsRequest(_messages.Message): - r"""A DataflowProjectsDeleteSnapshotsRequest object. - - Fields: - location: The location that contains this snapshot. - projectId: The ID of the Cloud Platform project that the snapshot belongs - to. - snapshotId: The ID of the snapshot. - """ - - location = _messages.StringField(1) - projectId = _messages.StringField(2, required=True) - snapshotId = _messages.StringField(3) - - -class DataflowProjectsJobsAggregatedRequest(_messages.Message): - r"""A DataflowProjectsJobsAggregatedRequest object. - - Enums: - FilterValueValuesEnum: The kind of filter to use. - ViewValueValuesEnum: Deprecated. ListJobs always returns summaries now. - Use GetJob for other JobViews. - - Fields: - filter: The kind of filter to use. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - name: Optional. The job name. - pageSize: If there are many jobs, limit response to at most this many. The - actual number of jobs returned will be the lesser of max_responses and - an unspecified server-defined limit. - pageToken: Set this to the 'next_page_token' field of a previous response - to request additional results in a long list. - projectId: The project which owns the jobs. - view: Deprecated. ListJobs always returns summaries now. Use GetJob for - other JobViews. - """ - - class FilterValueValuesEnum(_messages.Enum): - r"""The kind of filter to use. - - Values: - UNKNOWN: The filter isn't specified, or is unknown. This returns all - jobs ordered on descending `JobUuid`. - ALL: Returns all running jobs first ordered on creation timestamp, then - returns all terminated jobs ordered on the termination timestamp. - TERMINATED: Filters the jobs that have a terminated state, ordered on - the termination timestamp. Example terminated states: - `JOB_STATE_STOPPED`, `JOB_STATE_UPDATED`, `JOB_STATE_DRAINED`, etc. - ACTIVE: Filters the jobs that are running ordered on the creation - timestamp. - """ - UNKNOWN = 0 - ALL = 1 - TERMINATED = 2 - ACTIVE = 3 - - class ViewValueValuesEnum(_messages.Enum): - r"""Deprecated. ListJobs always returns summaries now. Use GetJob for - other JobViews. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - filter = _messages.EnumField('FilterValueValuesEnum', 1) - location = _messages.StringField(2) - name = _messages.StringField(3) - pageSize = _messages.IntegerField(4, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(5) - projectId = _messages.StringField(6, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 7) - - -class DataflowProjectsJobsCreateRequest(_messages.Message): - r"""A DataflowProjectsJobsCreateRequest object. - - Enums: - ViewValueValuesEnum: The level of information requested in response. - - Fields: - job: A Job resource to be passed as the request body. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - projectId: The ID of the Cloud Platform project that the job belongs to. - replaceJobId: Deprecated. This field is now in the Job message. - view: The level of information requested in response. - """ - - class ViewValueValuesEnum(_messages.Enum): - r"""The level of information requested in response. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - job = _messages.MessageField('Job', 1) - location = _messages.StringField(2) - projectId = _messages.StringField(3, required=True) - replaceJobId = _messages.StringField(4) - view = _messages.EnumField('ViewValueValuesEnum', 5) - - -class DataflowProjectsJobsDebugGetConfigRequest(_messages.Message): - r"""A DataflowProjectsJobsDebugGetConfigRequest object. - - Fields: - getDebugConfigRequest: A GetDebugConfigRequest resource to be passed as - the request body. - jobId: The job id. - projectId: The project id. - """ - - getDebugConfigRequest = _messages.MessageField('GetDebugConfigRequest', 1) - jobId = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsJobsDebugSendCaptureRequest(_messages.Message): - r"""A DataflowProjectsJobsDebugSendCaptureRequest object. - - Fields: - jobId: The job id. - projectId: The project id. - sendDebugCaptureRequest: A SendDebugCaptureRequest resource to be passed - as the request body. - """ - - jobId = _messages.StringField(1, required=True) - projectId = _messages.StringField(2, required=True) - sendDebugCaptureRequest = _messages.MessageField('SendDebugCaptureRequest', 3) - - -class DataflowProjectsJobsGetMetricsRequest(_messages.Message): - r"""A DataflowProjectsJobsGetMetricsRequest object. - - Fields: - jobId: The job to get metrics for. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - projectId: A project id. - startTime: Return only metric data that has changed since this time. - Default is to return all information about all metrics for the job. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2) - projectId = _messages.StringField(3, required=True) - startTime = _messages.StringField(4) - - -class DataflowProjectsJobsGetRequest(_messages.Message): - r"""A DataflowProjectsJobsGetRequest object. - - Enums: - ViewValueValuesEnum: The level of information requested in response. - - Fields: - jobId: The job ID. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - projectId: The ID of the Cloud Platform project that the job belongs to. - view: The level of information requested in response. - """ - - class ViewValueValuesEnum(_messages.Enum): - r"""The level of information requested in response. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2) - projectId = _messages.StringField(3, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 4) - - -class DataflowProjectsJobsListRequest(_messages.Message): - r"""A DataflowProjectsJobsListRequest object. - - Enums: - FilterValueValuesEnum: The kind of filter to use. - ViewValueValuesEnum: Deprecated. ListJobs always returns summaries now. - Use GetJob for other JobViews. - - Fields: - filter: The kind of filter to use. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - name: Optional. The job name. - pageSize: If there are many jobs, limit response to at most this many. The - actual number of jobs returned will be the lesser of max_responses and - an unspecified server-defined limit. - pageToken: Set this to the 'next_page_token' field of a previous response - to request additional results in a long list. - projectId: The project which owns the jobs. - view: Deprecated. ListJobs always returns summaries now. Use GetJob for - other JobViews. - """ - - class FilterValueValuesEnum(_messages.Enum): - r"""The kind of filter to use. - - Values: - UNKNOWN: The filter isn't specified, or is unknown. This returns all - jobs ordered on descending `JobUuid`. - ALL: Returns all running jobs first ordered on creation timestamp, then - returns all terminated jobs ordered on the termination timestamp. - TERMINATED: Filters the jobs that have a terminated state, ordered on - the termination timestamp. Example terminated states: - `JOB_STATE_STOPPED`, `JOB_STATE_UPDATED`, `JOB_STATE_DRAINED`, etc. - ACTIVE: Filters the jobs that are running ordered on the creation - timestamp. - """ - UNKNOWN = 0 - ALL = 1 - TERMINATED = 2 - ACTIVE = 3 - - class ViewValueValuesEnum(_messages.Enum): - r"""Deprecated. ListJobs always returns summaries now. Use GetJob for - other JobViews. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - filter = _messages.EnumField('FilterValueValuesEnum', 1) - location = _messages.StringField(2) - name = _messages.StringField(3) - pageSize = _messages.IntegerField(4, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(5) - projectId = _messages.StringField(6, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 7) - - -class DataflowProjectsJobsMessagesListRequest(_messages.Message): - r"""A DataflowProjectsJobsMessagesListRequest object. - - Enums: - MinimumImportanceValueValuesEnum: Filter to only get messages with - importance >= level - - Fields: - endTime: Return only messages with timestamps < end_time. The default is - now (i.e. return up to the latest messages available). - jobId: The job to get messages about. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - minimumImportance: Filter to only get messages with importance >= level - pageSize: If specified, determines the maximum number of messages to - return. If unspecified, the service may choose an appropriate default, - or may return an arbitrarily large number of results. - pageToken: If supplied, this should be the value of next_page_token - returned by an earlier call. This will cause the next page of results to - be returned. - projectId: A project id. - startTime: If specified, return only messages with timestamps >= - start_time. The default is the job creation time (i.e. beginning of - messages). - """ - - class MinimumImportanceValueValuesEnum(_messages.Enum): - r"""Filter to only get messages with importance >= level - - Values: - JOB_MESSAGE_IMPORTANCE_UNKNOWN: The message importance isn't specified, - or is unknown. - JOB_MESSAGE_DEBUG: The message is at the 'debug' level: typically only - useful for software engineers working on the code the job is running. - Typically, Dataflow pipeline runners do not display log messages at - this level by default. - JOB_MESSAGE_DETAILED: The message is at the 'detailed' level: somewhat - verbose, but potentially useful to users. Typically, Dataflow pipeline - runners do not display log messages at this level by default. These - messages are displayed by default in the Dataflow monitoring UI. - JOB_MESSAGE_BASIC: The message is at the 'basic' level: useful for - keeping track of the execution of a Dataflow pipeline. Typically, - Dataflow pipeline runners display log messages at this level by - default, and these messages are displayed by default in the Dataflow - monitoring UI. - JOB_MESSAGE_WARNING: The message is at the 'warning' level: indicating a - condition pertaining to a job which may require human intervention. - Typically, Dataflow pipeline runners display log messages at this - level by default, and these messages are displayed by default in the - Dataflow monitoring UI. - JOB_MESSAGE_ERROR: The message is at the 'error' level: indicating a - condition preventing a job from succeeding. Typically, Dataflow - pipeline runners display log messages at this level by default, and - these messages are displayed by default in the Dataflow monitoring UI. - """ - JOB_MESSAGE_IMPORTANCE_UNKNOWN = 0 - JOB_MESSAGE_DEBUG = 1 - JOB_MESSAGE_DETAILED = 2 - JOB_MESSAGE_BASIC = 3 - JOB_MESSAGE_WARNING = 4 - JOB_MESSAGE_ERROR = 5 - - endTime = _messages.StringField(1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3) - minimumImportance = _messages.EnumField('MinimumImportanceValueValuesEnum', 4) - pageSize = _messages.IntegerField(5, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(6) - projectId = _messages.StringField(7, required=True) - startTime = _messages.StringField(8) - - -class DataflowProjectsJobsSnapshotRequest(_messages.Message): - r"""A DataflowProjectsJobsSnapshotRequest object. - - Fields: - jobId: The job to be snapshotted. - projectId: The project which owns the job to be snapshotted. - snapshotJobRequest: A SnapshotJobRequest resource to be passed as the - request body. - """ - - jobId = _messages.StringField(1, required=True) - projectId = _messages.StringField(2, required=True) - snapshotJobRequest = _messages.MessageField('SnapshotJobRequest', 3) - - -class DataflowProjectsJobsUpdateRequest(_messages.Message): - r"""A DataflowProjectsJobsUpdateRequest object. - - Fields: - job: A Job resource to be passed as the request body. - jobId: The job ID. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - projectId: The ID of the Cloud Platform project that the job belongs to. - updateMask: The list of fields to update relative to Job. If empty, only - RequestedJobState will be considered for update. If the FieldMask is not - empty and RequestedJobState is none/empty, The fields specified in the - update mask will be the only ones considered for update. If both - RequestedJobState and update_mask are specified, an error will be - returned as we cannot update both state and mask. - """ - - job = _messages.MessageField('Job', 1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3) - projectId = _messages.StringField(4, required=True) - updateMask = _messages.StringField(5) - - -class DataflowProjectsJobsWorkItemsLeaseRequest(_messages.Message): - r"""A DataflowProjectsJobsWorkItemsLeaseRequest object. - - Fields: - jobId: Identifies the workflow job this worker belongs to. - leaseWorkItemRequest: A LeaseWorkItemRequest resource to be passed as the - request body. - projectId: Identifies the project this worker belongs to. - """ - - jobId = _messages.StringField(1, required=True) - leaseWorkItemRequest = _messages.MessageField('LeaseWorkItemRequest', 2) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsJobsWorkItemsReportStatusRequest(_messages.Message): - r"""A DataflowProjectsJobsWorkItemsReportStatusRequest object. - - Fields: - jobId: The job which the WorkItem is part of. - projectId: The project which owns the WorkItem's job. - reportWorkItemStatusRequest: A ReportWorkItemStatusRequest resource to be - passed as the request body. - """ - - jobId = _messages.StringField(1, required=True) - projectId = _messages.StringField(2, required=True) - reportWorkItemStatusRequest = _messages.MessageField('ReportWorkItemStatusRequest', 3) - - -class DataflowProjectsLocationsFlexTemplatesLaunchRequest(_messages.Message): - r"""A DataflowProjectsLocationsFlexTemplatesLaunchRequest object. - - Fields: - launchFlexTemplateRequest: A LaunchFlexTemplateRequest resource to be - passed as the request body. - location: Required. The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. E.g., us-central1, us-west1. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - """ - - launchFlexTemplateRequest = _messages.MessageField('LaunchFlexTemplateRequest', 1) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsLocationsJobsCreateRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsCreateRequest object. - - Enums: - ViewValueValuesEnum: The level of information requested in response. - - Fields: - job: A Job resource to be passed as the request body. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - projectId: The ID of the Cloud Platform project that the job belongs to. - replaceJobId: Deprecated. This field is now in the Job message. - view: The level of information requested in response. - """ - - class ViewValueValuesEnum(_messages.Enum): - r"""The level of information requested in response. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - job = _messages.MessageField('Job', 1) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - replaceJobId = _messages.StringField(4) - view = _messages.EnumField('ViewValueValuesEnum', 5) - - -class DataflowProjectsLocationsJobsDebugGetConfigRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsDebugGetConfigRequest object. - - Fields: - getDebugConfigRequest: A GetDebugConfigRequest resource to be passed as - the request body. - jobId: The job id. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - projectId: The project id. - """ - - getDebugConfigRequest = _messages.MessageField('GetDebugConfigRequest', 1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3, required=True) - projectId = _messages.StringField(4, required=True) - - -class DataflowProjectsLocationsJobsDebugGetWorkerStacktracesRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsDebugGetWorkerStacktracesRequest object. - - Fields: - getWorkerStacktracesRequest: A GetWorkerStacktracesRequest resource to be - passed as the request body. - jobId: The job for which to get stacktraces. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - projectId: The project id. - """ - - getWorkerStacktracesRequest = _messages.MessageField('GetWorkerStacktracesRequest', 1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3, required=True) - projectId = _messages.StringField(4, required=True) - - -class DataflowProjectsLocationsJobsDebugSendCaptureRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsDebugSendCaptureRequest object. - - Fields: - jobId: The job id. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - projectId: The project id. - sendDebugCaptureRequest: A SendDebugCaptureRequest resource to be passed - as the request body. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - sendDebugCaptureRequest = _messages.MessageField('SendDebugCaptureRequest', 4) - - -class DataflowProjectsLocationsJobsGetExecutionDetailsRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsGetExecutionDetailsRequest object. - - Fields: - jobId: The job to get execution details for. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - pageSize: If specified, determines the maximum number of stages to return. - If unspecified, the service may choose an appropriate default, or may - return an arbitrarily large number of results. - pageToken: If supplied, this should be the value of next_page_token - returned by an earlier call. This will cause the next page of results to - be returned. - projectId: A project id. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - pageSize = _messages.IntegerField(3, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(4) - projectId = _messages.StringField(5, required=True) - - -class DataflowProjectsLocationsJobsGetMetricsRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsGetMetricsRequest object. - - Fields: - jobId: The job to get metrics for. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - projectId: A project id. - startTime: Return only metric data that has changed since this time. - Default is to return all information about all metrics for the job. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - startTime = _messages.StringField(4) - - -class DataflowProjectsLocationsJobsGetRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsGetRequest object. - - Enums: - ViewValueValuesEnum: The level of information requested in response. - - Fields: - jobId: The job ID. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - projectId: The ID of the Cloud Platform project that the job belongs to. - view: The level of information requested in response. - """ - - class ViewValueValuesEnum(_messages.Enum): - r"""The level of information requested in response. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 4) - - -class DataflowProjectsLocationsJobsListRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsListRequest object. - - Enums: - FilterValueValuesEnum: The kind of filter to use. - ViewValueValuesEnum: Deprecated. ListJobs always returns summaries now. - Use GetJob for other JobViews. - - Fields: - filter: The kind of filter to use. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - name: Optional. The job name. - pageSize: If there are many jobs, limit response to at most this many. The - actual number of jobs returned will be the lesser of max_responses and - an unspecified server-defined limit. - pageToken: Set this to the 'next_page_token' field of a previous response - to request additional results in a long list. - projectId: The project which owns the jobs. - view: Deprecated. ListJobs always returns summaries now. Use GetJob for - other JobViews. - """ - - class FilterValueValuesEnum(_messages.Enum): - r"""The kind of filter to use. - - Values: - UNKNOWN: The filter isn't specified, or is unknown. This returns all - jobs ordered on descending `JobUuid`. - ALL: Returns all running jobs first ordered on creation timestamp, then - returns all terminated jobs ordered on the termination timestamp. - TERMINATED: Filters the jobs that have a terminated state, ordered on - the termination timestamp. Example terminated states: - `JOB_STATE_STOPPED`, `JOB_STATE_UPDATED`, `JOB_STATE_DRAINED`, etc. - ACTIVE: Filters the jobs that are running ordered on the creation - timestamp. - """ - UNKNOWN = 0 - ALL = 1 - TERMINATED = 2 - ACTIVE = 3 - - class ViewValueValuesEnum(_messages.Enum): - r"""Deprecated. ListJobs always returns summaries now. Use GetJob for - other JobViews. - - Values: - JOB_VIEW_UNKNOWN: The job view to return isn't specified, or is unknown. - Responses will contain at least the `JOB_VIEW_SUMMARY` information, - and may contain additional information. - JOB_VIEW_SUMMARY: Request summary information only: Project ID, Job ID, - job name, job type, job status, start/end time, and Cloud SDK version - details. - JOB_VIEW_ALL: Request all information available for this job. When the - job is in `JOB_STATE_PENDING`, the job has been created but is not yet - running, and not all job information is available. For complete job - information, wait until the job in is `JOB_STATE_RUNNING`. For more - information, see [JobState](https://cloud.google.com/dataflow/docs/ref - erence/rest/v1b3/projects.jobs#jobstate). - JOB_VIEW_DESCRIPTION: Request summary info and limited job description - data for steps, labels and environment. - """ - JOB_VIEW_UNKNOWN = 0 - JOB_VIEW_SUMMARY = 1 - JOB_VIEW_ALL = 2 - JOB_VIEW_DESCRIPTION = 3 - - filter = _messages.EnumField('FilterValueValuesEnum', 1) - location = _messages.StringField(2, required=True) - name = _messages.StringField(3) - pageSize = _messages.IntegerField(4, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(5) - projectId = _messages.StringField(6, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 7) - - -class DataflowProjectsLocationsJobsMessagesListRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsMessagesListRequest object. - - Enums: - MinimumImportanceValueValuesEnum: Filter to only get messages with - importance >= level - - Fields: - endTime: Return only messages with timestamps < end_time. The default is - now (i.e. return up to the latest messages available). - jobId: The job to get messages about. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - minimumImportance: Filter to only get messages with importance >= level - pageSize: If specified, determines the maximum number of messages to - return. If unspecified, the service may choose an appropriate default, - or may return an arbitrarily large number of results. - pageToken: If supplied, this should be the value of next_page_token - returned by an earlier call. This will cause the next page of results to - be returned. - projectId: A project id. - startTime: If specified, return only messages with timestamps >= - start_time. The default is the job creation time (i.e. beginning of - messages). - """ - - class MinimumImportanceValueValuesEnum(_messages.Enum): - r"""Filter to only get messages with importance >= level - - Values: - JOB_MESSAGE_IMPORTANCE_UNKNOWN: The message importance isn't specified, - or is unknown. - JOB_MESSAGE_DEBUG: The message is at the 'debug' level: typically only - useful for software engineers working on the code the job is running. - Typically, Dataflow pipeline runners do not display log messages at - this level by default. - JOB_MESSAGE_DETAILED: The message is at the 'detailed' level: somewhat - verbose, but potentially useful to users. Typically, Dataflow pipeline - runners do not display log messages at this level by default. These - messages are displayed by default in the Dataflow monitoring UI. - JOB_MESSAGE_BASIC: The message is at the 'basic' level: useful for - keeping track of the execution of a Dataflow pipeline. Typically, - Dataflow pipeline runners display log messages at this level by - default, and these messages are displayed by default in the Dataflow - monitoring UI. - JOB_MESSAGE_WARNING: The message is at the 'warning' level: indicating a - condition pertaining to a job which may require human intervention. - Typically, Dataflow pipeline runners display log messages at this - level by default, and these messages are displayed by default in the - Dataflow monitoring UI. - JOB_MESSAGE_ERROR: The message is at the 'error' level: indicating a - condition preventing a job from succeeding. Typically, Dataflow - pipeline runners display log messages at this level by default, and - these messages are displayed by default in the Dataflow monitoring UI. - """ - JOB_MESSAGE_IMPORTANCE_UNKNOWN = 0 - JOB_MESSAGE_DEBUG = 1 - JOB_MESSAGE_DETAILED = 2 - JOB_MESSAGE_BASIC = 3 - JOB_MESSAGE_WARNING = 4 - JOB_MESSAGE_ERROR = 5 - - endTime = _messages.StringField(1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3, required=True) - minimumImportance = _messages.EnumField('MinimumImportanceValueValuesEnum', 4) - pageSize = _messages.IntegerField(5, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(6) - projectId = _messages.StringField(7, required=True) - startTime = _messages.StringField(8) - - -class DataflowProjectsLocationsJobsSnapshotRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsSnapshotRequest object. - - Fields: - jobId: The job to be snapshotted. - location: The location that contains this job. - projectId: The project which owns the job to be snapshotted. - snapshotJobRequest: A SnapshotJobRequest resource to be passed as the - request body. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - snapshotJobRequest = _messages.MessageField('SnapshotJobRequest', 4) - - -class DataflowProjectsLocationsJobsSnapshotsListRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsSnapshotsListRequest object. - - Fields: - jobId: If specified, list snapshots created from this job. - location: The location to list snapshots in. - projectId: The project ID to list snapshots for. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsLocationsJobsStagesGetExecutionDetailsRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsStagesGetExecutionDetailsRequest object. - - Fields: - endTime: Upper time bound of work items to include, by start time. - jobId: The job to get execution details for. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - pageSize: If specified, determines the maximum number of work items to - return. If unspecified, the service may choose an appropriate default, - or may return an arbitrarily large number of results. - pageToken: If supplied, this should be the value of next_page_token - returned by an earlier call. This will cause the next page of results to - be returned. - projectId: A project id. - stageId: The stage for which to fetch information. - startTime: Lower time bound of work items to include, by start time. - """ - - endTime = _messages.StringField(1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3, required=True) - pageSize = _messages.IntegerField(4, variant=_messages.Variant.INT32) - pageToken = _messages.StringField(5) - projectId = _messages.StringField(6, required=True) - stageId = _messages.StringField(7, required=True) - startTime = _messages.StringField(8) - - -class DataflowProjectsLocationsJobsUpdateRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsUpdateRequest object. - - Fields: - job: A Job resource to be passed as the request body. - jobId: The job ID. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - projectId: The ID of the Cloud Platform project that the job belongs to. - updateMask: The list of fields to update relative to Job. If empty, only - RequestedJobState will be considered for update. If the FieldMask is not - empty and RequestedJobState is none/empty, The fields specified in the - update mask will be the only ones considered for update. If both - RequestedJobState and update_mask are specified, an error will be - returned as we cannot update both state and mask. - """ - - job = _messages.MessageField('Job', 1) - jobId = _messages.StringField(2, required=True) - location = _messages.StringField(3, required=True) - projectId = _messages.StringField(4, required=True) - updateMask = _messages.StringField(5) - - -class DataflowProjectsLocationsJobsWorkItemsLeaseRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsWorkItemsLeaseRequest object. - - Fields: - jobId: Identifies the workflow job this worker belongs to. - leaseWorkItemRequest: A LeaseWorkItemRequest resource to be passed as the - request body. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the WorkItem's job. - projectId: Identifies the project this worker belongs to. - """ - - jobId = _messages.StringField(1, required=True) - leaseWorkItemRequest = _messages.MessageField('LeaseWorkItemRequest', 2) - location = _messages.StringField(3, required=True) - projectId = _messages.StringField(4, required=True) - - -class DataflowProjectsLocationsJobsWorkItemsReportStatusRequest(_messages.Message): - r"""A DataflowProjectsLocationsJobsWorkItemsReportStatusRequest object. - - Fields: - jobId: The job which the WorkItem is part of. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the WorkItem's job. - projectId: The project which owns the WorkItem's job. - reportWorkItemStatusRequest: A ReportWorkItemStatusRequest resource to be - passed as the request body. - """ - - jobId = _messages.StringField(1, required=True) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - reportWorkItemStatusRequest = _messages.MessageField('ReportWorkItemStatusRequest', 4) - - -class DataflowProjectsLocationsSnapshotsDeleteRequest(_messages.Message): - r"""A DataflowProjectsLocationsSnapshotsDeleteRequest object. - - Fields: - location: The location that contains this snapshot. - projectId: The ID of the Cloud Platform project that the snapshot belongs - to. - snapshotId: The ID of the snapshot. - """ - - location = _messages.StringField(1, required=True) - projectId = _messages.StringField(2, required=True) - snapshotId = _messages.StringField(3, required=True) - - -class DataflowProjectsLocationsSnapshotsGetRequest(_messages.Message): - r"""A DataflowProjectsLocationsSnapshotsGetRequest object. - - Fields: - location: The location that contains this snapshot. - projectId: The ID of the Cloud Platform project that the snapshot belongs - to. - snapshotId: The ID of the snapshot. - """ - - location = _messages.StringField(1, required=True) - projectId = _messages.StringField(2, required=True) - snapshotId = _messages.StringField(3, required=True) - - -class DataflowProjectsLocationsSnapshotsListRequest(_messages.Message): - r"""A DataflowProjectsLocationsSnapshotsListRequest object. - - Fields: - jobId: If specified, list snapshots created from this job. - location: The location to list snapshots in. - projectId: The project ID to list snapshots for. - """ - - jobId = _messages.StringField(1) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsLocationsTemplatesCreateRequest(_messages.Message): - r"""A DataflowProjectsLocationsTemplatesCreateRequest object. - - Fields: - createJobFromTemplateRequest: A CreateJobFromTemplateRequest resource to - be passed as the request body. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - """ - - createJobFromTemplateRequest = _messages.MessageField('CreateJobFromTemplateRequest', 1) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsLocationsTemplatesGetRequest(_messages.Message): - r"""A DataflowProjectsLocationsTemplatesGetRequest object. - - Enums: - ViewValueValuesEnum: The view to retrieve. Defaults to METADATA_ONLY. - - Fields: - gcsPath: Required. A Cloud Storage path to the template from which to - create the job. Must be valid Cloud Storage URL, beginning with 'gs://'. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - view: The view to retrieve. Defaults to METADATA_ONLY. - """ - - class ViewValueValuesEnum(_messages.Enum): - r"""The view to retrieve. Defaults to METADATA_ONLY. - - Values: - METADATA_ONLY: Template view that retrieves only the metadata associated - with the template. - """ - METADATA_ONLY = 0 - - gcsPath = _messages.StringField(1) - location = _messages.StringField(2, required=True) - projectId = _messages.StringField(3, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 4) - - -class DataflowProjectsLocationsTemplatesLaunchRequest(_messages.Message): - r"""A DataflowProjectsLocationsTemplatesLaunchRequest object. - - Fields: - dynamicTemplate_gcsPath: Path to the dynamic template specification file - on Cloud Storage. The file must be a JSON serialized - `DynamicTemplateFileSpec` object. - dynamicTemplate_stagingLocation: Cloud Storage path for staging - dependencies. Must be a valid Cloud Storage URL, beginning with `gs://`. - gcsPath: A Cloud Storage path to the template to use to create the job. - Must be valid Cloud Storage URL, beginning with `gs://`. - launchTemplateParameters: A LaunchTemplateParameters resource to be passed - as the request body. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - validateOnly: If true, the request is validated but not actually executed. - Defaults to false. - """ - - dynamicTemplate_gcsPath = _messages.StringField(1) - dynamicTemplate_stagingLocation = _messages.StringField(2) - gcsPath = _messages.StringField(3) - launchTemplateParameters = _messages.MessageField('LaunchTemplateParameters', 4) - location = _messages.StringField(5, required=True) - projectId = _messages.StringField(6, required=True) - validateOnly = _messages.BooleanField(7) - - -class DataflowProjectsLocationsWorkerMessagesRequest(_messages.Message): - r"""A DataflowProjectsLocationsWorkerMessagesRequest object. - - Fields: - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job. - projectId: The project to send the WorkerMessages to. - sendWorkerMessagesRequest: A SendWorkerMessagesRequest resource to be - passed as the request body. - """ - - location = _messages.StringField(1, required=True) - projectId = _messages.StringField(2, required=True) - sendWorkerMessagesRequest = _messages.MessageField('SendWorkerMessagesRequest', 3) - - -class DataflowProjectsSnapshotsGetRequest(_messages.Message): - r"""A DataflowProjectsSnapshotsGetRequest object. - - Fields: - location: The location that contains this snapshot. - projectId: The ID of the Cloud Platform project that the snapshot belongs - to. - snapshotId: The ID of the snapshot. - """ - - location = _messages.StringField(1) - projectId = _messages.StringField(2, required=True) - snapshotId = _messages.StringField(3, required=True) - - -class DataflowProjectsSnapshotsListRequest(_messages.Message): - r"""A DataflowProjectsSnapshotsListRequest object. - - Fields: - jobId: If specified, list snapshots created from this job. - location: The location to list snapshots in. - projectId: The project ID to list snapshots for. - """ - - jobId = _messages.StringField(1) - location = _messages.StringField(2) - projectId = _messages.StringField(3, required=True) - - -class DataflowProjectsTemplatesCreateRequest(_messages.Message): - r"""A DataflowProjectsTemplatesCreateRequest object. - - Fields: - createJobFromTemplateRequest: A CreateJobFromTemplateRequest resource to - be passed as the request body. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - """ - - createJobFromTemplateRequest = _messages.MessageField('CreateJobFromTemplateRequest', 1) - projectId = _messages.StringField(2, required=True) - - -class DataflowProjectsTemplatesGetRequest(_messages.Message): - r"""A DataflowProjectsTemplatesGetRequest object. - - Enums: - ViewValueValuesEnum: The view to retrieve. Defaults to METADATA_ONLY. - - Fields: - gcsPath: Required. A Cloud Storage path to the template from which to - create the job. Must be valid Cloud Storage URL, beginning with 'gs://'. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - view: The view to retrieve. Defaults to METADATA_ONLY. - """ - - class ViewValueValuesEnum(_messages.Enum): - r"""The view to retrieve. Defaults to METADATA_ONLY. - - Values: - METADATA_ONLY: Template view that retrieves only the metadata associated - with the template. - """ - METADATA_ONLY = 0 - - gcsPath = _messages.StringField(1) - location = _messages.StringField(2) - projectId = _messages.StringField(3, required=True) - view = _messages.EnumField('ViewValueValuesEnum', 4) - - -class DataflowProjectsTemplatesLaunchRequest(_messages.Message): - r"""A DataflowProjectsTemplatesLaunchRequest object. - - Fields: - dynamicTemplate_gcsPath: Path to the dynamic template specification file - on Cloud Storage. The file must be a JSON serialized - `DynamicTemplateFileSpec` object. - dynamicTemplate_stagingLocation: Cloud Storage path for staging - dependencies. Must be a valid Cloud Storage URL, beginning with `gs://`. - gcsPath: A Cloud Storage path to the template to use to create the job. - Must be valid Cloud Storage URL, beginning with `gs://`. - launchTemplateParameters: A LaunchTemplateParameters resource to be passed - as the request body. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) to - which to direct the request. - projectId: Required. The ID of the Cloud Platform project that the job - belongs to. - validateOnly: If true, the request is validated but not actually executed. - Defaults to false. - """ - - dynamicTemplate_gcsPath = _messages.StringField(1) - dynamicTemplate_stagingLocation = _messages.StringField(2) - gcsPath = _messages.StringField(3) - launchTemplateParameters = _messages.MessageField('LaunchTemplateParameters', 4) - location = _messages.StringField(5) - projectId = _messages.StringField(6, required=True) - validateOnly = _messages.BooleanField(7) - - -class DataflowProjectsWorkerMessagesRequest(_messages.Message): - r"""A DataflowProjectsWorkerMessagesRequest object. - - Fields: - projectId: The project to send the WorkerMessages to. - sendWorkerMessagesRequest: A SendWorkerMessagesRequest resource to be - passed as the request body. - """ - - projectId = _messages.StringField(1, required=True) - sendWorkerMessagesRequest = _messages.MessageField('SendWorkerMessagesRequest', 2) - - -class DatastoreIODetails(_messages.Message): - r"""Metadata for a Datastore connector used by the job. - - Fields: - namespace: Namespace used in the connection. - projectId: ProjectId accessed in the connection. - """ - - namespace = _messages.StringField(1) - projectId = _messages.StringField(2) - - -class DebugOptions(_messages.Message): - r"""Describes any options that have an effect on the debugging of pipelines. - - Fields: - dataSampling: Configuration options for sampling elements from a running - pipeline. - enableHotKeyLogging: Optional. When true, enables the logging of the - literal hot key to the user's Cloud Logging. - """ - - dataSampling = _messages.MessageField('DataSamplingConfig', 1) - enableHotKeyLogging = _messages.BooleanField(2) - - -class DeleteSnapshotResponse(_messages.Message): - r"""Response from deleting a snapshot.""" - - -class DerivedSource(_messages.Message): - r"""Specification of one of the bundles produced as a result of splitting a - Source (e.g. when executing a SourceSplitRequest, or when splitting an - active task using WorkItemStatus.dynamic_source_split), relative to the - source being split. - - Enums: - DerivationModeValueValuesEnum: What source to base the produced source on - (if any). - - Fields: - derivationMode: What source to base the produced source on (if any). - source: Specification of the source. - """ - - class DerivationModeValueValuesEnum(_messages.Enum): - r"""What source to base the produced source on (if any). - - Values: - SOURCE_DERIVATION_MODE_UNKNOWN: The source derivation is unknown, or - unspecified. - SOURCE_DERIVATION_MODE_INDEPENDENT: Produce a completely independent - Source with no base. - SOURCE_DERIVATION_MODE_CHILD_OF_CURRENT: Produce a Source based on the - Source being split. - SOURCE_DERIVATION_MODE_SIBLING_OF_CURRENT: Produce a Source based on the - base of the Source being split. - """ - SOURCE_DERIVATION_MODE_UNKNOWN = 0 - SOURCE_DERIVATION_MODE_INDEPENDENT = 1 - SOURCE_DERIVATION_MODE_CHILD_OF_CURRENT = 2 - SOURCE_DERIVATION_MODE_SIBLING_OF_CURRENT = 3 - - derivationMode = _messages.EnumField('DerivationModeValueValuesEnum', 1) - source = _messages.MessageField('Source', 2) - - -class Disk(_messages.Message): - r"""Describes the data disk used by a workflow job. - - Fields: - diskType: Disk storage type, as defined by Google Compute Engine. This - must be a disk type appropriate to the project and zone in which the - workers will run. If unknown or unspecified, the service will attempt to - choose a reasonable default. For example, the standard persistent disk - type is a resource name typically ending in "pd-standard". If SSD - persistent disks are available, the resource name typically ends with - "pd-ssd". The actual valid values are defined the Google Compute Engine - API, not by the Cloud Dataflow API; consult the Google Compute Engine - documentation for more information about determining the set of - available disk types for a particular project and zone. Google Compute - Engine Disk types are local to a particular project in a particular - zone, and so the resource name will typically look something like this: - compute.googleapis.com/projects/project-id/zones/zone/diskTypes/pd- - standard - mountPoint: Directory in a VM where disk is mounted. - sizeGb: Size of disk in GB. If zero or unspecified, the service will - attempt to choose a reasonable default. - """ - - diskType = _messages.StringField(1) - mountPoint = _messages.StringField(2) - sizeGb = _messages.IntegerField(3, variant=_messages.Variant.INT32) - - -class DisplayData(_messages.Message): - r"""Data provided with a pipeline or transform to provide descriptive info. - - Fields: - boolValue: Contains value if the data is of a boolean type. - durationValue: Contains value if the data is of duration type. - floatValue: Contains value if the data is of float type. - int64Value: Contains value if the data is of int64 type. - javaClassValue: Contains value if the data is of java class type. - key: The key identifying the display data. This is intended to be used as - a label for the display data when viewed in a dax monitoring system. - label: An optional label to display in a dax UI for the element. - namespace: The namespace for the key. This is usually a class name or - programming language namespace (i.e. python module) which defines the - display data. This allows a dax monitoring system to specially handle - the data and perform custom rendering. - shortStrValue: A possible additional shorter value to display. For example - a java_class_name_value of com.mypackage.MyDoFn will be stored with - MyDoFn as the short_str_value and com.mypackage.MyDoFn as the - java_class_name value. short_str_value can be displayed and - java_class_name_value will be displayed as a tooltip. - strValue: Contains value if the data is of string type. - timestampValue: Contains value if the data is of timestamp type. - url: An optional full URL. - """ - - boolValue = _messages.BooleanField(1) - durationValue = _messages.StringField(2) - floatValue = _messages.FloatField(3, variant=_messages.Variant.FLOAT) - int64Value = _messages.IntegerField(4) - javaClassValue = _messages.StringField(5) - key = _messages.StringField(6) - label = _messages.StringField(7) - namespace = _messages.StringField(8) - shortStrValue = _messages.StringField(9) - strValue = _messages.StringField(10) - timestampValue = _messages.StringField(11) - url = _messages.StringField(12) - - -class DistributionUpdate(_messages.Message): - r"""A metric value representing a distribution. - - Fields: - count: The count of the number of elements present in the distribution. - histogram: (Optional) Histogram of value counts for the distribution. - max: The maximum value present in the distribution. - min: The minimum value present in the distribution. - sum: Use an int64 since we'd prefer the added precision. If overflow is a - common problem we can detect it and use an additional int64 or a double. - sumOfSquares: Use a double since the sum of squares is likely to overflow - int64. - """ - - count = _messages.MessageField('SplitInt64', 1) - histogram = _messages.MessageField('Histogram', 2) - max = _messages.MessageField('SplitInt64', 3) - min = _messages.MessageField('SplitInt64', 4) - sum = _messages.MessageField('SplitInt64', 5) - sumOfSquares = _messages.FloatField(6) - - -class DynamicSourceSplit(_messages.Message): - r"""When a task splits using WorkItemStatus.dynamic_source_split, this - message describes the two parts of the split relative to the description of - the current task's input. - - Fields: - primary: Primary part (continued to be processed by worker). Specified - relative to the previously-current source. Becomes current. - residual: Residual part (returned to the pool of work). Specified relative - to the previously-current source. - """ - - primary = _messages.MessageField('DerivedSource', 1) - residual = _messages.MessageField('DerivedSource', 2) - - -class Environment(_messages.Message): - r"""Describes the environment in which a Dataflow Job runs. - - Enums: - FlexResourceSchedulingGoalValueValuesEnum: Optional. Which Flexible - Resource Scheduling mode to run in. - ShuffleModeValueValuesEnum: Output only. The shuffle mode used for the - job. - StreamingModeValueValuesEnum: Optional. Specifies the Streaming Engine - message processing guarantees. Reduces cost and latency but might result - in duplicate messages committed to storage. Designed to run simple - mapping streaming ETL jobs at the lowest cost. For example, Change Data - Capture (CDC) to BigQuery is a canonical use case. For more information, - see [Set the pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - - Messages: - InternalExperimentsValue: Experimental settings. - SdkPipelineOptionsValue: The Cloud Dataflow SDK pipeline options specified - by the user. These options are passed through the service and are used - to recreate the SDK pipeline options on the worker in a language - agnostic and platform independent way. - UserAgentValue: Optional. A description of the process that generated the - request. - VersionValue: A structure describing which components and their versions - of the service are required in order to run the job. - - Fields: - clusterManagerApiService: The type of cluster manager API to use. If - unknown or unspecified, the service will attempt to choose a reasonable - default. This should be in the form of the API service name, e.g. - "compute.googleapis.com". - dataset: Optional. The dataset for the current project where various - workflow related tables are stored. The supported resource type is: - Google BigQuery: bigquery.googleapis.com/{dataset} - debugOptions: Optional. Any debugging options to be supplied to the job. - experiments: The list of experiments to enable. This field should be used - for SDK related experiments and not for service related experiments. The - proper field for service related experiments is service_options. - flexResourceSchedulingGoal: Optional. Which Flexible Resource Scheduling - mode to run in. - internalExperiments: Experimental settings. - sdkPipelineOptions: The Cloud Dataflow SDK pipeline options specified by - the user. These options are passed through the service and are used to - recreate the SDK pipeline options on the worker in a language agnostic - and platform independent way. - serviceAccountEmail: Optional. Identity to run virtual machines as. - Defaults to the default account. - serviceKmsKeyName: Optional. If set, contains the Cloud KMS key identifier - used to encrypt data at rest, AKA a Customer Managed Encryption Key - (CMEK). Format: - projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY - serviceOptions: Optional. The list of service options to enable. This - field should be used for service related experiments only. These - experiments, when graduating to GA, should be replaced by dedicated - fields or become default (i.e. always on). - shuffleMode: Output only. The shuffle mode used for the job. - streamingMode: Optional. Specifies the Streaming Engine message processing - guarantees. Reduces cost and latency but might result in duplicate - messages committed to storage. Designed to run simple mapping streaming - ETL jobs at the lowest cost. For example, Change Data Capture (CDC) to - BigQuery is a canonical use case. For more information, see [Set the - pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - tempStoragePrefix: The prefix of the resources the system should use for - temporary storage. The system will append the suffix "/temp-{JOBNAME} to - this resource prefix, where {JOBNAME} is the value of the job_name - field. The resulting bucket and object prefix is used as the prefix of - the resources used to store temporary data needed during the job - execution. NOTE: This will override the value in taskrunner_settings. - The supported resource type is: Google Cloud Storage: - storage.googleapis.com/{bucket}/{object} - bucket.storage.googleapis.com/{object} - usePublicIps: Optional. True when any worker pool that uses public IPs is - present. - useStreamingEngineResourceBasedBilling: Output only. Whether the job uses - the Streaming Engine resource-based billing model. - userAgent: Optional. A description of the process that generated the - request. - version: A structure describing which components and their versions of the - service are required in order to run the job. - workerPools: The worker pools. At least one "harness" worker pool must be - specified in order for the job to have workers. - workerRegion: Optional. The Compute Engine region - (https://cloud.google.com/compute/docs/regions-zones/regions-zones) in - which worker processing should occur, e.g. "us-west1". Mutually - exclusive with worker_zone. If neither worker_region nor worker_zone is - specified, default to the control plane's region. - workerZone: Optional. The Compute Engine zone - (https://cloud.google.com/compute/docs/regions-zones/regions-zones) in - which worker processing should occur, e.g. "us-west1-a". Mutually - exclusive with worker_region. If neither worker_region nor worker_zone - is specified, a zone in the control plane's region is chosen based on - available capacity. - """ - - class FlexResourceSchedulingGoalValueValuesEnum(_messages.Enum): - r"""Optional. Which Flexible Resource Scheduling mode to run in. - - Values: - FLEXRS_UNSPECIFIED: Run in the default mode. - FLEXRS_SPEED_OPTIMIZED: Optimize for lower execution time. - FLEXRS_COST_OPTIMIZED: Optimize for lower cost. - """ - FLEXRS_UNSPECIFIED = 0 - FLEXRS_SPEED_OPTIMIZED = 1 - FLEXRS_COST_OPTIMIZED = 2 - - class ShuffleModeValueValuesEnum(_messages.Enum): - r"""Output only. The shuffle mode used for the job. - - Values: - SHUFFLE_MODE_UNSPECIFIED: Shuffle mode information is not available. - VM_BASED: Shuffle is done on the worker VMs. - SERVICE_BASED: Shuffle is done on the service side. - """ - SHUFFLE_MODE_UNSPECIFIED = 0 - VM_BASED = 1 - SERVICE_BASED = 2 - - class StreamingModeValueValuesEnum(_messages.Enum): - r"""Optional. Specifies the Streaming Engine message processing - guarantees. Reduces cost and latency but might result in duplicate - messages committed to storage. Designed to run simple mapping streaming - ETL jobs at the lowest cost. For example, Change Data Capture (CDC) to - BigQuery is a canonical use case. For more information, see [Set the - pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - - Values: - STREAMING_MODE_UNSPECIFIED: Run in the default mode. - STREAMING_MODE_EXACTLY_ONCE: In this mode, message deduplication is - performed against persistent state to make sure each message is - processed and committed to storage exactly once. - STREAMING_MODE_AT_LEAST_ONCE: Message deduplication is not performed. - Messages might be processed multiple times, and the results are - applied multiple times. Note: Setting this value also enables - Streaming Engine and Streaming Engine resource-based billing. - """ - STREAMING_MODE_UNSPECIFIED = 0 - STREAMING_MODE_EXACTLY_ONCE = 1 - STREAMING_MODE_AT_LEAST_ONCE = 2 - - @encoding.MapUnrecognizedFields('additionalProperties') - class InternalExperimentsValue(_messages.Message): - r"""Experimental settings. - - Messages: - AdditionalProperty: An additional property for a - InternalExperimentsValue object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a InternalExperimentsValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class SdkPipelineOptionsValue(_messages.Message): - r"""The Cloud Dataflow SDK pipeline options specified by the user. These - options are passed through the service and are used to recreate the SDK - pipeline options on the worker in a language agnostic and platform - independent way. - - Messages: - AdditionalProperty: An additional property for a SdkPipelineOptionsValue - object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a SdkPipelineOptionsValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class UserAgentValue(_messages.Message): - r"""Optional. A description of the process that generated the request. - - Messages: - AdditionalProperty: An additional property for a UserAgentValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UserAgentValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class VersionValue(_messages.Message): - r"""A structure describing which components and their versions of the - service are required in order to run the job. - - Messages: - AdditionalProperty: An additional property for a VersionValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a VersionValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - clusterManagerApiService = _messages.StringField(1) - dataset = _messages.StringField(2) - debugOptions = _messages.MessageField('DebugOptions', 3) - experiments = _messages.StringField(4, repeated=True) - flexResourceSchedulingGoal = _messages.EnumField('FlexResourceSchedulingGoalValueValuesEnum', 5) - internalExperiments = _messages.MessageField('InternalExperimentsValue', 6) - sdkPipelineOptions = _messages.MessageField('SdkPipelineOptionsValue', 7) - serviceAccountEmail = _messages.StringField(8) - serviceKmsKeyName = _messages.StringField(9) - serviceOptions = _messages.StringField(10, repeated=True) - shuffleMode = _messages.EnumField('ShuffleModeValueValuesEnum', 11) - streamingMode = _messages.EnumField('StreamingModeValueValuesEnum', 12) - tempStoragePrefix = _messages.StringField(13) - usePublicIps = _messages.BooleanField(14) - useStreamingEngineResourceBasedBilling = _messages.BooleanField(15) - userAgent = _messages.MessageField('UserAgentValue', 16) - version = _messages.MessageField('VersionValue', 17) - workerPools = _messages.MessageField('WorkerPool', 18, repeated=True) - workerRegion = _messages.StringField(19) - workerZone = _messages.StringField(20) - - -class ExecutionStageState(_messages.Message): - r"""A message describing the state of a particular execution stage. - - Enums: - ExecutionStageStateValueValuesEnum: Executions stage states allow the same - set of values as JobState. - - Fields: - currentStateTime: The time at which the stage transitioned to this state. - executionStageName: The name of the execution stage. - executionStageState: Executions stage states allow the same set of values - as JobState. - """ - - class ExecutionStageStateValueValuesEnum(_messages.Enum): - r"""Executions stage states allow the same set of values as JobState. - - Values: - JOB_STATE_UNKNOWN: The job's run state isn't specified. - JOB_STATE_STOPPED: `JOB_STATE_STOPPED` indicates that the job has not - yet started to run. - JOB_STATE_RUNNING: `JOB_STATE_RUNNING` indicates that the job is - currently running. - JOB_STATE_DONE: `JOB_STATE_DONE` indicates that the job has successfully - completed. This is a terminal job state. This state may be set by the - Cloud Dataflow service, as a transition from `JOB_STATE_RUNNING`. It - may also be set via a Cloud Dataflow `UpdateJob` call, if the job has - not yet reached a terminal state. - JOB_STATE_FAILED: `JOB_STATE_FAILED` indicates that the job has failed. - This is a terminal job state. This state may only be set by the Cloud - Dataflow service, and only as a transition from `JOB_STATE_RUNNING`. - JOB_STATE_CANCELLED: `JOB_STATE_CANCELLED` indicates that the job has - been explicitly cancelled. This is a terminal job state. This state - may only be set via a Cloud Dataflow `UpdateJob` call, and only if the - job has not yet reached another terminal state. - JOB_STATE_UPDATED: `JOB_STATE_UPDATED` indicates that the job was - successfully updated, meaning that this job was stopped and another - job was started, inheriting state from this one. This is a terminal - job state. This state may only be set by the Cloud Dataflow service, - and only as a transition from `JOB_STATE_RUNNING`. - JOB_STATE_DRAINING: `JOB_STATE_DRAINING` indicates that the job is in - the process of draining. A draining job has stopped pulling from its - input sources and is processing any data that remains in-flight. This - state may be set via a Cloud Dataflow `UpdateJob` call, but only as a - transition from `JOB_STATE_RUNNING`. Jobs that are draining may only - transition to `JOB_STATE_DRAINED`, `JOB_STATE_CANCELLED`, or - `JOB_STATE_FAILED`. - JOB_STATE_DRAINED: `JOB_STATE_DRAINED` indicates that the job has been - drained. A drained job terminated by stopping pulling from its input - sources and processing any data that remained in-flight when draining - was requested. This state is a terminal state, may only be set by the - Cloud Dataflow service, and only as a transition from - `JOB_STATE_DRAINING`. - JOB_STATE_PENDING: `JOB_STATE_PENDING` indicates that the job has been - created but is not yet running. Jobs that are pending may only - transition to `JOB_STATE_RUNNING`, or `JOB_STATE_FAILED`. - JOB_STATE_CANCELLING: `JOB_STATE_CANCELLING` indicates that the job has - been explicitly cancelled and is in the process of stopping. Jobs that - are cancelling may only transition to `JOB_STATE_CANCELLED` or - `JOB_STATE_FAILED`. - JOB_STATE_QUEUED: `JOB_STATE_QUEUED` indicates that the job has been - created but is being delayed until launch. Jobs that are queued may - only transition to `JOB_STATE_PENDING` or `JOB_STATE_CANCELLED`. - JOB_STATE_RESOURCE_CLEANING_UP: `JOB_STATE_RESOURCE_CLEANING_UP` - indicates that the batch job's associated resources are currently - being cleaned up after a successful run. Currently, this is an opt-in - feature, please reach out to Cloud support team if you are interested. - JOB_STATE_PAUSING: `JOB_STATE_PAUSING` is not implemented yet. - JOB_STATE_PAUSED: `JOB_STATE_PAUSED` is not implemented yet. - """ - JOB_STATE_UNKNOWN = 0 - JOB_STATE_STOPPED = 1 - JOB_STATE_RUNNING = 2 - JOB_STATE_DONE = 3 - JOB_STATE_FAILED = 4 - JOB_STATE_CANCELLED = 5 - JOB_STATE_UPDATED = 6 - JOB_STATE_DRAINING = 7 - JOB_STATE_DRAINED = 8 - JOB_STATE_PENDING = 9 - JOB_STATE_CANCELLING = 10 - JOB_STATE_QUEUED = 11 - JOB_STATE_RESOURCE_CLEANING_UP = 12 - JOB_STATE_PAUSING = 13 - JOB_STATE_PAUSED = 14 - - currentStateTime = _messages.StringField(1) - executionStageName = _messages.StringField(2) - executionStageState = _messages.EnumField('ExecutionStageStateValueValuesEnum', 3) - - -class ExecutionStageSummary(_messages.Message): - r"""Description of the composing transforms, names/ids, and input/outputs of - a stage of execution. Some composing transforms and sources may have been - generated by the Dataflow service during execution planning. - - Enums: - KindValueValuesEnum: Type of transform this stage is executing. - - Fields: - componentSource: Collections produced and consumed by component transforms - of this stage. - componentTransform: Transforms that comprise this execution stage. - id: Dataflow service generated id for this stage. - inputSource: Input sources for this stage. - kind: Type of transform this stage is executing. - name: Dataflow service generated name for this stage. - outputSource: Output sources for this stage. - prerequisiteStage: Other stages that must complete before this stage can - run. - """ - - class KindValueValuesEnum(_messages.Enum): - r"""Type of transform this stage is executing. - - Values: - UNKNOWN_KIND: Unrecognized transform type. - PAR_DO_KIND: ParDo transform. - GROUP_BY_KEY_KIND: Group By Key transform. - FLATTEN_KIND: Flatten transform. - READ_KIND: Read transform. - WRITE_KIND: Write transform. - CONSTANT_KIND: Constructs from a constant value, such as with Create.of. - SINGLETON_KIND: Creates a Singleton view of a collection. - SHUFFLE_KIND: Opening or closing a shuffle session, often as part of a - GroupByKey. - """ - UNKNOWN_KIND = 0 - PAR_DO_KIND = 1 - GROUP_BY_KEY_KIND = 2 - FLATTEN_KIND = 3 - READ_KIND = 4 - WRITE_KIND = 5 - CONSTANT_KIND = 6 - SINGLETON_KIND = 7 - SHUFFLE_KIND = 8 - - componentSource = _messages.MessageField('ComponentSource', 1, repeated=True) - componentTransform = _messages.MessageField('ComponentTransform', 2, repeated=True) - id = _messages.StringField(3) - inputSource = _messages.MessageField('StageSource', 4, repeated=True) - kind = _messages.EnumField('KindValueValuesEnum', 5) - name = _messages.StringField(6) - outputSource = _messages.MessageField('StageSource', 7, repeated=True) - prerequisiteStage = _messages.StringField(8, repeated=True) - - -class FailedLocation(_messages.Message): - r"""Indicates which [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) failed - to respond to a request for data. - - Fields: - name: The name of the [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that failed to respond. - """ - - name = _messages.StringField(1) - - -class FileIODetails(_messages.Message): - r"""Metadata for a File connector used by the job. - - Fields: - filePattern: File Pattern used to access files by the connector. - """ - - filePattern = _messages.StringField(1) - - -class FlattenInstruction(_messages.Message): - r"""An instruction that copies its inputs (zero or more) to its (single) - output. - - Fields: - inputs: Describes the inputs to the flatten instruction. - """ - - inputs = _messages.MessageField('InstructionInput', 1, repeated=True) - - -class FlexTemplateRuntimeEnvironment(_messages.Message): - r"""The environment values to be set at runtime for flex template. - - Enums: - AutoscalingAlgorithmValueValuesEnum: The algorithm to use for autoscaling - FlexrsGoalValueValuesEnum: Set FlexRS goal for the job. - https://cloud.google.com/dataflow/docs/guides/flexrs - IpConfigurationValueValuesEnum: Configuration for VM IPs. - StreamingModeValueValuesEnum: Optional. Specifies the Streaming Engine - message processing guarantees. Reduces cost and latency but might result - in duplicate messages committed to storage. Designed to run simple - mapping streaming ETL jobs at the lowest cost. For example, Change Data - Capture (CDC) to BigQuery is a canonical use case. For more information, - see [Set the pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - - Messages: - AdditionalUserLabelsValue: Additional user labels to be specified for the - job. Keys and values must follow the restrictions specified in the - [labeling restrictions](https://cloud.google.com/compute/docs/labeling- - resources#restrictions) page. An object containing a list of "key": - value pairs. Example: { "name": "wrench", "mass": "1kg", "count": "3" }. - - Fields: - additionalExperiments: Additional experiment flags for the job. - additionalPipelineOptions: Optional. Additional pipeline option flags for - the job. - additionalUserLabels: Additional user labels to be specified for the job. - Keys and values must follow the restrictions specified in the [labeling - restrictions](https://cloud.google.com/compute/docs/labeling- - resources#restrictions) page. An object containing a list of "key": - value pairs. Example: { "name": "wrench", "mass": "1kg", "count": "3" }. - autoscalingAlgorithm: The algorithm to use for autoscaling - diskSizeGb: Worker disk size, in gigabytes. - dumpHeapOnOom: If true, when processing time is spent almost entirely on - garbage collection (GC), saves a heap dump before ending the thread or - process. If false, ends the thread or process without saving a heap - dump. Does not save a heap dump when the Java Virtual Machine (JVM) has - an out of memory error during processing. The location of the heap file - is either echoed back to the user, or the user is given the opportunity - to download the heap file. - enableLauncherVmSerialPortLogging: If true serial port logging will be - enabled for the launcher VM. - enableStreamingEngine: Whether to enable Streaming Engine for the job. - flexrsGoal: Set FlexRS goal for the job. - https://cloud.google.com/dataflow/docs/guides/flexrs - ipConfiguration: Configuration for VM IPs. - kmsKeyName: Name for the Cloud KMS key for the job. Key format is: - projects//locations//keyRings//cryptoKeys/ - launcherMachineType: The machine type to use for launching the job. If not - set, Dataflow will select a default machine type. - machineType: The machine type to use for the job. Defaults to the value - from the template if not specified. - maxWorkers: The maximum number of Google Compute Engine instances to be - made available to your pipeline during execution, from 1 to 1000. - network: Network to which VMs will be assigned. If empty or unspecified, - the service will use the network "default". - numWorkers: The initial number of Google Compute Engine instances for the - job. - saveHeapDumpsToGcsPath: Cloud Storage bucket (directory) to upload heap - dumps to. Enabling this field implies that `dump_heap_on_oom` is set to - true. - sdkContainerImage: Docker registry location of container image to use for - the 'worker harness. Default is the container for the version of the - SDK. Note this field is only valid for portable pipelines. - serviceAccountEmail: The email address of the service account to run the - job as. - stagingLocation: The Cloud Storage path for staging local files. Must be a - valid Cloud Storage URL, beginning with `gs://`. - streamingMode: Optional. Specifies the Streaming Engine message processing - guarantees. Reduces cost and latency but might result in duplicate - messages committed to storage. Designed to run simple mapping streaming - ETL jobs at the lowest cost. For example, Change Data Capture (CDC) to - BigQuery is a canonical use case. For more information, see [Set the - pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - subnetwork: Subnetwork to which VMs will be assigned, if desired. You can - specify a subnetwork using either a complete URL or an abbreviated path. - Expected to be of the form "https://www.googleapis.com/compute/v1/projec - ts/HOST_PROJECT_ID/regions/REGION/subnetworks/SUBNETWORK" or - "regions/REGION/subnetworks/SUBNETWORK". If the subnetwork is located in - a Shared VPC network, you must use the complete URL. - tempLocation: The Cloud Storage path to use for temporary files. Must be a - valid Cloud Storage URL, beginning with `gs://`. - workerRegion: The Compute Engine region - (https://cloud.google.com/compute/docs/regions-zones/regions-zones) in - which worker processing should occur, e.g. "us-west1". Mutually - exclusive with worker_zone. If neither worker_region nor worker_zone is - specified, default to the control plane's region. - workerZone: The Compute Engine zone - (https://cloud.google.com/compute/docs/regions-zones/regions-zones) in - which worker processing should occur, e.g. "us-west1-a". Mutually - exclusive with worker_region. If neither worker_region nor worker_zone - is specified, a zone in the control plane's region is chosen based on - available capacity. If both `worker_zone` and `zone` are set, - `worker_zone` takes precedence. - zone: The Compute Engine [availability - zone](https://cloud.google.com/compute/docs/regions-zones/regions-zones) - for launching worker instances to run your pipeline. In the future, - worker_zone will take precedence. - """ - - class AutoscalingAlgorithmValueValuesEnum(_messages.Enum): - r"""The algorithm to use for autoscaling - - Values: - AUTOSCALING_ALGORITHM_UNKNOWN: The algorithm is unknown, or unspecified. - AUTOSCALING_ALGORITHM_NONE: Disable autoscaling. - AUTOSCALING_ALGORITHM_BASIC: Increase worker count over time to reduce - job execution time. - """ - AUTOSCALING_ALGORITHM_UNKNOWN = 0 - AUTOSCALING_ALGORITHM_NONE = 1 - AUTOSCALING_ALGORITHM_BASIC = 2 - - class FlexrsGoalValueValuesEnum(_messages.Enum): - r"""Set FlexRS goal for the job. - https://cloud.google.com/dataflow/docs/guides/flexrs - - Values: - FLEXRS_UNSPECIFIED: Run in the default mode. - FLEXRS_SPEED_OPTIMIZED: Optimize for lower execution time. - FLEXRS_COST_OPTIMIZED: Optimize for lower cost. - """ - FLEXRS_UNSPECIFIED = 0 - FLEXRS_SPEED_OPTIMIZED = 1 - FLEXRS_COST_OPTIMIZED = 2 - - class IpConfigurationValueValuesEnum(_messages.Enum): - r"""Configuration for VM IPs. - - Values: - WORKER_IP_UNSPECIFIED: The configuration is unknown, or unspecified. - WORKER_IP_PUBLIC: Workers should have public IP addresses. - WORKER_IP_PRIVATE: Workers should have private IP addresses. - """ - WORKER_IP_UNSPECIFIED = 0 - WORKER_IP_PUBLIC = 1 - WORKER_IP_PRIVATE = 2 - - class StreamingModeValueValuesEnum(_messages.Enum): - r"""Optional. Specifies the Streaming Engine message processing - guarantees. Reduces cost and latency but might result in duplicate - messages committed to storage. Designed to run simple mapping streaming - ETL jobs at the lowest cost. For example, Change Data Capture (CDC) to - BigQuery is a canonical use case. For more information, see [Set the - pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - - Values: - STREAMING_MODE_UNSPECIFIED: Run in the default mode. - STREAMING_MODE_EXACTLY_ONCE: In this mode, message deduplication is - performed against persistent state to make sure each message is - processed and committed to storage exactly once. - STREAMING_MODE_AT_LEAST_ONCE: Message deduplication is not performed. - Messages might be processed multiple times, and the results are - applied multiple times. Note: Setting this value also enables - Streaming Engine and Streaming Engine resource-based billing. - """ - STREAMING_MODE_UNSPECIFIED = 0 - STREAMING_MODE_EXACTLY_ONCE = 1 - STREAMING_MODE_AT_LEAST_ONCE = 2 - - @encoding.MapUnrecognizedFields('additionalProperties') - class AdditionalUserLabelsValue(_messages.Message): - r"""Additional user labels to be specified for the job. Keys and values - must follow the restrictions specified in the [labeling - restrictions](https://cloud.google.com/compute/docs/labeling- - resources#restrictions) page. An object containing a list of "key": value - pairs. Example: { "name": "wrench", "mass": "1kg", "count": "3" }. - - Messages: - AdditionalProperty: An additional property for a - AdditionalUserLabelsValue object. - - Fields: - additionalProperties: Additional properties of type - AdditionalUserLabelsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a AdditionalUserLabelsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - additionalExperiments = _messages.StringField(1, repeated=True) - additionalPipelineOptions = _messages.StringField(2, repeated=True) - additionalUserLabels = _messages.MessageField('AdditionalUserLabelsValue', 3) - autoscalingAlgorithm = _messages.EnumField('AutoscalingAlgorithmValueValuesEnum', 4) - diskSizeGb = _messages.IntegerField(5, variant=_messages.Variant.INT32) - dumpHeapOnOom = _messages.BooleanField(6) - enableLauncherVmSerialPortLogging = _messages.BooleanField(7) - enableStreamingEngine = _messages.BooleanField(8) - flexrsGoal = _messages.EnumField('FlexrsGoalValueValuesEnum', 9) - ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 10) - kmsKeyName = _messages.StringField(11) - launcherMachineType = _messages.StringField(12) - machineType = _messages.StringField(13) - maxWorkers = _messages.IntegerField(14, variant=_messages.Variant.INT32) - network = _messages.StringField(15) - numWorkers = _messages.IntegerField(16, variant=_messages.Variant.INT32) - saveHeapDumpsToGcsPath = _messages.StringField(17) - sdkContainerImage = _messages.StringField(18) - serviceAccountEmail = _messages.StringField(19) - stagingLocation = _messages.StringField(20) - streamingMode = _messages.EnumField('StreamingModeValueValuesEnum', 21) - subnetwork = _messages.StringField(22) - tempLocation = _messages.StringField(23) - workerRegion = _messages.StringField(24) - workerZone = _messages.StringField(25) - zone = _messages.StringField(26) - - -class FloatingPointList(_messages.Message): - r"""A metric value representing a list of floating point numbers. - - Fields: - elements: Elements of the list. - """ - - elements = _messages.FloatField(1, repeated=True) - - -class FloatingPointMean(_messages.Message): - r"""A representation of a floating point mean metric contribution. - - Fields: - count: The number of values being aggregated. - sum: The sum of all values being aggregated. - """ - - count = _messages.MessageField('SplitInt64', 1) - sum = _messages.FloatField(2) - - -class GPUUsage(_messages.Message): - r"""Information about the GPU usage on the worker. - - Fields: - timestamp: Required. Timestamp of the measurement. - utilization: Required. Utilization info about the GPU. - """ - - timestamp = _messages.StringField(1) - utilization = _messages.MessageField('GPUUtilization', 2) - - -class GPUUtilization(_messages.Message): - r"""Utilization details about the GPU. - - Fields: - rate: Required. GPU utilization rate of any kernel over the last sample - period in the range of [0, 1]. - """ - - rate = _messages.FloatField(1) - - -class GetDebugConfigRequest(_messages.Message): - r"""Request to get updated debug configuration for component. - - Fields: - componentId: The internal component id for which debug configuration is - requested. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - workerId: The worker id, i.e., VM hostname. - """ - - componentId = _messages.StringField(1) - location = _messages.StringField(2) - workerId = _messages.StringField(3) - - -class GetDebugConfigResponse(_messages.Message): - r"""Response to a get debug configuration request. - - Fields: - config: The encoded debug configuration for the requested component. - """ - - config = _messages.StringField(1) - - -class GetTemplateResponse(_messages.Message): - r"""The response to a GetTemplate request. - - Enums: - TemplateTypeValueValuesEnum: Template Type. - - Fields: - metadata: The template metadata describing the template name, available - parameters, etc. - runtimeMetadata: Describes the runtime metadata with SDKInfo and available - parameters. - status: The status of the get template request. Any problems with the - request will be indicated in the error_details. - templateType: Template Type. - """ - - class TemplateTypeValueValuesEnum(_messages.Enum): - r"""Template Type. - - Values: - UNKNOWN: Unknown Template Type. - LEGACY: Legacy Template. - FLEX: Flex Template. - """ - UNKNOWN = 0 - LEGACY = 1 - FLEX = 2 - - metadata = _messages.MessageField('TemplateMetadata', 1) - runtimeMetadata = _messages.MessageField('RuntimeMetadata', 2) - status = _messages.MessageField('Status', 3) - templateType = _messages.EnumField('TemplateTypeValueValuesEnum', 4) - - -class GetWorkerStacktracesRequest(_messages.Message): - r"""Request to get worker stacktraces from debug capture. - - Fields: - endTime: The end time for the stacktrace query. The returned stacktraces - will be a recent stack trace at or shortly before this time. - workerId: The worker for which to get stacktraces. The returned - stacktraces will be for the SDK harness running on this worker. - """ - - endTime = _messages.StringField(1) - workerId = _messages.StringField(2) - - -class GetWorkerStacktracesResponse(_messages.Message): - r"""Response to get worker stacktraces from debug capture. - - Fields: - sdks: Repeated as unified worker may have multiple SDK processes. - """ - - sdks = _messages.MessageField('Sdk', 1, repeated=True) - - -class Histogram(_messages.Message): - r"""Histogram of value counts for a distribution. Buckets have an inclusive - lower bound and exclusive upper bound and use "1,2,5 bucketing": The first - bucket range is from [0,1) and all subsequent bucket boundaries are powers - of ten multiplied by 1, 2, or 5. Thus, bucket boundaries are 0, 1, 2, 5, 10, - 20, 50, 100, 200, 500, 1000, ... Negative values are not supported. - - Fields: - bucketCounts: Counts of values in each bucket. For efficiency, prefix and - trailing buckets with count = 0 are elided. Buckets can store the full - range of values of an unsigned long, with ULLONG_MAX falling into the - 59th bucket with range [1e19, 2e19). - firstBucketOffset: Starting index of first stored bucket. The non- - inclusive upper-bound of the ith bucket is given by: - pow(10,(i-first_bucket_offset)/3) * (1,2,5)[(i-first_bucket_offset)%3] - """ - - bucketCounts = _messages.IntegerField(1, repeated=True) - firstBucketOffset = _messages.IntegerField(2, variant=_messages.Variant.INT32) - - -class HotKeyDebuggingInfo(_messages.Message): - r"""Information useful for debugging a hot key detection. - - Messages: - DetectedHotKeysValue: Debugging information for each detected hot key. - Keyed by a hash of the key. - - Fields: - detectedHotKeys: Debugging information for each detected hot key. Keyed by - a hash of the key. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class DetectedHotKeysValue(_messages.Message): - r"""Debugging information for each detected hot key. Keyed by a hash of - the key. - - Messages: - AdditionalProperty: An additional property for a DetectedHotKeysValue - object. - - Fields: - additionalProperties: Additional properties of type DetectedHotKeysValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a DetectedHotKeysValue object. - - Fields: - key: Name of the additional property. - value: A HotKeyInfo attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('HotKeyInfo', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - detectedHotKeys = _messages.MessageField('DetectedHotKeysValue', 1) - - -class HotKeyDetection(_messages.Message): - r"""Proto describing a hot key detected on a given WorkItem. - - Fields: - hotKeyAge: The age of the hot key measured from when it was first - detected. - systemName: System-defined name of the step containing this hot key. - Unique across the workflow. - userStepName: User-provided name of the step that contains this hot key. - """ - - hotKeyAge = _messages.StringField(1) - systemName = _messages.StringField(2) - userStepName = _messages.StringField(3) - - -class HotKeyInfo(_messages.Message): - r"""Information about a hot key. - - Fields: - hotKeyAge: The age of the hot key measured from when it was first - detected. - key: A detected hot key that is causing limited parallelism. This field - will be populated only if the following flag is set to true: "-- - enable_hot_key_logging". - keyTruncated: If true, then the above key is truncated and cannot be - deserialized. This occurs if the key above is populated and the key size - is >5MB. - """ - - hotKeyAge = _messages.StringField(1) - key = _messages.StringField(2) - keyTruncated = _messages.BooleanField(3) - - -class InstructionInput(_messages.Message): - r"""An input of an instruction, as a reference to an output of a producer - instruction. - - Fields: - outputNum: The output index (origin zero) within the producer. - producerInstructionIndex: The index (origin zero) of the parallel - instruction that produces the output to be consumed by this input. This - index is relative to the list of instructions in this input's - instruction's containing MapTask. - """ - - outputNum = _messages.IntegerField(1, variant=_messages.Variant.INT32) - producerInstructionIndex = _messages.IntegerField(2, variant=_messages.Variant.INT32) - - -class InstructionOutput(_messages.Message): - r"""An output of an instruction. - - Messages: - CodecValue: The codec to use to encode data being written via this output. - - Fields: - codec: The codec to use to encode data being written via this output. - name: The user-provided name of this output. - onlyCountKeyBytes: For system-generated byte and mean byte metrics, - certain instructions should only report the key size. - onlyCountValueBytes: For system-generated byte and mean byte metrics, - certain instructions should only report the value size. - originalName: System-defined name for this output in the original workflow - graph. Outputs that do not contribute to an original instruction do not - set this. - systemName: System-defined name of this output. Unique across the - workflow. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class CodecValue(_messages.Message): - r"""The codec to use to encode data being written via this output. - - Messages: - AdditionalProperty: An additional property for a CodecValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a CodecValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - codec = _messages.MessageField('CodecValue', 1) - name = _messages.StringField(2) - onlyCountKeyBytes = _messages.BooleanField(3) - onlyCountValueBytes = _messages.BooleanField(4) - originalName = _messages.StringField(5) - systemName = _messages.StringField(6) - - -class IntegerGauge(_messages.Message): - r"""A metric value representing temporal values of a variable. - - Fields: - timestamp: The time at which this value was measured. Measured as msecs - from epoch. - value: The value of the variable represented by this gauge. - """ - - timestamp = _messages.StringField(1) - value = _messages.MessageField('SplitInt64', 2) - - -class IntegerList(_messages.Message): - r"""A metric value representing a list of integers. - - Fields: - elements: Elements of the list. - """ - - elements = _messages.MessageField('SplitInt64', 1, repeated=True) - - -class IntegerMean(_messages.Message): - r"""A representation of an integer mean metric contribution. - - Fields: - count: The number of values being aggregated. - sum: The sum of all values being aggregated. - """ - - count = _messages.MessageField('SplitInt64', 1) - sum = _messages.MessageField('SplitInt64', 2) - - -class Job(_messages.Message): - r"""Defines a job to be run by the Cloud Dataflow service. Do not enter - confidential information when you supply string values using the API. - - Enums: - CurrentStateValueValuesEnum: The current state of the job. Jobs are - created in the `JOB_STATE_STOPPED` state unless otherwise specified. A - job in the `JOB_STATE_RUNNING` state may asynchronously enter a terminal - state. After a job has reached a terminal state, no further state - updates may be made. This field might be mutated by the Dataflow - service; callers cannot mutate it. - RequestedStateValueValuesEnum: The job's requested state. Applies to - `UpdateJob` requests. Set `requested_state` with `UpdateJob` requests to - switch between the states `JOB_STATE_STOPPED` and `JOB_STATE_RUNNING`. - You can also use `UpdateJob` requests to change a job's state from - `JOB_STATE_RUNNING` to `JOB_STATE_CANCELLED`, `JOB_STATE_DONE`, or - `JOB_STATE_DRAINED`. These states irrevocably terminate the job if it - hasn't already reached a terminal state. This field has no effect on - `CreateJob` requests. - TypeValueValuesEnum: Optional. The type of Dataflow job. - - Messages: - LabelsValue: User-defined labels for this job. The labels map can contain - no more than 64 entries. Entries of the labels map are UTF8 strings that - comply with the following restrictions: * Keys must conform to regexp: - \p{Ll}\p{Lo}{0,62} * Values must conform to regexp: - [\p{Ll}\p{Lo}\p{N}_-]{0,63} * Both keys and values are additionally - constrained to be <= 128 bytes in size. - TransformNameMappingValue: Optional. The map of transform name prefixes of - the job to be replaced to the corresponding name prefixes of the new - job. - - Fields: - clientRequestId: The client's unique identifier of the job, re-used across - retried attempts. If this field is set, the service will ensure its - uniqueness. The request to create a job will fail if the service has - knowledge of a previously submitted job with the same client's ID and - job name. The caller may use this field to ensure idempotence of job - creation across retried attempts to create a job. By default, the field - is empty and, in that case, the service ignores it. - createTime: The timestamp when the job was initially created. Immutable - and set by the Cloud Dataflow service. - createdFromSnapshotId: If this is specified, the job's initial state is - populated from the given snapshot. - currentState: The current state of the job. Jobs are created in the - `JOB_STATE_STOPPED` state unless otherwise specified. A job in the - `JOB_STATE_RUNNING` state may asynchronously enter a terminal state. - After a job has reached a terminal state, no further state updates may - be made. This field might be mutated by the Dataflow service; callers - cannot mutate it. - currentStateTime: The timestamp associated with the current state. - environment: Optional. The environment for the job. - executionInfo: Deprecated. - id: The unique ID of this job. This field is set by the Dataflow service - when the job is created, and is immutable for the life of the job. - jobMetadata: This field is populated by the Dataflow service to support - filtering jobs by the metadata values provided here. Populated for - ListJobs and all GetJob views SUMMARY and higher. - labels: User-defined labels for this job. The labels map can contain no - more than 64 entries. Entries of the labels map are UTF8 strings that - comply with the following restrictions: * Keys must conform to regexp: - \p{Ll}\p{Lo}{0,62} * Values must conform to regexp: - [\p{Ll}\p{Lo}\p{N}_-]{0,63} * Both keys and values are additionally - constrained to be <= 128 bytes in size. - location: Optional. The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains this job. - name: Optional. The user-specified Dataflow job name. Only one active job - with a given name can exist in a project within one region at any given - time. Jobs in different regions can have the same name. If a caller - attempts to create a job with the same name as an active job that - already exists, the attempt returns the existing job. The name must - match the regular expression `[a-z]([-a-z0-9]{0,1022}[a-z0-9])?` - pausable: Output only. Indicates whether the job can be paused. - pipelineDescription: Preliminary field: The format of this data may change - at any time. A description of the user pipeline and stages through which - it is executed. Created by Cloud Dataflow service. Only retrieved with - JOB_VIEW_DESCRIPTION or JOB_VIEW_ALL. - projectId: The ID of the Google Cloud project that the job belongs to. - replaceJobId: If this job is an update of an existing job, this field is - the job ID of the job it replaced. When sending a `CreateJobRequest`, - you can update a job by specifying it here. The job named here is - stopped, and its intermediate state is transferred to this job. - replacedByJobId: If another job is an update of this job (and thus, this - job is in `JOB_STATE_UPDATED`), this field contains the ID of that job. - requestedState: The job's requested state. Applies to `UpdateJob` - requests. Set `requested_state` with `UpdateJob` requests to switch - between the states `JOB_STATE_STOPPED` and `JOB_STATE_RUNNING`. You can - also use `UpdateJob` requests to change a job's state from - `JOB_STATE_RUNNING` to `JOB_STATE_CANCELLED`, `JOB_STATE_DONE`, or - `JOB_STATE_DRAINED`. These states irrevocably terminate the job if it - hasn't already reached a terminal state. This field has no effect on - `CreateJob` requests. - runtimeUpdatableParams: This field may ONLY be modified at runtime using - the projects.jobs.update method to adjust job behavior. This field has - no effect when specified at job creation. - satisfiesPzi: Output only. Reserved for future use. This field is set only - in responses from the server; it is ignored if it is set in any - requests. - satisfiesPzs: Reserved for future use. This field is set only in responses - from the server; it is ignored if it is set in any requests. - serviceResources: Output only. Resources used by the Dataflow Service to - run the job. - stageStates: This field may be mutated by the Cloud Dataflow service; - callers cannot mutate it. - startTime: The timestamp when the job was started (transitioned to - JOB_STATE_PENDING). Flexible resource scheduling jobs are started with - some delay after job creation, so start_time is unset before start and - is updated when the job is started by the Cloud Dataflow service. For - other jobs, start_time always equals to create_time and is immutable and - set by the Cloud Dataflow service. - steps: Exactly one of step or steps_location should be specified. The top- - level steps that constitute the entire job. Only retrieved with - JOB_VIEW_ALL. - stepsLocation: The Cloud Storage location where the steps are stored. - tempFiles: A set of files the system should be aware of that are used for - temporary storage. These temporary files will be removed on job - completion. No duplicates are allowed. No file patterns are supported. - The supported files are: Google Cloud Storage: - storage.googleapis.com/{bucket}/{object} - bucket.storage.googleapis.com/{object} - transformNameMapping: Optional. The map of transform name prefixes of the - job to be replaced to the corresponding name prefixes of the new job. - type: Optional. The type of Dataflow job. - """ - - class CurrentStateValueValuesEnum(_messages.Enum): - r"""The current state of the job. Jobs are created in the - `JOB_STATE_STOPPED` state unless otherwise specified. A job in the - `JOB_STATE_RUNNING` state may asynchronously enter a terminal state. After - a job has reached a terminal state, no further state updates may be made. - This field might be mutated by the Dataflow service; callers cannot mutate - it. - - Values: - JOB_STATE_UNKNOWN: The job's run state isn't specified. - JOB_STATE_STOPPED: `JOB_STATE_STOPPED` indicates that the job has not - yet started to run. - JOB_STATE_RUNNING: `JOB_STATE_RUNNING` indicates that the job is - currently running. - JOB_STATE_DONE: `JOB_STATE_DONE` indicates that the job has successfully - completed. This is a terminal job state. This state may be set by the - Cloud Dataflow service, as a transition from `JOB_STATE_RUNNING`. It - may also be set via a Cloud Dataflow `UpdateJob` call, if the job has - not yet reached a terminal state. - JOB_STATE_FAILED: `JOB_STATE_FAILED` indicates that the job has failed. - This is a terminal job state. This state may only be set by the Cloud - Dataflow service, and only as a transition from `JOB_STATE_RUNNING`. - JOB_STATE_CANCELLED: `JOB_STATE_CANCELLED` indicates that the job has - been explicitly cancelled. This is a terminal job state. This state - may only be set via a Cloud Dataflow `UpdateJob` call, and only if the - job has not yet reached another terminal state. - JOB_STATE_UPDATED: `JOB_STATE_UPDATED` indicates that the job was - successfully updated, meaning that this job was stopped and another - job was started, inheriting state from this one. This is a terminal - job state. This state may only be set by the Cloud Dataflow service, - and only as a transition from `JOB_STATE_RUNNING`. - JOB_STATE_DRAINING: `JOB_STATE_DRAINING` indicates that the job is in - the process of draining. A draining job has stopped pulling from its - input sources and is processing any data that remains in-flight. This - state may be set via a Cloud Dataflow `UpdateJob` call, but only as a - transition from `JOB_STATE_RUNNING`. Jobs that are draining may only - transition to `JOB_STATE_DRAINED`, `JOB_STATE_CANCELLED`, or - `JOB_STATE_FAILED`. - JOB_STATE_DRAINED: `JOB_STATE_DRAINED` indicates that the job has been - drained. A drained job terminated by stopping pulling from its input - sources and processing any data that remained in-flight when draining - was requested. This state is a terminal state, may only be set by the - Cloud Dataflow service, and only as a transition from - `JOB_STATE_DRAINING`. - JOB_STATE_PENDING: `JOB_STATE_PENDING` indicates that the job has been - created but is not yet running. Jobs that are pending may only - transition to `JOB_STATE_RUNNING`, or `JOB_STATE_FAILED`. - JOB_STATE_CANCELLING: `JOB_STATE_CANCELLING` indicates that the job has - been explicitly cancelled and is in the process of stopping. Jobs that - are cancelling may only transition to `JOB_STATE_CANCELLED` or - `JOB_STATE_FAILED`. - JOB_STATE_QUEUED: `JOB_STATE_QUEUED` indicates that the job has been - created but is being delayed until launch. Jobs that are queued may - only transition to `JOB_STATE_PENDING` or `JOB_STATE_CANCELLED`. - JOB_STATE_RESOURCE_CLEANING_UP: `JOB_STATE_RESOURCE_CLEANING_UP` - indicates that the batch job's associated resources are currently - being cleaned up after a successful run. Currently, this is an opt-in - feature, please reach out to Cloud support team if you are interested. - JOB_STATE_PAUSING: `JOB_STATE_PAUSING` is not implemented yet. - JOB_STATE_PAUSED: `JOB_STATE_PAUSED` is not implemented yet. - """ - JOB_STATE_UNKNOWN = 0 - JOB_STATE_STOPPED = 1 - JOB_STATE_RUNNING = 2 - JOB_STATE_DONE = 3 - JOB_STATE_FAILED = 4 - JOB_STATE_CANCELLED = 5 - JOB_STATE_UPDATED = 6 - JOB_STATE_DRAINING = 7 - JOB_STATE_DRAINED = 8 - JOB_STATE_PENDING = 9 - JOB_STATE_CANCELLING = 10 - JOB_STATE_QUEUED = 11 - JOB_STATE_RESOURCE_CLEANING_UP = 12 - JOB_STATE_PAUSING = 13 - JOB_STATE_PAUSED = 14 - - class RequestedStateValueValuesEnum(_messages.Enum): - r"""The job's requested state. Applies to `UpdateJob` requests. Set - `requested_state` with `UpdateJob` requests to switch between the states - `JOB_STATE_STOPPED` and `JOB_STATE_RUNNING`. You can also use `UpdateJob` - requests to change a job's state from `JOB_STATE_RUNNING` to - `JOB_STATE_CANCELLED`, `JOB_STATE_DONE`, or `JOB_STATE_DRAINED`. These - states irrevocably terminate the job if it hasn't already reached a - terminal state. This field has no effect on `CreateJob` requests. - - Values: - JOB_STATE_UNKNOWN: The job's run state isn't specified. - JOB_STATE_STOPPED: `JOB_STATE_STOPPED` indicates that the job has not - yet started to run. - JOB_STATE_RUNNING: `JOB_STATE_RUNNING` indicates that the job is - currently running. - JOB_STATE_DONE: `JOB_STATE_DONE` indicates that the job has successfully - completed. This is a terminal job state. This state may be set by the - Cloud Dataflow service, as a transition from `JOB_STATE_RUNNING`. It - may also be set via a Cloud Dataflow `UpdateJob` call, if the job has - not yet reached a terminal state. - JOB_STATE_FAILED: `JOB_STATE_FAILED` indicates that the job has failed. - This is a terminal job state. This state may only be set by the Cloud - Dataflow service, and only as a transition from `JOB_STATE_RUNNING`. - JOB_STATE_CANCELLED: `JOB_STATE_CANCELLED` indicates that the job has - been explicitly cancelled. This is a terminal job state. This state - may only be set via a Cloud Dataflow `UpdateJob` call, and only if the - job has not yet reached another terminal state. - JOB_STATE_UPDATED: `JOB_STATE_UPDATED` indicates that the job was - successfully updated, meaning that this job was stopped and another - job was started, inheriting state from this one. This is a terminal - job state. This state may only be set by the Cloud Dataflow service, - and only as a transition from `JOB_STATE_RUNNING`. - JOB_STATE_DRAINING: `JOB_STATE_DRAINING` indicates that the job is in - the process of draining. A draining job has stopped pulling from its - input sources and is processing any data that remains in-flight. This - state may be set via a Cloud Dataflow `UpdateJob` call, but only as a - transition from `JOB_STATE_RUNNING`. Jobs that are draining may only - transition to `JOB_STATE_DRAINED`, `JOB_STATE_CANCELLED`, or - `JOB_STATE_FAILED`. - JOB_STATE_DRAINED: `JOB_STATE_DRAINED` indicates that the job has been - drained. A drained job terminated by stopping pulling from its input - sources and processing any data that remained in-flight when draining - was requested. This state is a terminal state, may only be set by the - Cloud Dataflow service, and only as a transition from - `JOB_STATE_DRAINING`. - JOB_STATE_PENDING: `JOB_STATE_PENDING` indicates that the job has been - created but is not yet running. Jobs that are pending may only - transition to `JOB_STATE_RUNNING`, or `JOB_STATE_FAILED`. - JOB_STATE_CANCELLING: `JOB_STATE_CANCELLING` indicates that the job has - been explicitly cancelled and is in the process of stopping. Jobs that - are cancelling may only transition to `JOB_STATE_CANCELLED` or - `JOB_STATE_FAILED`. - JOB_STATE_QUEUED: `JOB_STATE_QUEUED` indicates that the job has been - created but is being delayed until launch. Jobs that are queued may - only transition to `JOB_STATE_PENDING` or `JOB_STATE_CANCELLED`. - JOB_STATE_RESOURCE_CLEANING_UP: `JOB_STATE_RESOURCE_CLEANING_UP` - indicates that the batch job's associated resources are currently - being cleaned up after a successful run. Currently, this is an opt-in - feature, please reach out to Cloud support team if you are interested. - JOB_STATE_PAUSING: `JOB_STATE_PAUSING` is not implemented yet. - JOB_STATE_PAUSED: `JOB_STATE_PAUSED` is not implemented yet. - """ - JOB_STATE_UNKNOWN = 0 - JOB_STATE_STOPPED = 1 - JOB_STATE_RUNNING = 2 - JOB_STATE_DONE = 3 - JOB_STATE_FAILED = 4 - JOB_STATE_CANCELLED = 5 - JOB_STATE_UPDATED = 6 - JOB_STATE_DRAINING = 7 - JOB_STATE_DRAINED = 8 - JOB_STATE_PENDING = 9 - JOB_STATE_CANCELLING = 10 - JOB_STATE_QUEUED = 11 - JOB_STATE_RESOURCE_CLEANING_UP = 12 - JOB_STATE_PAUSING = 13 - JOB_STATE_PAUSED = 14 - - class TypeValueValuesEnum(_messages.Enum): - r"""Optional. The type of Dataflow job. - - Values: - JOB_TYPE_UNKNOWN: The type of the job is unspecified, or unknown. - JOB_TYPE_BATCH: A batch job with a well-defined end point: data is read, - data is processed, data is written, and the job is done. - JOB_TYPE_STREAMING: A continuously streaming job with no end: data is - read, processed, and written continuously. - """ - JOB_TYPE_UNKNOWN = 0 - JOB_TYPE_BATCH = 1 - JOB_TYPE_STREAMING = 2 - - @encoding.MapUnrecognizedFields('additionalProperties') - class LabelsValue(_messages.Message): - r"""User-defined labels for this job. The labels map can contain no more - than 64 entries. Entries of the labels map are UTF8 strings that comply - with the following restrictions: * Keys must conform to regexp: - \p{Ll}\p{Lo}{0,62} * Values must conform to regexp: - [\p{Ll}\p{Lo}\p{N}_-]{0,63} * Both keys and values are additionally - constrained to be <= 128 bytes in size. - - Messages: - AdditionalProperty: An additional property for a LabelsValue object. - - Fields: - additionalProperties: Additional properties of type LabelsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a LabelsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class TransformNameMappingValue(_messages.Message): - r"""Optional. The map of transform name prefixes of the job to be replaced - to the corresponding name prefixes of the new job. - - Messages: - AdditionalProperty: An additional property for a - TransformNameMappingValue object. - - Fields: - additionalProperties: Additional properties of type - TransformNameMappingValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a TransformNameMappingValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - clientRequestId = _messages.StringField(1) - createTime = _messages.StringField(2) - createdFromSnapshotId = _messages.StringField(3) - currentState = _messages.EnumField('CurrentStateValueValuesEnum', 4) - currentStateTime = _messages.StringField(5) - environment = _messages.MessageField('Environment', 6) - executionInfo = _messages.MessageField('JobExecutionInfo', 7) - id = _messages.StringField(8) - jobMetadata = _messages.MessageField('JobMetadata', 9) - labels = _messages.MessageField('LabelsValue', 10) - location = _messages.StringField(11) - name = _messages.StringField(12) - pausable = _messages.BooleanField(13) - pipelineDescription = _messages.MessageField('PipelineDescription', 14) - projectId = _messages.StringField(15) - replaceJobId = _messages.StringField(16) - replacedByJobId = _messages.StringField(17) - requestedState = _messages.EnumField('RequestedStateValueValuesEnum', 18) - runtimeUpdatableParams = _messages.MessageField('RuntimeUpdatableParams', 19) - satisfiesPzi = _messages.BooleanField(20) - satisfiesPzs = _messages.BooleanField(21) - serviceResources = _messages.MessageField('ServiceResources', 22) - stageStates = _messages.MessageField('ExecutionStageState', 23, repeated=True) - startTime = _messages.StringField(24) - steps = _messages.MessageField('Step', 25, repeated=True) - stepsLocation = _messages.StringField(26) - tempFiles = _messages.StringField(27, repeated=True) - transformNameMapping = _messages.MessageField('TransformNameMappingValue', 28) - type = _messages.EnumField('TypeValueValuesEnum', 29) - - -class JobExecutionDetails(_messages.Message): - r"""Information about the execution of a job. - - Fields: - nextPageToken: If present, this response does not contain all requested - tasks. To obtain the next page of results, repeat the request with - page_token set to this value. - stages: The stages of the job execution. - """ - - nextPageToken = _messages.StringField(1) - stages = _messages.MessageField('StageSummary', 2, repeated=True) - - -class JobExecutionInfo(_messages.Message): - r"""Additional information about how a Cloud Dataflow job will be executed - that isn't contained in the submitted job. - - Messages: - StagesValue: A mapping from each stage to the information about that - stage. - - Fields: - stages: A mapping from each stage to the information about that stage. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class StagesValue(_messages.Message): - r"""A mapping from each stage to the information about that stage. - - Messages: - AdditionalProperty: An additional property for a StagesValue object. - - Fields: - additionalProperties: Additional properties of type StagesValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a StagesValue object. - - Fields: - key: Name of the additional property. - value: A JobExecutionStageInfo attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('JobExecutionStageInfo', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - stages = _messages.MessageField('StagesValue', 1) - - -class JobExecutionStageInfo(_messages.Message): - r"""Contains information about how a particular google.dataflow.v1beta3.Step - will be executed. - - Fields: - stepName: The steps associated with the execution stage. Note that stages - may have several steps, and that a given step might be run by more than - one stage. - """ - - stepName = _messages.StringField(1, repeated=True) - - -class JobMessage(_messages.Message): - r"""A particular message pertaining to a Dataflow job. - - Enums: - MessageImportanceValueValuesEnum: Importance level of the message. - - Fields: - id: Deprecated. - messageImportance: Importance level of the message. - messageText: The text of the message. - time: The timestamp of the message. - """ - - class MessageImportanceValueValuesEnum(_messages.Enum): - r"""Importance level of the message. - - Values: - JOB_MESSAGE_IMPORTANCE_UNKNOWN: The message importance isn't specified, - or is unknown. - JOB_MESSAGE_DEBUG: The message is at the 'debug' level: typically only - useful for software engineers working on the code the job is running. - Typically, Dataflow pipeline runners do not display log messages at - this level by default. - JOB_MESSAGE_DETAILED: The message is at the 'detailed' level: somewhat - verbose, but potentially useful to users. Typically, Dataflow pipeline - runners do not display log messages at this level by default. These - messages are displayed by default in the Dataflow monitoring UI. - JOB_MESSAGE_BASIC: The message is at the 'basic' level: useful for - keeping track of the execution of a Dataflow pipeline. Typically, - Dataflow pipeline runners display log messages at this level by - default, and these messages are displayed by default in the Dataflow - monitoring UI. - JOB_MESSAGE_WARNING: The message is at the 'warning' level: indicating a - condition pertaining to a job which may require human intervention. - Typically, Dataflow pipeline runners display log messages at this - level by default, and these messages are displayed by default in the - Dataflow monitoring UI. - JOB_MESSAGE_ERROR: The message is at the 'error' level: indicating a - condition preventing a job from succeeding. Typically, Dataflow - pipeline runners display log messages at this level by default, and - these messages are displayed by default in the Dataflow monitoring UI. - """ - JOB_MESSAGE_IMPORTANCE_UNKNOWN = 0 - JOB_MESSAGE_DEBUG = 1 - JOB_MESSAGE_DETAILED = 2 - JOB_MESSAGE_BASIC = 3 - JOB_MESSAGE_WARNING = 4 - JOB_MESSAGE_ERROR = 5 - - id = _messages.StringField(1) - messageImportance = _messages.EnumField('MessageImportanceValueValuesEnum', 2) - messageText = _messages.StringField(3) - time = _messages.StringField(4) - - -class JobMetadata(_messages.Message): - r"""Metadata available primarily for filtering jobs. Will be included in the - ListJob response and Job SUMMARY view. - - Messages: - UserDisplayPropertiesValue: List of display properties to help UI filter - jobs. - - Fields: - bigTableDetails: Identification of a Cloud Bigtable source used in the - Dataflow job. - bigqueryDetails: Identification of a BigQuery source used in the Dataflow - job. - datastoreDetails: Identification of a Datastore source used in the - Dataflow job. - fileDetails: Identification of a File source used in the Dataflow job. - pubsubDetails: Identification of a Pub/Sub source used in the Dataflow - job. - sdkVersion: The SDK version used to run the job. - spannerDetails: Identification of a Spanner source used in the Dataflow - job. - userDisplayProperties: List of display properties to help UI filter jobs. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UserDisplayPropertiesValue(_messages.Message): - r"""List of display properties to help UI filter jobs. - - Messages: - AdditionalProperty: An additional property for a - UserDisplayPropertiesValue object. - - Fields: - additionalProperties: Additional properties of type - UserDisplayPropertiesValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UserDisplayPropertiesValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - bigTableDetails = _messages.MessageField('BigTableIODetails', 1, repeated=True) - bigqueryDetails = _messages.MessageField('BigQueryIODetails', 2, repeated=True) - datastoreDetails = _messages.MessageField('DatastoreIODetails', 3, repeated=True) - fileDetails = _messages.MessageField('FileIODetails', 4, repeated=True) - pubsubDetails = _messages.MessageField('PubSubIODetails', 5, repeated=True) - sdkVersion = _messages.MessageField('SdkVersion', 6) - spannerDetails = _messages.MessageField('SpannerIODetails', 7, repeated=True) - userDisplayProperties = _messages.MessageField('UserDisplayPropertiesValue', 8) - - -class JobMetrics(_messages.Message): - r"""JobMetrics contains a collection of metrics describing the detailed - progress of a Dataflow job. Metrics correspond to user-defined and system- - defined metrics in the job. For more information, see [Dataflow job metrics] - (https://cloud.google.com/dataflow/docs/guides/using-monitoring-intf). This - resource captures only the most recent values of each metric; time-series - data can be queried for them (under the same metric names) from Cloud - Monitoring. - - Fields: - metricTime: Timestamp as of which metric values are current. - metrics: All metrics for this job. - """ - - metricTime = _messages.StringField(1) - metrics = _messages.MessageField('MetricUpdate', 2, repeated=True) - - -class KeyRangeDataDiskAssignment(_messages.Message): - r"""Data disk assignment information for a specific key-range of a sharded - computation. Currently we only support UTF-8 character splits to simplify - encoding into JSON. - - Fields: - dataDisk: The name of the data disk where data for this range is stored. - This name is local to the Google Cloud Platform project and uniquely - identifies the disk within that project, for example - "myproject-1014-104817-4c2-harness-0-disk-1". - end: The end (exclusive) of the key range. - start: The start (inclusive) of the key range. - """ - - dataDisk = _messages.StringField(1) - end = _messages.StringField(2) - start = _messages.StringField(3) - - -class KeyRangeLocation(_messages.Message): - r"""Location information for a specific key-range of a sharded computation. - Currently we only support UTF-8 character splits to simplify encoding into - JSON. - - Fields: - dataDisk: The name of the data disk where data for this range is stored. - This name is local to the Google Cloud Platform project and uniquely - identifies the disk within that project, for example - "myproject-1014-104817-4c2-harness-0-disk-1". - deliveryEndpoint: The physical location of this range assignment to be - used for streaming computation cross-worker message delivery. - deprecatedPersistentDirectory: DEPRECATED. The location of the persistent - state for this range, as a persistent directory in the worker local - filesystem. - end: The end (exclusive) of the key range. - start: The start (inclusive) of the key range. - """ - - dataDisk = _messages.StringField(1) - deliveryEndpoint = _messages.StringField(2) - deprecatedPersistentDirectory = _messages.StringField(3) - end = _messages.StringField(4) - start = _messages.StringField(5) - - -class LaunchFlexTemplateParameter(_messages.Message): - r"""Launch FlexTemplate Parameter. - - Messages: - LaunchOptionsValue: Launch options for this flex template job. This is a - common set of options across languages and templates. This should not be - used to pass job parameters. - ParametersValue: The parameters for FlexTemplate. Ex. {"num_workers":"5"} - TransformNameMappingsValue: Use this to pass transform_name_mappings for - streaming update jobs. Ex:{"oldTransformName":"newTransformName",...}' - - Fields: - containerSpec: Spec about the container image to launch. - containerSpecGcsPath: Cloud Storage path to a file with json serialized - ContainerSpec as content. - environment: The runtime environment for the FlexTemplate job - jobName: Required. The job name to use for the created job. For update job - request, job name should be same as the existing running job. - launchOptions: Launch options for this flex template job. This is a common - set of options across languages and templates. This should not be used - to pass job parameters. - parameters: The parameters for FlexTemplate. Ex. {"num_workers":"5"} - transformNameMappings: Use this to pass transform_name_mappings for - streaming update jobs. Ex:{"oldTransformName":"newTransformName",...}' - update: Set this to true if you are sending a request to update a running - streaming job. When set, the job name should be the same as the running - job. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class LaunchOptionsValue(_messages.Message): - r"""Launch options for this flex template job. This is a common set of - options across languages and templates. This should not be used to pass - job parameters. - - Messages: - AdditionalProperty: An additional property for a LaunchOptionsValue - object. - - Fields: - additionalProperties: Additional properties of type LaunchOptionsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a LaunchOptionsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class ParametersValue(_messages.Message): - r"""The parameters for FlexTemplate. Ex. {"num_workers":"5"} - - Messages: - AdditionalProperty: An additional property for a ParametersValue object. - - Fields: - additionalProperties: Additional properties of type ParametersValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ParametersValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class TransformNameMappingsValue(_messages.Message): - r"""Use this to pass transform_name_mappings for streaming update jobs. - Ex:{"oldTransformName":"newTransformName",...}' - - Messages: - AdditionalProperty: An additional property for a - TransformNameMappingsValue object. - - Fields: - additionalProperties: Additional properties of type - TransformNameMappingsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a TransformNameMappingsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - containerSpec = _messages.MessageField('ContainerSpec', 1) - containerSpecGcsPath = _messages.StringField(2) - environment = _messages.MessageField('FlexTemplateRuntimeEnvironment', 3) - jobName = _messages.StringField(4) - launchOptions = _messages.MessageField('LaunchOptionsValue', 5) - parameters = _messages.MessageField('ParametersValue', 6) - transformNameMappings = _messages.MessageField('TransformNameMappingsValue', 7) - update = _messages.BooleanField(8) - - -class LaunchFlexTemplateRequest(_messages.Message): - r"""A request to launch a Cloud Dataflow job from a FlexTemplate. - - Fields: - launchParameter: Required. Parameter to launch a job form Flex Template. - validateOnly: If true, the request is validated but not actually executed. - Defaults to false. - """ - - launchParameter = _messages.MessageField('LaunchFlexTemplateParameter', 1) - validateOnly = _messages.BooleanField(2) - - -class LaunchFlexTemplateResponse(_messages.Message): - r"""Response to the request to launch a job from Flex Template. - - Fields: - job: The job that was launched, if the request was not a dry run and the - job was successfully launched. - """ - - job = _messages.MessageField('Job', 1) - - -class LaunchTemplateParameters(_messages.Message): - r"""Parameters to provide to the template being launched. Note that the - [metadata in the pipeline code] - (https://cloud.google.com/dataflow/docs/guides/templates/creating- - templates#metadata) determines which runtime parameters are valid. - - Messages: - ParametersValue: The runtime parameters to pass to the job. - TransformNameMappingValue: Only applicable when updating a pipeline. Map - of transform name prefixes of the job to be replaced to the - corresponding name prefixes of the new job. - - Fields: - environment: The runtime environment for the job. - jobName: Required. The job name to use for the created job. The name must - match the regular expression `[a-z]([-a-z0-9]{0,1022}[a-z0-9])?` - parameters: The runtime parameters to pass to the job. - transformNameMapping: Only applicable when updating a pipeline. Map of - transform name prefixes of the job to be replaced to the corresponding - name prefixes of the new job. - update: If set, replace the existing pipeline with the name specified by - jobName with this pipeline, preserving state. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class ParametersValue(_messages.Message): - r"""The runtime parameters to pass to the job. - - Messages: - AdditionalProperty: An additional property for a ParametersValue object. - - Fields: - additionalProperties: Additional properties of type ParametersValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ParametersValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class TransformNameMappingValue(_messages.Message): - r"""Only applicable when updating a pipeline. Map of transform name - prefixes of the job to be replaced to the corresponding name prefixes of - the new job. - - Messages: - AdditionalProperty: An additional property for a - TransformNameMappingValue object. - - Fields: - additionalProperties: Additional properties of type - TransformNameMappingValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a TransformNameMappingValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - environment = _messages.MessageField('RuntimeEnvironment', 1) - jobName = _messages.StringField(2) - parameters = _messages.MessageField('ParametersValue', 3) - transformNameMapping = _messages.MessageField('TransformNameMappingValue', 4) - update = _messages.BooleanField(5) - - -class LaunchTemplateResponse(_messages.Message): - r"""Response to the request to launch a template. - - Fields: - job: The job that was launched, if the request was not a dry run and the - job was successfully launched. - """ - - job = _messages.MessageField('Job', 1) - - -class LeaseWorkItemRequest(_messages.Message): - r"""Request to lease WorkItems. - - Messages: - UnifiedWorkerRequestValue: Untranslated bag-of-bytes WorkRequest from - UnifiedWorker. - - Fields: - currentWorkerTime: The current timestamp at the worker. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the WorkItem's job. - projectNumber: Optional. The project number of the project this worker - belongs to. - requestedLeaseDuration: The initial lease period. - unifiedWorkerRequest: Untranslated bag-of-bytes WorkRequest from - UnifiedWorker. - workItemTypes: Filter for WorkItem type. - workerCapabilities: Worker capabilities. WorkItems might be limited to - workers with specific capabilities. - workerId: Identifies the worker leasing work -- typically the ID of the - virtual machine running the worker. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UnifiedWorkerRequestValue(_messages.Message): - r"""Untranslated bag-of-bytes WorkRequest from UnifiedWorker. - - Messages: - AdditionalProperty: An additional property for a - UnifiedWorkerRequestValue object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UnifiedWorkerRequestValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - currentWorkerTime = _messages.StringField(1) - location = _messages.StringField(2) - projectNumber = _messages.IntegerField(3) - requestedLeaseDuration = _messages.StringField(4) - unifiedWorkerRequest = _messages.MessageField('UnifiedWorkerRequestValue', 5) - workItemTypes = _messages.StringField(6, repeated=True) - workerCapabilities = _messages.StringField(7, repeated=True) - workerId = _messages.StringField(8) - - -class LeaseWorkItemResponse(_messages.Message): - r"""Response to a request to lease WorkItems. - - Messages: - UnifiedWorkerResponseValue: Untranslated bag-of-bytes WorkResponse for - UnifiedWorker. - - Fields: - unifiedWorkerResponse: Untranslated bag-of-bytes WorkResponse for - UnifiedWorker. - workItems: A list of the leased WorkItems. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UnifiedWorkerResponseValue(_messages.Message): - r"""Untranslated bag-of-bytes WorkResponse for UnifiedWorker. - - Messages: - AdditionalProperty: An additional property for a - UnifiedWorkerResponseValue object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UnifiedWorkerResponseValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - unifiedWorkerResponse = _messages.MessageField('UnifiedWorkerResponseValue', 1) - workItems = _messages.MessageField('WorkItem', 2, repeated=True) - - -class Linear(_messages.Message): - r"""Linear buckets with the following boundaries for indices in 0 to n-1. - - i in [0, n-1]: [start + (i)*width, start + (i+1)*width) - - Fields: - numberOfBuckets: Must be greater than 0. - start: Lower bound of the first bucket. - width: Distance between bucket boundaries. Must be greater than 0. - """ - - numberOfBuckets = _messages.IntegerField(1, variant=_messages.Variant.INT32) - start = _messages.FloatField(2) - width = _messages.FloatField(3) - - -class ListJobMessagesResponse(_messages.Message): - r"""Response to a request to list job messages. - - Fields: - autoscalingEvents: Autoscaling events in ascending timestamp order. - jobMessages: Messages in ascending timestamp order. - nextPageToken: The token to obtain the next page of results if there are - more. - """ - - autoscalingEvents = _messages.MessageField('AutoscalingEvent', 1, repeated=True) - jobMessages = _messages.MessageField('JobMessage', 2, repeated=True) - nextPageToken = _messages.StringField(3) - - -class ListJobsResponse(_messages.Message): - r"""Response to a request to list Cloud Dataflow jobs in a project. This - might be a partial response, depending on the page size in the - ListJobsRequest. However, if the project does not have any jobs, an instance - of ListJobsResponse is not returned and the requests's response body is - empty {}. - - Fields: - failedLocation: Zero or more messages describing the [regional endpoints] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that failed to respond. - jobs: A subset of the requested job information. - nextPageToken: Set if there may be more results than fit in this response. - """ - - failedLocation = _messages.MessageField('FailedLocation', 1, repeated=True) - jobs = _messages.MessageField('Job', 2, repeated=True) - nextPageToken = _messages.StringField(3) - - -class ListSnapshotsResponse(_messages.Message): - r"""List of snapshots. - - Fields: - snapshots: Returned snapshots. - """ - - snapshots = _messages.MessageField('Snapshot', 1, repeated=True) - - -class MapTask(_messages.Message): - r"""MapTask consists of an ordered set of instructions, each of which - describes one particular low-level operation for the worker to perform in - order to accomplish the MapTask's WorkItem. Each instruction must appear in - the list before any instructions which depends on its output. - - Fields: - counterPrefix: Counter prefix that can be used to prefix counters. Not - currently used in Dataflow. - instructions: The instructions in the MapTask. - stageName: System-defined name of the stage containing this MapTask. - Unique across the workflow. - systemName: System-defined name of this MapTask. Unique across the - workflow. - """ - - counterPrefix = _messages.StringField(1) - instructions = _messages.MessageField('ParallelInstruction', 2, repeated=True) - stageName = _messages.StringField(3) - systemName = _messages.StringField(4) - - -class MemInfo(_messages.Message): - r"""Information about the memory usage of a worker or a container within a - worker. - - Fields: - currentLimitBytes: Instantenous memory limit in bytes. - currentOoms: Number of Out of Memory (OOM) events recorded since the - previous measurement. - currentRssBytes: Instantenous memory (RSS) size in bytes. - timestamp: Timestamp of the measurement. - totalGbMs: Total memory (RSS) usage since start up in GB * ms. - """ - - currentLimitBytes = _messages.IntegerField(1, variant=_messages.Variant.UINT64) - currentOoms = _messages.IntegerField(2) - currentRssBytes = _messages.IntegerField(3, variant=_messages.Variant.UINT64) - timestamp = _messages.StringField(4) - totalGbMs = _messages.IntegerField(5, variant=_messages.Variant.UINT64) - - -class MetricShortId(_messages.Message): - r"""The metric short id is returned to the user alongside an offset into - ReportWorkItemStatusRequest - - Fields: - metricIndex: The index of the corresponding metric in the - ReportWorkItemStatusRequest. Required. - shortId: The service-generated short identifier for the metric. - """ - - metricIndex = _messages.IntegerField(1, variant=_messages.Variant.INT32) - shortId = _messages.IntegerField(2) - - -class MetricStructuredName(_messages.Message): - r"""Identifies a metric, by describing the source which generated the - metric. - - Messages: - ContextValue: Zero or more labeled fields which identify the part of the - job this metric is associated with, such as the name of a step or - collection. For example, built-in counters associated with steps will - have context['step'] = . Counters associated with PCollections in the - SDK will have context['pcollection'] = . - - Fields: - context: Zero or more labeled fields which identify the part of the job - this metric is associated with, such as the name of a step or - collection. For example, built-in counters associated with steps will - have context['step'] = . Counters associated with PCollections in the - SDK will have context['pcollection'] = . - name: Worker-defined metric name. - origin: Origin (namespace) of metric name. May be blank for user-define - metrics; will be "dataflow" for metrics defined by the Dataflow service - or SDK. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class ContextValue(_messages.Message): - r"""Zero or more labeled fields which identify the part of the job this - metric is associated with, such as the name of a step or collection. For - example, built-in counters associated with steps will have context['step'] - = . Counters associated with PCollections in the SDK will have - context['pcollection'] = . - - Messages: - AdditionalProperty: An additional property for a ContextValue object. - - Fields: - additionalProperties: Additional properties of type ContextValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ContextValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - context = _messages.MessageField('ContextValue', 1) - name = _messages.StringField(2) - origin = _messages.StringField(3) - - -class MetricUpdate(_messages.Message): - r"""Describes the state of a metric. - - Fields: - boundedTrie: Worker-computed aggregate value for the "Trie" aggregation - kind. The only possible value type is a BoundedTrieNode. Introduced this - field to avoid breaking older SDKs when Dataflow service starts to - populate the `bounded_trie` field. - cumulative: True if this metric is reported as the total cumulative - aggregate value accumulated since the worker started working on this - WorkItem. By default this is false, indicating that this metric is - reported as a delta that is not associated with any WorkItem. - distribution: A struct value describing properties of a distribution of - numeric values. - gauge: A struct value describing properties of a Gauge. Metrics of gauge - type show the value of a metric across time, and is aggregated based on - the newest value. - internal: Worker-computed aggregate value for internal use by the Dataflow - service. - kind: Metric aggregation kind. The possible metric aggregation kinds are - "Sum", "Max", "Min", "Mean", "Set", "And", "Or", and "Distribution". The - specified aggregation kind is case-insensitive. If omitted, this is not - an aggregated value but instead a single metric sample value. - meanCount: Worker-computed aggregate value for the "Mean" aggregation - kind. This holds the count of the aggregated values and is used in - combination with mean_sum above to obtain the actual mean aggregate - value. The only possible value type is Long. - meanSum: Worker-computed aggregate value for the "Mean" aggregation kind. - This holds the sum of the aggregated values and is used in combination - with mean_count below to obtain the actual mean aggregate value. The - only possible value types are Long and Double. - name: Name of the metric. - scalar: Worker-computed aggregate value for aggregation kinds "Sum", - "Max", "Min", "And", and "Or". The possible value types are Long, - Double, and Boolean. - set: Worker-computed aggregate value for the "Set" aggregation kind. The - only possible value type is a list of Values whose type can be Long, - Double, String, or BoundedTrie according to the metric's type. All - Values in the list must be of the same type. - trie: Worker-computed aggregate value for the "Trie" aggregation kind. The - only possible value type is a BoundedTrieNode. - updateTime: Timestamp associated with the metric value. Optional when - workers are reporting work progress; it will be filled in responses from - the metrics API. - """ - - boundedTrie = _messages.MessageField('extra_types.JsonValue', 1) - cumulative = _messages.BooleanField(2) - distribution = _messages.MessageField('extra_types.JsonValue', 3) - gauge = _messages.MessageField('extra_types.JsonValue', 4) - internal = _messages.MessageField('extra_types.JsonValue', 5) - kind = _messages.StringField(6) - meanCount = _messages.MessageField('extra_types.JsonValue', 7) - meanSum = _messages.MessageField('extra_types.JsonValue', 8) - name = _messages.MessageField('MetricStructuredName', 9) - scalar = _messages.MessageField('extra_types.JsonValue', 10) - set = _messages.MessageField('extra_types.JsonValue', 11) - trie = _messages.MessageField('extra_types.JsonValue', 12) - updateTime = _messages.StringField(13) - - -class MetricValue(_messages.Message): - r"""The value of a metric along with its name and labels. - - Messages: - MetricLabelsValue: Optional. Set of metric labels for this metric. - - Fields: - metric: Base name for this metric. - metricLabels: Optional. Set of metric labels for this metric. - valueGauge64: Non-cumulative int64 value of this metric. - valueHistogram: Histogram value of this metric. - valueInt64: Integer value of this metric. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class MetricLabelsValue(_messages.Message): - r"""Optional. Set of metric labels for this metric. - - Messages: - AdditionalProperty: An additional property for a MetricLabelsValue - object. - - Fields: - additionalProperties: Additional properties of type MetricLabelsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a MetricLabelsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - metric = _messages.StringField(1) - metricLabels = _messages.MessageField('MetricLabelsValue', 2) - valueGauge64 = _messages.MessageField('DataflowGaugeValue', 3) - valueHistogram = _messages.MessageField('DataflowHistogramValue', 4) - valueInt64 = _messages.IntegerField(5) - - -class MountedDataDisk(_messages.Message): - r"""Describes mounted data disk. - - Fields: - dataDisk: The name of the data disk. This name is local to the Google - Cloud Platform project and uniquely identifies the disk within that - project, for example "myproject-1014-104817-4c2-harness-0-disk-1". - """ - - dataDisk = _messages.StringField(1) - - -class MultiOutputInfo(_messages.Message): - r"""Information about an output of a multi-output DoFn. - - Fields: - tag: The id of the tag the user code will emit to this output by; this - should correspond to the tag of some SideInputInfo. - """ - - tag = _messages.StringField(1) - - -class NameAndKind(_messages.Message): - r"""Basic metadata about a counter. - - Enums: - KindValueValuesEnum: Counter aggregation kind. - - Fields: - kind: Counter aggregation kind. - name: Name of the counter. - """ - - class KindValueValuesEnum(_messages.Enum): - r"""Counter aggregation kind. - - Values: - INVALID: Counter aggregation kind was not set. - SUM: Aggregated value is the sum of all contributed values. - MAX: Aggregated value is the max of all contributed values. - MIN: Aggregated value is the min of all contributed values. - MEAN: Aggregated value is the mean of all contributed values. - OR: Aggregated value represents the logical 'or' of all contributed - values. - AND: Aggregated value represents the logical 'and' of all contributed - values. - SET: Aggregated value is a set of unique contributed values. - DISTRIBUTION: Aggregated value captures statistics about a distribution. - LATEST_VALUE: Aggregated value tracks the latest value of a variable. - """ - INVALID = 0 - SUM = 1 - MAX = 2 - MIN = 3 - MEAN = 4 - OR = 5 - AND = 6 - SET = 7 - DISTRIBUTION = 8 - LATEST_VALUE = 9 - - kind = _messages.EnumField('KindValueValuesEnum', 1) - name = _messages.StringField(2) - - -class OutlierStats(_messages.Message): - r"""Statistics for the underflow and overflow bucket. - - Fields: - overflowCount: Number of values that are larger than the upper bound of - the largest bucket. - overflowMean: Mean of values in the overflow bucket. - underflowCount: Number of values that are smaller than the lower bound of - the smallest bucket. - underflowMean: Mean of values in the undeflow bucket. - """ - - overflowCount = _messages.IntegerField(1) - overflowMean = _messages.FloatField(2) - underflowCount = _messages.IntegerField(3) - underflowMean = _messages.FloatField(4) - - -class Package(_messages.Message): - r"""The packages that must be installed in order for a worker to run the - steps of the Cloud Dataflow job that will be assigned to its worker pool. - This is the mechanism by which the Cloud Dataflow SDK causes code to be - loaded onto the workers. For example, the Cloud Dataflow Java SDK might use - this to install jars containing the user's code and all of the various - dependencies (libraries, data files, etc.) required in order for that code - to run. - - Fields: - location: The resource to read the package from. The supported resource - type is: Google Cloud Storage: storage.googleapis.com/{bucket} - bucket.storage.googleapis.com/ - name: The name of the package. - sha256: Optional. The hex-encoded SHA256 checksum of the package. If the - checksum is provided, the worker will verify the checksum of the package - before using it. If the checksum does not match, the worker will fail to - start. - """ - - location = _messages.StringField(1) - name = _messages.StringField(2) - sha256 = _messages.StringField(3) - - -class ParDoInstruction(_messages.Message): - r"""An instruction that does a ParDo operation. Takes one main input and - zero or more side inputs, and produces zero or more outputs. Runs user code. - - Messages: - UserFnValue: The user function to invoke. - - Fields: - input: The input. - multiOutputInfos: Information about each of the outputs, if user_fn is a - MultiDoFn. - numOutputs: The number of outputs. - sideInputs: Zero or more side inputs. - userFn: The user function to invoke. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UserFnValue(_messages.Message): - r"""The user function to invoke. - - Messages: - AdditionalProperty: An additional property for a UserFnValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UserFnValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - input = _messages.MessageField('InstructionInput', 1) - multiOutputInfos = _messages.MessageField('MultiOutputInfo', 2, repeated=True) - numOutputs = _messages.IntegerField(3, variant=_messages.Variant.INT32) - sideInputs = _messages.MessageField('SideInputInfo', 4, repeated=True) - userFn = _messages.MessageField('UserFnValue', 5) - - -class ParallelInstruction(_messages.Message): - r"""Describes a particular operation comprising a MapTask. - - Fields: - flatten: Additional information for Flatten instructions. - name: User-provided name of this operation. - originalName: System-defined name for the operation in the original - workflow graph. - outputs: Describes the outputs of the instruction. - parDo: Additional information for ParDo instructions. - partialGroupByKey: Additional information for PartialGroupByKey - instructions. - read: Additional information for Read instructions. - systemName: System-defined name of this operation. Unique across the - workflow. - write: Additional information for Write instructions. - """ - - flatten = _messages.MessageField('FlattenInstruction', 1) - name = _messages.StringField(2) - originalName = _messages.StringField(3) - outputs = _messages.MessageField('InstructionOutput', 4, repeated=True) - parDo = _messages.MessageField('ParDoInstruction', 5) - partialGroupByKey = _messages.MessageField('PartialGroupByKeyInstruction', 6) - read = _messages.MessageField('ReadInstruction', 7) - systemName = _messages.StringField(8) - write = _messages.MessageField('WriteInstruction', 9) - - -class Parameter(_messages.Message): - r"""Structured data associated with this message. - - Fields: - key: Key or name for this parameter. - value: Value for this parameter. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - -class ParameterMetadata(_messages.Message): - r"""Metadata for a specific parameter. - - Enums: - ParamTypeValueValuesEnum: Optional. The type of the parameter. Used for - selecting input picker. - - Messages: - CustomMetadataValue: Optional. Additional metadata for describing this - parameter. - - Fields: - customMetadata: Optional. Additional metadata for describing this - parameter. - defaultValue: Optional. The default values will pre-populate the parameter - with the given value from the proto. If default_value is left empty, the - parameter will be populated with a default of the relevant type, e.g. - false for a boolean. - enumOptions: Optional. The options shown when ENUM ParameterType is - specified. - groupName: Optional. Specifies a group name for this parameter to be - rendered under. Group header text will be rendered exactly as specified - in this field. Only considered when parent_name is NOT provided. - helpText: Required. The help text to display for the parameter. - hiddenUi: Optional. Whether the parameter should be hidden in the UI. - isOptional: Optional. Whether the parameter is optional. Defaults to - false. - label: Required. The label to display for the parameter. - name: Required. The name of the parameter. - paramType: Optional. The type of the parameter. Used for selecting input - picker. - parentName: Optional. Specifies the name of the parent parameter. Used in - conjunction with 'parent_trigger_values' to make this parameter - conditional (will only be rendered conditionally). Should be mappable to - a ParameterMetadata.name field. - parentTriggerValues: Optional. The value(s) of the 'parent_name' parameter - which will trigger this parameter to be shown. If left empty, ANY non- - empty value in parent_name will trigger this parameter to be shown. Only - considered when this parameter is conditional (when 'parent_name' has - been provided). - regexes: Optional. Regexes that the parameter must match. - """ - - class ParamTypeValueValuesEnum(_messages.Enum): - r"""Optional. The type of the parameter. Used for selecting input picker. - - Values: - DEFAULT: Default input type. - TEXT: The parameter specifies generic text input. - GCS_READ_BUCKET: The parameter specifies a Cloud Storage Bucket to read - from. - GCS_WRITE_BUCKET: The parameter specifies a Cloud Storage Bucket to - write to. - GCS_READ_FILE: The parameter specifies a Cloud Storage file path to read - from. - GCS_WRITE_FILE: The parameter specifies a Cloud Storage file path to - write to. - GCS_READ_FOLDER: The parameter specifies a Cloud Storage folder path to - read from. - GCS_WRITE_FOLDER: The parameter specifies a Cloud Storage folder to - write to. - PUBSUB_TOPIC: The parameter specifies a Pub/Sub Topic. - PUBSUB_SUBSCRIPTION: The parameter specifies a Pub/Sub Subscription. - BIGQUERY_TABLE: The parameter specifies a BigQuery table. - JAVASCRIPT_UDF_FILE: The parameter specifies a JavaScript UDF in Cloud - Storage. - SERVICE_ACCOUNT: The parameter specifies a Service Account email. - MACHINE_TYPE: The parameter specifies a Machine Type. - KMS_KEY_NAME: The parameter specifies a KMS Key name. - WORKER_REGION: The parameter specifies a Worker Region. - WORKER_ZONE: The parameter specifies a Worker Zone. - BOOLEAN: The parameter specifies a boolean input. - ENUM: The parameter specifies an enum input. - NUMBER: The parameter specifies a number input. - KAFKA_TOPIC: Deprecated. Please use KAFKA_READ_TOPIC instead. - KAFKA_READ_TOPIC: The parameter specifies the fully-qualified name of an - Apache Kafka topic. This can be either a Google Managed Kafka topic or - a non-managed Kafka topic. - KAFKA_WRITE_TOPIC: The parameter specifies the fully-qualified name of - an Apache Kafka topic. This can be an existing Google Managed Kafka - topic, the name for a new Google Managed Kafka topic, or an existing - non-managed Kafka topic. - """ - DEFAULT = 0 - TEXT = 1 - GCS_READ_BUCKET = 2 - GCS_WRITE_BUCKET = 3 - GCS_READ_FILE = 4 - GCS_WRITE_FILE = 5 - GCS_READ_FOLDER = 6 - GCS_WRITE_FOLDER = 7 - PUBSUB_TOPIC = 8 - PUBSUB_SUBSCRIPTION = 9 - BIGQUERY_TABLE = 10 - JAVASCRIPT_UDF_FILE = 11 - SERVICE_ACCOUNT = 12 - MACHINE_TYPE = 13 - KMS_KEY_NAME = 14 - WORKER_REGION = 15 - WORKER_ZONE = 16 - BOOLEAN = 17 - ENUM = 18 - NUMBER = 19 - KAFKA_TOPIC = 20 - KAFKA_READ_TOPIC = 21 - KAFKA_WRITE_TOPIC = 22 - - @encoding.MapUnrecognizedFields('additionalProperties') - class CustomMetadataValue(_messages.Message): - r"""Optional. Additional metadata for describing this parameter. - - Messages: - AdditionalProperty: An additional property for a CustomMetadataValue - object. - - Fields: - additionalProperties: Additional properties of type CustomMetadataValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a CustomMetadataValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - customMetadata = _messages.MessageField('CustomMetadataValue', 1) - defaultValue = _messages.StringField(2) - enumOptions = _messages.MessageField('ParameterMetadataEnumOption', 3, repeated=True) - groupName = _messages.StringField(4) - helpText = _messages.StringField(5) - hiddenUi = _messages.BooleanField(6) - isOptional = _messages.BooleanField(7) - label = _messages.StringField(8) - name = _messages.StringField(9) - paramType = _messages.EnumField('ParamTypeValueValuesEnum', 10) - parentName = _messages.StringField(11) - parentTriggerValues = _messages.StringField(12, repeated=True) - regexes = _messages.StringField(13, repeated=True) - - -class ParameterMetadataEnumOption(_messages.Message): - r"""ParameterMetadataEnumOption specifies the option shown in the enum form. - - Fields: - description: Optional. The description to display for the enum option. - label: Optional. The label to display for the enum option. - value: Required. The value of the enum option. - """ - - description = _messages.StringField(1) - label = _messages.StringField(2) - value = _messages.StringField(3) - - -class PartialGroupByKeyInstruction(_messages.Message): - r"""An instruction that does a partial group-by-key. One input and one - output. - - Messages: - InputElementCodecValue: The codec to use for interpreting an element in - the input PTable. - ValueCombiningFnValue: The value combining function to invoke. - - Fields: - input: Describes the input to the partial group-by-key instruction. - inputElementCodec: The codec to use for interpreting an element in the - input PTable. - originalCombineValuesInputStoreName: If this instruction includes a - combining function this is the name of the intermediate store between - the GBK and the CombineValues. - originalCombineValuesStepName: If this instruction includes a combining - function, this is the name of the CombineValues instruction lifted into - this instruction. - sideInputs: Zero or more side inputs. - valueCombiningFn: The value combining function to invoke. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class InputElementCodecValue(_messages.Message): - r"""The codec to use for interpreting an element in the input PTable. - - Messages: - AdditionalProperty: An additional property for a InputElementCodecValue - object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a InputElementCodecValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class ValueCombiningFnValue(_messages.Message): - r"""The value combining function to invoke. - - Messages: - AdditionalProperty: An additional property for a ValueCombiningFnValue - object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ValueCombiningFnValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - input = _messages.MessageField('InstructionInput', 1) - inputElementCodec = _messages.MessageField('InputElementCodecValue', 2) - originalCombineValuesInputStoreName = _messages.StringField(3) - originalCombineValuesStepName = _messages.StringField(4) - sideInputs = _messages.MessageField('SideInputInfo', 5, repeated=True) - valueCombiningFn = _messages.MessageField('ValueCombiningFnValue', 6) - - -class PerStepNamespaceMetrics(_messages.Message): - r"""Metrics for a particular unfused step and namespace. A metric is - uniquely identified by the `metrics_namespace`, `original_step`, `metric - name` and `metric_labels`. - - Fields: - metricValues: Optional. Metrics that are recorded for this namespace and - unfused step. - metricsNamespace: The namespace of these metrics on the worker. - originalStep: The original system name of the unfused step that these - metrics are reported from. - """ - - metricValues = _messages.MessageField('MetricValue', 1, repeated=True) - metricsNamespace = _messages.StringField(2) - originalStep = _messages.StringField(3) - - -class PerWorkerMetrics(_messages.Message): - r"""Per worker metrics. - - Fields: - perStepNamespaceMetrics: Optional. Metrics for a particular unfused step - and namespace. - """ - - perStepNamespaceMetrics = _messages.MessageField('PerStepNamespaceMetrics', 1, repeated=True) - - -class PipelineDescription(_messages.Message): - r"""A descriptive representation of submitted pipeline as well as the - executed form. This data is provided by the Dataflow service for ease of - visualizing the pipeline and interpreting Dataflow provided metrics. - - Fields: - displayData: Pipeline level display data. - executionPipelineStage: Description of each stage of execution of the - pipeline. - originalPipelineTransform: Description of each transform in the pipeline - and collections between them. - stepNamesHash: A hash value of the submitted pipeline portable graph step - names if exists. - """ - - displayData = _messages.MessageField('DisplayData', 1, repeated=True) - executionPipelineStage = _messages.MessageField('ExecutionStageSummary', 2, repeated=True) - originalPipelineTransform = _messages.MessageField('TransformSummary', 3, repeated=True) - stepNamesHash = _messages.StringField(4) - - -class Point(_messages.Message): - r"""A point in the timeseries. - - Fields: - time: The timestamp of the point. - value: The value of the point. - """ - - time = _messages.StringField(1) - value = _messages.FloatField(2) - - -class Position(_messages.Message): - r"""Position defines a position within a collection of data. The value can - be either the end position, a key (used with ordered collections), a byte - offset, or a record index. - - Fields: - byteOffset: Position is a byte offset. - concatPosition: CloudPosition is a concat position. - end: Position is past all other positions. Also useful for the end - position of an unbounded range. - key: Position is a string key, ordered lexicographically. - recordIndex: Position is a record index. - shufflePosition: CloudPosition is a base64 encoded BatchShufflePosition - (with FIXED sharding). - """ - - byteOffset = _messages.IntegerField(1) - concatPosition = _messages.MessageField('ConcatPosition', 2) - end = _messages.BooleanField(3) - key = _messages.StringField(4) - recordIndex = _messages.IntegerField(5) - shufflePosition = _messages.StringField(6) - - -class ProgressTimeseries(_messages.Message): - r"""Information about the progress of some component of job execution. - - Fields: - currentProgress: The current progress of the component, in the range - [0,1]. - dataPoints: History of progress for the component. Points are sorted by - time. - """ - - currentProgress = _messages.FloatField(1) - dataPoints = _messages.MessageField('Point', 2, repeated=True) - - -class PubSubIODetails(_messages.Message): - r"""Metadata for a Pub/Sub connector used by the job. - - Fields: - subscription: Subscription used in the connection. - topic: Topic accessed in the connection. - """ - - subscription = _messages.StringField(1) - topic = _messages.StringField(2) - - -class PubsubLocation(_messages.Message): - r"""Identifies a pubsub location to use for transferring data into or out of - a streaming Dataflow job. - - Fields: - dropLateData: Indicates whether the pipeline allows late-arriving data. - dynamicDestinations: If true, then this location represents dynamic - topics. - idLabel: If set, contains a pubsub label from which to extract record ids. - If left empty, record deduplication will be strictly best effort. - subscription: A pubsub subscription, in the form of - "pubsub.googleapis.com/subscriptions//" - timestampLabel: If set, contains a pubsub label from which to extract - record timestamps. If left empty, record timestamps will be generated - upon arrival. - topic: A pubsub topic, in the form of "pubsub.googleapis.com/topics//" - trackingSubscription: If set, specifies the pubsub subscription that will - be used for tracking custom time timestamps for watermark estimation. - withAttributes: If true, then the client has requested to get pubsub - attributes. - """ - - dropLateData = _messages.BooleanField(1) - dynamicDestinations = _messages.BooleanField(2) - idLabel = _messages.StringField(3) - subscription = _messages.StringField(4) - timestampLabel = _messages.StringField(5) - topic = _messages.StringField(6) - trackingSubscription = _messages.StringField(7) - withAttributes = _messages.BooleanField(8) - - -class PubsubSnapshotMetadata(_messages.Message): - r"""Represents a Pubsub snapshot. - - Fields: - expireTime: The expire time of the Pubsub snapshot. - snapshotName: The name of the Pubsub snapshot. - topicName: The name of the Pubsub topic. - """ - - expireTime = _messages.StringField(1) - snapshotName = _messages.StringField(2) - topicName = _messages.StringField(3) - - -class ReadInstruction(_messages.Message): - r"""An instruction that reads records. Takes no inputs, produces one output. - - Fields: - source: The source to read from. - """ - - source = _messages.MessageField('Source', 1) - - -class ReportWorkItemStatusRequest(_messages.Message): - r"""Request to report the status of WorkItems. - - Messages: - UnifiedWorkerRequestValue: Untranslated bag-of-bytes - WorkProgressUpdateRequest from UnifiedWorker. - - Fields: - currentWorkerTime: The current timestamp at the worker. - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the WorkItem's job. - projectNumber: Optional. The project number of the project which owns the - WorkItem's job. - unifiedWorkerRequest: Untranslated bag-of-bytes WorkProgressUpdateRequest - from UnifiedWorker. - workItemStatuses: The order is unimportant, except that the order of the - WorkItemServiceState messages in the ReportWorkItemStatusResponse - corresponds to the order of WorkItemStatus messages here. - workerId: The ID of the worker reporting the WorkItem status. If this does - not match the ID of the worker which the Dataflow service believes - currently has the lease on the WorkItem, the report will be dropped - (with an error response). - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UnifiedWorkerRequestValue(_messages.Message): - r"""Untranslated bag-of-bytes WorkProgressUpdateRequest from - UnifiedWorker. - - Messages: - AdditionalProperty: An additional property for a - UnifiedWorkerRequestValue object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UnifiedWorkerRequestValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - currentWorkerTime = _messages.StringField(1) - location = _messages.StringField(2) - projectNumber = _messages.IntegerField(3) - unifiedWorkerRequest = _messages.MessageField('UnifiedWorkerRequestValue', 4) - workItemStatuses = _messages.MessageField('WorkItemStatus', 5, repeated=True) - workerId = _messages.StringField(6) - - -class ReportWorkItemStatusResponse(_messages.Message): - r"""Response from a request to report the status of WorkItems. - - Messages: - UnifiedWorkerResponseValue: Untranslated bag-of-bytes - WorkProgressUpdateResponse for UnifiedWorker. - - Fields: - unifiedWorkerResponse: Untranslated bag-of-bytes - WorkProgressUpdateResponse for UnifiedWorker. - workItemServiceStates: A set of messages indicating the service-side state - for each WorkItem whose status was reported, in the same order as the - WorkItemStatus messages in the ReportWorkItemStatusRequest which - resulting in this response. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UnifiedWorkerResponseValue(_messages.Message): - r"""Untranslated bag-of-bytes WorkProgressUpdateResponse for - UnifiedWorker. - - Messages: - AdditionalProperty: An additional property for a - UnifiedWorkerResponseValue object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UnifiedWorkerResponseValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - unifiedWorkerResponse = _messages.MessageField('UnifiedWorkerResponseValue', 1) - workItemServiceStates = _messages.MessageField('WorkItemServiceState', 2, repeated=True) - - -class ReportedParallelism(_messages.Message): - r"""Represents the level of parallelism in a WorkItem's input, reported by - the worker. - - Fields: - isInfinite: Specifies whether the parallelism is infinite. If true, - "value" is ignored. Infinite parallelism means the service will assume - that the work item can always be split into more non-empty work items by - dynamic splitting. This is a work-around for lack of support for - infinity by the current JSON-based Java RPC stack. - value: Specifies the level of parallelism in case it is finite. - """ - - isInfinite = _messages.BooleanField(1) - value = _messages.FloatField(2) - - -class ResourceUtilizationReport(_messages.Message): - r"""Worker metrics exported from workers. This contains resource utilization - metrics accumulated from a variety of sources. For more information, see - go/df-resource-signals. - - Messages: - ContainersValue: Per container information. Key: container name. - - Fields: - containers: Per container information. Key: container name. - cpuTime: CPU utilization samples. - gpuUsage: Optional. GPU usage samples. - memoryInfo: Memory utilization samples. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class ContainersValue(_messages.Message): - r"""Per container information. Key: container name. - - Messages: - AdditionalProperty: An additional property for a ContainersValue object. - - Fields: - additionalProperties: Additional properties of type ContainersValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ContainersValue object. - - Fields: - key: Name of the additional property. - value: A ResourceUtilizationReport attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('ResourceUtilizationReport', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - containers = _messages.MessageField('ContainersValue', 1) - cpuTime = _messages.MessageField('CPUTime', 2, repeated=True) - gpuUsage = _messages.MessageField('GPUUsage', 3, repeated=True) - memoryInfo = _messages.MessageField('MemInfo', 4, repeated=True) - - -class ResourceUtilizationReportResponse(_messages.Message): - r"""Service-side response to WorkerMessage reporting resource utilization. - """ - - - -class RuntimeEnvironment(_messages.Message): - r"""The environment values to set at runtime. - - Enums: - IpConfigurationValueValuesEnum: Optional. Configuration for VM IPs. - StreamingModeValueValuesEnum: Optional. Specifies the Streaming Engine - message processing guarantees. Reduces cost and latency but might result - in duplicate messages committed to storage. Designed to run simple - mapping streaming ETL jobs at the lowest cost. For example, Change Data - Capture (CDC) to BigQuery is a canonical use case. For more information, - see [Set the pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - - Messages: - AdditionalUserLabelsValue: Optional. Additional user labels to be - specified for the job. Keys and values should follow the restrictions - specified in the [labeling - restrictions](https://cloud.google.com/compute/docs/labeling- - resources#restrictions) page. An object containing a list of "key": - value pairs. Example: { "name": "wrench", "mass": "1kg", "count": "3" }. - - Fields: - additionalExperiments: Optional. Additional experiment flags for the job, - specified with the `--experiments` option. - additionalPipelineOptions: Optional. Additional pipeline option flags for - the job. - additionalUserLabels: Optional. Additional user labels to be specified for - the job. Keys and values should follow the restrictions specified in the - [labeling restrictions](https://cloud.google.com/compute/docs/labeling- - resources#restrictions) page. An object containing a list of "key": - value pairs. Example: { "name": "wrench", "mass": "1kg", "count": "3" }. - bypassTempDirValidation: Optional. Whether to bypass the safety checks for - the job's temporary directory. Use with caution. - diskSizeGb: Optional. The disk size, in gigabytes, to use on each remote - Compute Engine worker instance. - enableStreamingEngine: Optional. Whether to enable Streaming Engine for - the job. - ipConfiguration: Optional. Configuration for VM IPs. - kmsKeyName: Optional. Name for the Cloud KMS key for the job. Key format - is: projects//locations//keyRings//cryptoKeys/ - machineType: Optional. The machine type to use for the job. Defaults to - the value from the template if not specified. - maxWorkers: Optional. The maximum number of Google Compute Engine - instances to be made available to your pipeline during execution, from 1 - to 1000. The default value is 1. - network: Optional. Network to which VMs will be assigned. If empty or - unspecified, the service will use the network "default". - numWorkers: Optional. The initial number of Google Compute Engine - instances for the job. The default value is 11. - serviceAccountEmail: Optional. The email address of the service account to - run the job as. - streamingMode: Optional. Specifies the Streaming Engine message processing - guarantees. Reduces cost and latency but might result in duplicate - messages committed to storage. Designed to run simple mapping streaming - ETL jobs at the lowest cost. For example, Change Data Capture (CDC) to - BigQuery is a canonical use case. For more information, see [Set the - pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - subnetwork: Optional. Subnetwork to which VMs will be assigned, if - desired. You can specify a subnetwork using either a complete URL or an - abbreviated path. Expected to be of the form "https://www.googleapis.com - /compute/v1/projects/HOST_PROJECT_ID/regions/REGION/subnetworks/SUBNETWO - RK" or "regions/REGION/subnetworks/SUBNETWORK". If the subnetwork is - located in a Shared VPC network, you must use the complete URL. - tempLocation: Required. The Cloud Storage path to use for temporary files. - Must be a valid Cloud Storage URL, beginning with `gs://`. - workerRegion: Required. The Compute Engine region - (https://cloud.google.com/compute/docs/regions-zones/regions-zones) in - which worker processing should occur, e.g. "us-west1". Mutually - exclusive with worker_zone. If neither worker_region nor worker_zone is - specified, default to the control plane's region. - workerZone: Optional. The Compute Engine zone - (https://cloud.google.com/compute/docs/regions-zones/regions-zones) in - which worker processing should occur, e.g. "us-west1-a". Mutually - exclusive with worker_region. If neither worker_region nor worker_zone - is specified, a zone in the control plane's region is chosen based on - available capacity. If both `worker_zone` and `zone` are set, - `worker_zone` takes precedence. - zone: Optional. The Compute Engine [availability - zone](https://cloud.google.com/compute/docs/regions-zones/regions-zones) - for launching worker instances to run your pipeline. In the future, - worker_zone will take precedence. - """ - - class IpConfigurationValueValuesEnum(_messages.Enum): - r"""Optional. Configuration for VM IPs. - - Values: - WORKER_IP_UNSPECIFIED: The configuration is unknown, or unspecified. - WORKER_IP_PUBLIC: Workers should have public IP addresses. - WORKER_IP_PRIVATE: Workers should have private IP addresses. - """ - WORKER_IP_UNSPECIFIED = 0 - WORKER_IP_PUBLIC = 1 - WORKER_IP_PRIVATE = 2 - - class StreamingModeValueValuesEnum(_messages.Enum): - r"""Optional. Specifies the Streaming Engine message processing - guarantees. Reduces cost and latency but might result in duplicate - messages committed to storage. Designed to run simple mapping streaming - ETL jobs at the lowest cost. For example, Change Data Capture (CDC) to - BigQuery is a canonical use case. For more information, see [Set the - pipeline streaming - mode](https://cloud.google.com/dataflow/docs/guides/streaming-modes). - - Values: - STREAMING_MODE_UNSPECIFIED: Run in the default mode. - STREAMING_MODE_EXACTLY_ONCE: In this mode, message deduplication is - performed against persistent state to make sure each message is - processed and committed to storage exactly once. - STREAMING_MODE_AT_LEAST_ONCE: Message deduplication is not performed. - Messages might be processed multiple times, and the results are - applied multiple times. Note: Setting this value also enables - Streaming Engine and Streaming Engine resource-based billing. - """ - STREAMING_MODE_UNSPECIFIED = 0 - STREAMING_MODE_EXACTLY_ONCE = 1 - STREAMING_MODE_AT_LEAST_ONCE = 2 - - @encoding.MapUnrecognizedFields('additionalProperties') - class AdditionalUserLabelsValue(_messages.Message): - r"""Optional. Additional user labels to be specified for the job. Keys and - values should follow the restrictions specified in the [labeling - restrictions](https://cloud.google.com/compute/docs/labeling- - resources#restrictions) page. An object containing a list of "key": value - pairs. Example: { "name": "wrench", "mass": "1kg", "count": "3" }. - - Messages: - AdditionalProperty: An additional property for a - AdditionalUserLabelsValue object. - - Fields: - additionalProperties: Additional properties of type - AdditionalUserLabelsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a AdditionalUserLabelsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - additionalExperiments = _messages.StringField(1, repeated=True) - additionalPipelineOptions = _messages.StringField(2, repeated=True) - additionalUserLabels = _messages.MessageField('AdditionalUserLabelsValue', 3) - bypassTempDirValidation = _messages.BooleanField(4) - diskSizeGb = _messages.IntegerField(5, variant=_messages.Variant.INT32) - enableStreamingEngine = _messages.BooleanField(6) - ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 7) - kmsKeyName = _messages.StringField(8) - machineType = _messages.StringField(9) - maxWorkers = _messages.IntegerField(10, variant=_messages.Variant.INT32) - network = _messages.StringField(11) - numWorkers = _messages.IntegerField(12, variant=_messages.Variant.INT32) - serviceAccountEmail = _messages.StringField(13) - streamingMode = _messages.EnumField('StreamingModeValueValuesEnum', 14) - subnetwork = _messages.StringField(15) - tempLocation = _messages.StringField(16) - workerRegion = _messages.StringField(17) - workerZone = _messages.StringField(18) - zone = _messages.StringField(19) - - -class RuntimeMetadata(_messages.Message): - r"""RuntimeMetadata describing a runtime environment. - - Fields: - parameters: The parameters for the template. - sdkInfo: SDK Info for the template. - """ - - parameters = _messages.MessageField('ParameterMetadata', 1, repeated=True) - sdkInfo = _messages.MessageField('SDKInfo', 2) - - -class RuntimeUpdatableParams(_messages.Message): - r"""Additional job parameters that can only be updated during runtime using - the projects.jobs.update method. These fields have no effect when specified - during job creation. - - Fields: - acceptableBacklogDuration: Optional. Deprecated: Use `latency_tier` - instead. The backlog threshold duration in seconds for autoscaling. - Value must be non-negative. - autoscalingTier: Optional. Deprecated: Use `latency_tier` instead. The - backlog threshold tier for autoscaling. Value must be one of "low- - latency", "medium-latency", or "high-latency". - latencyTier: Optional. The backlog threshold tier for autoscaling. Value - must be one of "low-latency", "medium-latency", or "high-latency". - maxNumWorkers: The maximum number of workers to cap autoscaling at. This - field is currently only supported for Streaming Engine jobs. - minNumWorkers: The minimum number of workers to scale down to. This field - is currently only supported for Streaming Engine jobs. - workerUtilizationHint: Target worker utilization, compared against the - aggregate utilization of the worker pool by autoscaler, to determine - upscaling and downscaling when absent other constraints such as backlog. - For more information, see [Update an existing - pipeline](https://cloud.google.com/dataflow/docs/guides/updating-a- - pipeline). - """ - - acceptableBacklogDuration = _messages.StringField(1) - autoscalingTier = _messages.StringField(2) - latencyTier = _messages.StringField(3) - maxNumWorkers = _messages.IntegerField(4, variant=_messages.Variant.INT32) - minNumWorkers = _messages.IntegerField(5, variant=_messages.Variant.INT32) - workerUtilizationHint = _messages.FloatField(6) - - -class SDKInfo(_messages.Message): - r"""SDK Information. - - Enums: - LanguageValueValuesEnum: Required. The SDK Language. - - Fields: - language: Required. The SDK Language. - version: Optional. The SDK version. - """ - - class LanguageValueValuesEnum(_messages.Enum): - r"""Required. The SDK Language. - - Values: - UNKNOWN: UNKNOWN Language. - JAVA: Java. - PYTHON: Python. - GO: Go. - YAML: YAML. - """ - UNKNOWN = 0 - JAVA = 1 - PYTHON = 2 - GO = 3 - YAML = 4 - - language = _messages.EnumField('LanguageValueValuesEnum', 1) - version = _messages.StringField(2) - - -class Sdk(_messages.Message): - r"""A structured representation of an SDK. - - Fields: - sdkId: The SDK harness id. - stacks: The stacktraces for the processes running on the SDK harness. - """ - - sdkId = _messages.StringField(1) - stacks = _messages.MessageField('Stack', 2, repeated=True) - - -class SdkBug(_messages.Message): - r"""A bug found in the Dataflow SDK. - - Enums: - SeverityValueValuesEnum: Output only. How severe the SDK bug is. - TypeValueValuesEnum: Output only. Describes the impact of this SDK bug. - - Fields: - severity: Output only. How severe the SDK bug is. - type: Output only. Describes the impact of this SDK bug. - uri: Output only. Link to more information on the bug. - """ - - class SeverityValueValuesEnum(_messages.Enum): - r"""Output only. How severe the SDK bug is. - - Values: - SEVERITY_UNSPECIFIED: A bug of unknown severity. - NOTICE: A minor bug that that may reduce reliability or performance for - some jobs. Impact will be minimal or non-existent for most jobs. - WARNING: A bug that has some likelihood of causing performance - degradation, data loss, or job failures. - SEVERE: A bug with extremely significant impact. Jobs may fail - erroneously, performance may be severely degraded, and data loss may - be very likely. - """ - SEVERITY_UNSPECIFIED = 0 - NOTICE = 1 - WARNING = 2 - SEVERE = 3 - - class TypeValueValuesEnum(_messages.Enum): - r"""Output only. Describes the impact of this SDK bug. - - Values: - TYPE_UNSPECIFIED: Unknown issue with this SDK. - GENERAL: Catch-all for SDK bugs that don't fit in the below categories. - PERFORMANCE: Using this version of the SDK may result in degraded - performance. - DATALOSS: Using this version of the SDK may cause data loss. - """ - TYPE_UNSPECIFIED = 0 - GENERAL = 1 - PERFORMANCE = 2 - DATALOSS = 3 - - severity = _messages.EnumField('SeverityValueValuesEnum', 1) - type = _messages.EnumField('TypeValueValuesEnum', 2) - uri = _messages.StringField(3) - - -class SdkHarnessContainerImage(_messages.Message): - r"""Defines an SDK harness container for executing Dataflow pipelines. - - Fields: - capabilities: The set of capabilities enumerated in the above Environment - proto. See also [beam_runner_api.proto](https://github.com/apache/beam/b - lob/master/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/ - v1/beam_runner_api.proto) - containerImage: A docker container image that resides in Google Container - Registry. - environmentId: Environment ID for the Beam runner API proto Environment - that corresponds to the current SDK Harness. - useSingleCorePerContainer: If true, recommends the Dataflow service to use - only one core per SDK container instance with this image. If false (or - unset) recommends using more than one core per SDK container instance - with this image for efficiency. Note that Dataflow service may choose to - override this property if needed. - """ - - capabilities = _messages.StringField(1, repeated=True) - containerImage = _messages.StringField(2) - environmentId = _messages.StringField(3) - useSingleCorePerContainer = _messages.BooleanField(4) - - -class SdkVersion(_messages.Message): - r"""The version of the SDK used to run the job. - - Enums: - SdkSupportStatusValueValuesEnum: The support status for this SDK version. - - Fields: - bugs: Output only. Known bugs found in this SDK version. - sdkSupportStatus: The support status for this SDK version. - version: The version of the SDK used to run the job. - versionDisplayName: A readable string describing the version of the SDK. - """ - - class SdkSupportStatusValueValuesEnum(_messages.Enum): - r"""The support status for this SDK version. - - Values: - UNKNOWN: Cloud Dataflow is unaware of this version. - SUPPORTED: This is a known version of an SDK, and is supported. - STALE: A newer version of the SDK family exists, and an update is - recommended. - DEPRECATED: This version of the SDK is deprecated and will eventually be - unsupported. - UNSUPPORTED: Support for this SDK version has ended and it should no - longer be used. - """ - UNKNOWN = 0 - SUPPORTED = 1 - STALE = 2 - DEPRECATED = 3 - UNSUPPORTED = 4 - - bugs = _messages.MessageField('SdkBug', 1, repeated=True) - sdkSupportStatus = _messages.EnumField('SdkSupportStatusValueValuesEnum', 2) - version = _messages.StringField(3) - versionDisplayName = _messages.StringField(4) - - -class SendDebugCaptureRequest(_messages.Message): - r"""Request to send encoded debug information. Next ID: 8 - - Enums: - DataFormatValueValuesEnum: Format for the data field above (id=5). - - Fields: - componentId: The internal component id for which debug information is - sent. - data: The encoded debug information. - dataFormat: Format for the data field above (id=5). - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job specified by job_id. - workerId: The worker id, i.e., VM hostname. - """ - - class DataFormatValueValuesEnum(_messages.Enum): - r"""Format for the data field above (id=5). - - Values: - DATA_FORMAT_UNSPECIFIED: Format unspecified, parsing is determined based - upon page type and legacy encoding. (go/protodosdonts#do-include-an- - unspecified-value-in-an-enum) - RAW: Raw HTML string. - JSON: JSON-encoded string. - ZLIB: Websafe encoded zlib-compressed string. - BROTLI: Websafe encoded brotli-compressed string. - """ - DATA_FORMAT_UNSPECIFIED = 0 - RAW = 1 - JSON = 2 - ZLIB = 3 - BROTLI = 4 - - componentId = _messages.StringField(1) - data = _messages.StringField(2) - dataFormat = _messages.EnumField('DataFormatValueValuesEnum', 3) - location = _messages.StringField(4) - workerId = _messages.StringField(5) - - -class SendDebugCaptureResponse(_messages.Message): - r"""Response to a send capture request. nothing""" - - -class SendWorkerMessagesRequest(_messages.Message): - r"""A request for sending worker messages to the service. - - Fields: - location: The [regional endpoint] - (https://cloud.google.com/dataflow/docs/concepts/regional-endpoints) - that contains the job. - workerMessages: The WorkerMessages to send. - """ - - location = _messages.StringField(1) - workerMessages = _messages.MessageField('WorkerMessage', 2, repeated=True) - - -class SendWorkerMessagesResponse(_messages.Message): - r"""The response to the worker messages. - - Fields: - workerMessageResponses: The servers response to the worker messages. - """ - - workerMessageResponses = _messages.MessageField('WorkerMessageResponse', 1, repeated=True) - - -class SeqMapTask(_messages.Message): - r"""Describes a particular function to invoke. - - Messages: - UserFnValue: The user function to invoke. - - Fields: - inputs: Information about each of the inputs. - name: The user-provided name of the SeqDo operation. - outputInfos: Information about each of the outputs. - stageName: System-defined name of the stage containing the SeqDo - operation. Unique across the workflow. - systemName: System-defined name of the SeqDo operation. Unique across the - workflow. - userFn: The user function to invoke. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UserFnValue(_messages.Message): - r"""The user function to invoke. - - Messages: - AdditionalProperty: An additional property for a UserFnValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UserFnValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - inputs = _messages.MessageField('SideInputInfo', 1, repeated=True) - name = _messages.StringField(2) - outputInfos = _messages.MessageField('SeqMapTaskOutputInfo', 3, repeated=True) - stageName = _messages.StringField(4) - systemName = _messages.StringField(5) - userFn = _messages.MessageField('UserFnValue', 6) - - -class SeqMapTaskOutputInfo(_messages.Message): - r"""Information about an output of a SeqMapTask. - - Fields: - sink: The sink to write the output value to. - tag: The id of the TupleTag the user code will tag the output value by. - """ - - sink = _messages.MessageField('Sink', 1) - tag = _messages.StringField(2) - - -class ServiceResources(_messages.Message): - r"""Resources used by the Dataflow Service to run the job. - - Fields: - zones: Output only. List of Cloud Zones being used by the Dataflow Service - for this job. Example: us-central1-c - """ - - zones = _messages.StringField(1, repeated=True) - - -class ShellTask(_messages.Message): - r"""A task which consists of a shell command for the worker to execute. - - Fields: - command: The shell command to run. - exitCode: Exit code for the task. - """ - - command = _messages.StringField(1) - exitCode = _messages.IntegerField(2, variant=_messages.Variant.INT32) - - -class SideInputInfo(_messages.Message): - r"""Information about a side input of a DoFn or an input of a SeqDoFn. - - Messages: - KindValue: How to interpret the source element(s) as a side input value. - - Fields: - kind: How to interpret the source element(s) as a side input value. - sources: The source(s) to read element(s) from to get the value of this - side input. If more than one source, then the elements are taken from - the sources, in the specified order if order matters. At least one - source is required. - tag: The id of the tag the user code will access this side input by; this - should correspond to the tag of some MultiOutputInfo. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class KindValue(_messages.Message): - r"""How to interpret the source element(s) as a side input value. - - Messages: - AdditionalProperty: An additional property for a KindValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a KindValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - kind = _messages.MessageField('KindValue', 1) - sources = _messages.MessageField('Source', 2, repeated=True) - tag = _messages.StringField(3) - - -class Sink(_messages.Message): - r"""A sink that records can be encoded and written to. - - Messages: - CodecValue: The codec to use to encode data written to the sink. - SpecValue: The sink to write to, plus its parameters. - - Fields: - codec: The codec to use to encode data written to the sink. - spec: The sink to write to, plus its parameters. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class CodecValue(_messages.Message): - r"""The codec to use to encode data written to the sink. - - Messages: - AdditionalProperty: An additional property for a CodecValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a CodecValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class SpecValue(_messages.Message): - r"""The sink to write to, plus its parameters. - - Messages: - AdditionalProperty: An additional property for a SpecValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a SpecValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - codec = _messages.MessageField('CodecValue', 1) - spec = _messages.MessageField('SpecValue', 2) - - -class Snapshot(_messages.Message): - r"""Represents a snapshot of a job. - - Enums: - StateValueValuesEnum: State of the snapshot. - - Fields: - creationTime: The time this snapshot was created. - description: User specified description of the snapshot. Maybe empty. - diskSizeBytes: The disk byte size of the snapshot. Only available for - snapshots in READY state. - id: The unique ID of this snapshot. - projectId: The project this snapshot belongs to. - pubsubMetadata: Pub/Sub snapshot metadata. - region: Cloud region where this snapshot lives in, e.g., "us-central1". - sourceJobId: The job this snapshot was created from. - state: State of the snapshot. - ttl: The time after which this snapshot will be automatically deleted. - """ - - class StateValueValuesEnum(_messages.Enum): - r"""State of the snapshot. - - Values: - UNKNOWN_SNAPSHOT_STATE: Unknown state. - PENDING: Snapshot intent to create has been persisted, snapshotting of - state has not yet started. - RUNNING: Snapshotting is being performed. - READY: Snapshot has been created and is ready to be used. - FAILED: Snapshot failed to be created. - DELETED: Snapshot has been deleted. - """ - UNKNOWN_SNAPSHOT_STATE = 0 - PENDING = 1 - RUNNING = 2 - READY = 3 - FAILED = 4 - DELETED = 5 - - creationTime = _messages.StringField(1) - description = _messages.StringField(2) - diskSizeBytes = _messages.IntegerField(3) - id = _messages.StringField(4) - projectId = _messages.StringField(5) - pubsubMetadata = _messages.MessageField('PubsubSnapshotMetadata', 6, repeated=True) - region = _messages.StringField(7) - sourceJobId = _messages.StringField(8) - state = _messages.EnumField('StateValueValuesEnum', 9) - ttl = _messages.StringField(10) - - -class SnapshotJobRequest(_messages.Message): - r"""Request to create a snapshot of a job. - - Fields: - description: User specified description of the snapshot. Maybe empty. - location: The location that contains this job. - snapshotSources: If true, perform snapshots for sources which support - this. - ttl: TTL for the snapshot. - """ - - description = _messages.StringField(1) - location = _messages.StringField(2) - snapshotSources = _messages.BooleanField(3) - ttl = _messages.StringField(4) - - -class Source(_messages.Message): - r"""A source that records can be read and decoded from. - - Messages: - BaseSpecsValueListEntry: A BaseSpecsValueListEntry object. - CodecValue: The codec to use to decode data read from the source. - SpecValue: The source to read from, plus its parameters. - - Fields: - baseSpecs: While splitting, sources may specify the produced bundles as - differences against another source, in order to save backend-side memory - and allow bigger jobs. For details, see SourceSplitRequest. To support - this use case, the full set of parameters of the source is logically - obtained by taking the latest explicitly specified value of each - parameter in the order: base_specs (later items win), spec (overrides - anything in base_specs). - codec: The codec to use to decode data read from the source. - doesNotNeedSplitting: Setting this value to true hints to the framework - that the source doesn't need splitting, and using SourceSplitRequest on - it would yield SOURCE_SPLIT_OUTCOME_USE_CURRENT. E.g. a file splitter - may set this to true when splitting a single file into a set of byte - ranges of appropriate size, and set this to false when splitting a - filepattern into individual files. However, for efficiency, a file - splitter may decide to produce file subranges directly from the - filepattern to avoid a splitting round-trip. See SourceSplitRequest for - an overview of the splitting process. This field is meaningful only in - the Source objects populated by the user (e.g. when filling in a - DerivedSource). Source objects supplied by the framework to the user - don't have this field populated. - metadata: Optionally, metadata for this source can be supplied right away, - avoiding a SourceGetMetadataOperation roundtrip (see - SourceOperationRequest). This field is meaningful only in the Source - objects populated by the user (e.g. when filling in a DerivedSource). - Source objects supplied by the framework to the user don't have this - field populated. - spec: The source to read from, plus its parameters. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class BaseSpecsValueListEntry(_messages.Message): - r"""A BaseSpecsValueListEntry object. - - Messages: - AdditionalProperty: An additional property for a BaseSpecsValueListEntry - object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a BaseSpecsValueListEntry object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class CodecValue(_messages.Message): - r"""The codec to use to decode data read from the source. - - Messages: - AdditionalProperty: An additional property for a CodecValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a CodecValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class SpecValue(_messages.Message): - r"""The source to read from, plus its parameters. - - Messages: - AdditionalProperty: An additional property for a SpecValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a SpecValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - baseSpecs = _messages.MessageField('BaseSpecsValueListEntry', 1, repeated=True) - codec = _messages.MessageField('CodecValue', 2) - doesNotNeedSplitting = _messages.BooleanField(3) - metadata = _messages.MessageField('SourceMetadata', 4) - spec = _messages.MessageField('SpecValue', 5) - - -class SourceFork(_messages.Message): - r"""DEPRECATED in favor of DynamicSourceSplit. - - Fields: - primary: DEPRECATED - primarySource: DEPRECATED - residual: DEPRECATED - residualSource: DEPRECATED - """ - - primary = _messages.MessageField('SourceSplitShard', 1) - primarySource = _messages.MessageField('DerivedSource', 2) - residual = _messages.MessageField('SourceSplitShard', 3) - residualSource = _messages.MessageField('DerivedSource', 4) - - -class SourceGetMetadataRequest(_messages.Message): - r"""A request to compute the SourceMetadata of a Source. - - Fields: - source: Specification of the source whose metadata should be computed. - """ - - source = _messages.MessageField('Source', 1) - - -class SourceGetMetadataResponse(_messages.Message): - r"""The result of a SourceGetMetadataOperation. - - Fields: - metadata: The computed metadata. - """ - - metadata = _messages.MessageField('SourceMetadata', 1) - - -class SourceMetadata(_messages.Message): - r"""Metadata about a Source useful for automatically optimizing and tuning - the pipeline, etc. - - Fields: - estimatedSizeBytes: An estimate of the total size (in bytes) of the data - that would be read from this source. This estimate is in terms of - external storage size, before any decompression or other processing done - by the reader. - infinite: Specifies that the size of this source is known to be infinite - (this is a streaming source). - producesSortedKeys: Whether this source is known to produce key/value - pairs with the (encoded) keys in lexicographically sorted order. - """ - - estimatedSizeBytes = _messages.IntegerField(1) - infinite = _messages.BooleanField(2) - producesSortedKeys = _messages.BooleanField(3) - - -class SourceOperationRequest(_messages.Message): - r"""A work item that represents the different operations that can be - performed on a user-defined Source specification. - - Fields: - getMetadata: Information about a request to get metadata about a source. - name: User-provided name of the Read instruction for this source. - originalName: System-defined name for the Read instruction for this source - in the original workflow graph. - split: Information about a request to split a source. - stageName: System-defined name of the stage containing the source - operation. Unique across the workflow. - systemName: System-defined name of the Read instruction for this source. - Unique across the workflow. - """ - - getMetadata = _messages.MessageField('SourceGetMetadataRequest', 1) - name = _messages.StringField(2) - originalName = _messages.StringField(3) - split = _messages.MessageField('SourceSplitRequest', 4) - stageName = _messages.StringField(5) - systemName = _messages.StringField(6) - - -class SourceOperationResponse(_messages.Message): - r"""The result of a SourceOperationRequest, specified in - ReportWorkItemStatusRequest.source_operation when the work item is - completed. - - Fields: - getMetadata: A response to a request to get metadata about a source. - split: A response to a request to split a source. - """ - - getMetadata = _messages.MessageField('SourceGetMetadataResponse', 1) - split = _messages.MessageField('SourceSplitResponse', 2) - - -class SourceSplitOptions(_messages.Message): - r"""Hints for splitting a Source into bundles (parts for parallel - processing) using SourceSplitRequest. - - Fields: - desiredBundleSizeBytes: The source should be split into a set of bundles - where the estimated size of each is approximately this many bytes. - desiredShardSizeBytes: DEPRECATED in favor of desired_bundle_size_bytes. - """ - - desiredBundleSizeBytes = _messages.IntegerField(1) - desiredShardSizeBytes = _messages.IntegerField(2) - - -class SourceSplitRequest(_messages.Message): - r"""Represents the operation to split a high-level Source specification into - bundles (parts for parallel processing). At a high level, splitting of a - source into bundles happens as follows: SourceSplitRequest is applied to the - source. If it returns SOURCE_SPLIT_OUTCOME_USE_CURRENT, no further splitting - happens and the source is used "as is". Otherwise, splitting is applied - recursively to each produced DerivedSource. As an optimization, for any - Source, if its does_not_need_splitting is true, the framework assumes that - splitting this source would return SOURCE_SPLIT_OUTCOME_USE_CURRENT, and - doesn't initiate a SourceSplitRequest. This applies both to the initial - source being split and to bundles produced from it. - - Fields: - options: Hints for tuning the splitting process. - source: Specification of the source to be split. - """ - - options = _messages.MessageField('SourceSplitOptions', 1) - source = _messages.MessageField('Source', 2) - - -class SourceSplitResponse(_messages.Message): - r"""The response to a SourceSplitRequest. - - Enums: - OutcomeValueValuesEnum: Indicates whether splitting happened and produced - a list of bundles. If this is USE_CURRENT_SOURCE_AS_IS, the current - source should be processed "as is" without splitting. "bundles" is - ignored in this case. If this is SPLITTING_HAPPENED, then "bundles" - contains a list of bundles into which the source was split. - - Fields: - bundles: If outcome is SPLITTING_HAPPENED, then this is a list of bundles - into which the source was split. Otherwise this field is ignored. This - list can be empty, which means the source represents an empty input. - outcome: Indicates whether splitting happened and produced a list of - bundles. If this is USE_CURRENT_SOURCE_AS_IS, the current source should - be processed "as is" without splitting. "bundles" is ignored in this - case. If this is SPLITTING_HAPPENED, then "bundles" contains a list of - bundles into which the source was split. - shards: DEPRECATED in favor of bundles. - """ - - class OutcomeValueValuesEnum(_messages.Enum): - r"""Indicates whether splitting happened and produced a list of bundles. - If this is USE_CURRENT_SOURCE_AS_IS, the current source should be - processed "as is" without splitting. "bundles" is ignored in this case. If - this is SPLITTING_HAPPENED, then "bundles" contains a list of bundles into - which the source was split. - - Values: - SOURCE_SPLIT_OUTCOME_UNKNOWN: The source split outcome is unknown, or - unspecified. - SOURCE_SPLIT_OUTCOME_USE_CURRENT: The current source should be processed - "as is" without splitting. - SOURCE_SPLIT_OUTCOME_SPLITTING_HAPPENED: Splitting produced a list of - bundles. - """ - SOURCE_SPLIT_OUTCOME_UNKNOWN = 0 - SOURCE_SPLIT_OUTCOME_USE_CURRENT = 1 - SOURCE_SPLIT_OUTCOME_SPLITTING_HAPPENED = 2 - - bundles = _messages.MessageField('DerivedSource', 1, repeated=True) - outcome = _messages.EnumField('OutcomeValueValuesEnum', 2) - shards = _messages.MessageField('SourceSplitShard', 3, repeated=True) - - -class SourceSplitShard(_messages.Message): - r"""DEPRECATED in favor of DerivedSource. - - Enums: - DerivationModeValueValuesEnum: DEPRECATED - - Fields: - derivationMode: DEPRECATED - source: DEPRECATED - """ - - class DerivationModeValueValuesEnum(_messages.Enum): - r"""DEPRECATED - - Values: - SOURCE_DERIVATION_MODE_UNKNOWN: The source derivation is unknown, or - unspecified. - SOURCE_DERIVATION_MODE_INDEPENDENT: Produce a completely independent - Source with no base. - SOURCE_DERIVATION_MODE_CHILD_OF_CURRENT: Produce a Source based on the - Source being split. - SOURCE_DERIVATION_MODE_SIBLING_OF_CURRENT: Produce a Source based on the - base of the Source being split. - """ - SOURCE_DERIVATION_MODE_UNKNOWN = 0 - SOURCE_DERIVATION_MODE_INDEPENDENT = 1 - SOURCE_DERIVATION_MODE_CHILD_OF_CURRENT = 2 - SOURCE_DERIVATION_MODE_SIBLING_OF_CURRENT = 3 - - derivationMode = _messages.EnumField('DerivationModeValueValuesEnum', 1) - source = _messages.MessageField('Source', 2) - - -class SpannerIODetails(_messages.Message): - r"""Metadata for a Spanner connector used by the job. - - Fields: - databaseId: DatabaseId accessed in the connection. - instanceId: InstanceId accessed in the connection. - projectId: ProjectId accessed in the connection. - """ - - databaseId = _messages.StringField(1) - instanceId = _messages.StringField(2) - projectId = _messages.StringField(3) - - -class SplitInt64(_messages.Message): - r"""A representation of an int64, n, that is immune to precision loss when - encoded in JSON. - - Fields: - highBits: The high order bits, including the sign: n >> 32. - lowBits: The low order bits: n & 0xffffffff. - """ - - highBits = _messages.IntegerField(1, variant=_messages.Variant.INT32) - lowBits = _messages.IntegerField(2, variant=_messages.Variant.UINT32) - - -class Stack(_messages.Message): - r"""A structuredstacktrace for a process running on the worker. - - Fields: - stackContent: The raw stack trace. - threadCount: With java thread dumps we may get collapsed stacks e.g., N - threads in stack "". Instead of having to copy over the same stack trace - N times, this int field captures this. - threadName: Thread name. For example, "CommitThread-0,10,main" - threadState: The state of the thread. For example, "WAITING". - timestamp: Timestamp at which the stack was captured. - """ - - stackContent = _messages.StringField(1) - threadCount = _messages.IntegerField(2, variant=_messages.Variant.INT32) - threadName = _messages.StringField(3) - threadState = _messages.StringField(4) - timestamp = _messages.StringField(5) - - -class StageExecutionDetails(_messages.Message): - r"""Information about the workers and work items within a stage. - - Fields: - nextPageToken: If present, this response does not contain all requested - tasks. To obtain the next page of results, repeat the request with - page_token set to this value. - workers: Workers that have done work on the stage. - """ - - nextPageToken = _messages.StringField(1) - workers = _messages.MessageField('WorkerDetails', 2, repeated=True) - - -class StageSource(_messages.Message): - r"""Description of an input or output of an execution stage. - - Fields: - name: Dataflow service generated name for this source. - originalTransformOrCollection: User name for the original user transform - or collection with which this source is most closely associated. - sizeBytes: Size of the source, if measurable. - userName: Human-readable name for this source; may be user or system - generated. - """ - - name = _messages.StringField(1) - originalTransformOrCollection = _messages.StringField(2) - sizeBytes = _messages.IntegerField(3) - userName = _messages.StringField(4) - - -class StageSummary(_messages.Message): - r"""Information about a particular execution stage of a job. - - Enums: - StateValueValuesEnum: State of this stage. - - Fields: - endTime: End time of this stage. If the work item is completed, this is - the actual end time of the stage. Otherwise, it is the predicted end - time. - metrics: Metrics for this stage. - progress: Progress for this stage. Only applicable to Batch jobs. - stageId: ID of this stage - startTime: Start time of this stage. - state: State of this stage. - stragglerSummary: Straggler summary for this stage. - """ - - class StateValueValuesEnum(_messages.Enum): - r"""State of this stage. - - Values: - EXECUTION_STATE_UNKNOWN: The component state is unknown or unspecified. - EXECUTION_STATE_NOT_STARTED: The component is not yet running. - EXECUTION_STATE_RUNNING: The component is currently running. - EXECUTION_STATE_SUCCEEDED: The component succeeded. - EXECUTION_STATE_FAILED: The component failed. - EXECUTION_STATE_CANCELLED: Execution of the component was cancelled. - """ - EXECUTION_STATE_UNKNOWN = 0 - EXECUTION_STATE_NOT_STARTED = 1 - EXECUTION_STATE_RUNNING = 2 - EXECUTION_STATE_SUCCEEDED = 3 - EXECUTION_STATE_FAILED = 4 - EXECUTION_STATE_CANCELLED = 5 - - endTime = _messages.StringField(1) - metrics = _messages.MessageField('MetricUpdate', 2, repeated=True) - progress = _messages.MessageField('ProgressTimeseries', 3) - stageId = _messages.StringField(4) - startTime = _messages.StringField(5) - state = _messages.EnumField('StateValueValuesEnum', 6) - stragglerSummary = _messages.MessageField('StragglerSummary', 7) - - -class StandardQueryParameters(_messages.Message): - r"""Query parameters accepted by all methods. - - Enums: - FXgafvValueValuesEnum: V1 error format. - AltValueValuesEnum: Data format for response. - - Fields: - f__xgafv: V1 error format. - access_token: OAuth access token. - alt: Data format for response. - callback: JSONP - fields: Selector specifying which fields to include in a partial response. - key: API key. Your API key identifies your project and provides you with - API access, quota, and reports. Required unless you provide an OAuth 2.0 - token. - oauth_token: OAuth 2.0 token for the current user. - prettyPrint: Returns response with indentations and line breaks. - quotaUser: Available to use for quota purposes for server-side - applications. Can be any arbitrary string assigned to a user, but should - not exceed 40 characters. - trace: A tracing token of the form "token:" to include in api - requests. - uploadType: Legacy upload protocol for media (e.g. "media", "multipart"). - upload_protocol: Upload protocol for media (e.g. "raw", "multipart"). - """ - - class AltValueValuesEnum(_messages.Enum): - r"""Data format for response. - - Values: - json: Responses with Content-Type of application/json - media: Media download with context-dependent Content-Type - proto: Responses with Content-Type of application/x-protobuf - """ - json = 0 - media = 1 - proto = 2 - - class FXgafvValueValuesEnum(_messages.Enum): - r"""V1 error format. - - Values: - _1: v1 error format - _2: v2 error format - """ - _1 = 0 - _2 = 1 - - f__xgafv = _messages.EnumField('FXgafvValueValuesEnum', 1) - access_token = _messages.StringField(2) - alt = _messages.EnumField('AltValueValuesEnum', 3, default='json') - callback = _messages.StringField(4) - fields = _messages.StringField(5) - key = _messages.StringField(6) - oauth_token = _messages.StringField(7) - prettyPrint = _messages.BooleanField(8, default=True) - quotaUser = _messages.StringField(9) - trace = _messages.StringField(10) - uploadType = _messages.StringField(11) - upload_protocol = _messages.StringField(12) - - -class StateFamilyConfig(_messages.Message): - r"""State family configuration. - - Fields: - isRead: If true, this family corresponds to a read operation. - stateFamily: The state family value. - """ - - isRead = _messages.BooleanField(1) - stateFamily = _messages.StringField(2) - - -class Status(_messages.Message): - r"""The `Status` type defines a logical error model that is suitable for - different programming environments, including REST APIs and RPC APIs. It is - used by [gRPC](https://github.com/grpc). Each `Status` message contains - three pieces of data: error code, error message, and error details. You can - find out more about this error model and how to work with it in the [API - Design Guide](https://cloud.google.com/apis/design/errors). - - Messages: - DetailsValueListEntry: A DetailsValueListEntry object. - - Fields: - code: The status code, which should be an enum value of google.rpc.Code. - details: A list of messages that carry the error details. There is a - common set of message types for APIs to use. - message: A developer-facing error message, which should be in English. Any - user-facing error message should be localized and sent in the - google.rpc.Status.details field, or localized by the client. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class DetailsValueListEntry(_messages.Message): - r"""A DetailsValueListEntry object. - - Messages: - AdditionalProperty: An additional property for a DetailsValueListEntry - object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a DetailsValueListEntry object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - code = _messages.IntegerField(1, variant=_messages.Variant.INT32) - details = _messages.MessageField('DetailsValueListEntry', 2, repeated=True) - message = _messages.StringField(3) - - -class Step(_messages.Message): - r"""Defines a particular step within a Cloud Dataflow job. A job consists of - multiple steps, each of which performs some specific operation as part of - the overall job. Data is typically passed from one step to another as part - of the job. **Note:** The properties of this object are not stable and might - change. Here's an example of a sequence of steps which together implement a - Map-Reduce job: * Read a collection of data from some source, parsing the - collection's elements. * Validate the elements. * Apply a user-defined - function to map each element to some value and extract an element-specific - key value. * Group elements with the same key into a single element with - that key, transforming a multiply-keyed collection into a uniquely-keyed - collection. * Write the elements out to some data sink. Note that the Cloud - Dataflow service may be used to run many different types of jobs, not just - Map-Reduce. - - Messages: - PropertiesValue: Named properties associated with the step. Each kind of - predefined step has its own required set of properties. Must be provided - on Create. Only retrieved with JOB_VIEW_ALL. - - Fields: - kind: The kind of step in the Cloud Dataflow job. - name: The name that identifies the step. This must be unique for each step - with respect to all other steps in the Cloud Dataflow job. - properties: Named properties associated with the step. Each kind of - predefined step has its own required set of properties. Must be provided - on Create. Only retrieved with JOB_VIEW_ALL. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class PropertiesValue(_messages.Message): - r"""Named properties associated with the step. Each kind of predefined - step has its own required set of properties. Must be provided on Create. - Only retrieved with JOB_VIEW_ALL. - - Messages: - AdditionalProperty: An additional property for a PropertiesValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a PropertiesValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - kind = _messages.StringField(1) - name = _messages.StringField(2) - properties = _messages.MessageField('PropertiesValue', 3) - - -class Straggler(_messages.Message): - r"""Information for a straggler. - - Fields: - batchStraggler: Batch straggler identification and debugging information. - streamingStraggler: Streaming straggler identification and debugging - information. - """ - - batchStraggler = _messages.MessageField('StragglerInfo', 1) - streamingStraggler = _messages.MessageField('StreamingStragglerInfo', 2) - - -class StragglerDebuggingInfo(_messages.Message): - r"""Information useful for debugging a straggler. Each type will provide - specialized debugging information relevant for a particular cause. The - StragglerDebuggingInfo will be 1:1 mapping to the StragglerCause enum. - - Fields: - hotKey: Hot key debugging details. - """ - - hotKey = _messages.MessageField('HotKeyDebuggingInfo', 1) - - -class StragglerInfo(_messages.Message): - r"""Information useful for straggler identification and debugging. - - Messages: - CausesValue: The straggler causes, keyed by the string representation of - the StragglerCause enum and contains specialized debugging information - for each straggler cause. - - Fields: - causes: The straggler causes, keyed by the string representation of the - StragglerCause enum and contains specialized debugging information for - each straggler cause. - startTime: The time when the work item attempt became a straggler. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class CausesValue(_messages.Message): - r"""The straggler causes, keyed by the string representation of the - StragglerCause enum and contains specialized debugging information for - each straggler cause. - - Messages: - AdditionalProperty: An additional property for a CausesValue object. - - Fields: - additionalProperties: Additional properties of type CausesValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a CausesValue object. - - Fields: - key: Name of the additional property. - value: A StragglerDebuggingInfo attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('StragglerDebuggingInfo', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - causes = _messages.MessageField('CausesValue', 1) - startTime = _messages.StringField(2) - - -class StragglerSummary(_messages.Message): - r"""Summarized straggler identification details. - - Messages: - StragglerCauseCountValue: Aggregated counts of straggler causes, keyed by - the string representation of the StragglerCause enum. - - Fields: - recentStragglers: The most recent stragglers. - stragglerCauseCount: Aggregated counts of straggler causes, keyed by the - string representation of the StragglerCause enum. - totalStragglerCount: The total count of stragglers. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class StragglerCauseCountValue(_messages.Message): - r"""Aggregated counts of straggler causes, keyed by the string - representation of the StragglerCause enum. - - Messages: - AdditionalProperty: An additional property for a - StragglerCauseCountValue object. - - Fields: - additionalProperties: Additional properties of type - StragglerCauseCountValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a StragglerCauseCountValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.IntegerField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - recentStragglers = _messages.MessageField('Straggler', 1, repeated=True) - stragglerCauseCount = _messages.MessageField('StragglerCauseCountValue', 2) - totalStragglerCount = _messages.IntegerField(3) - - -class StreamLocation(_messages.Message): - r"""Describes a stream of data, either as input to be processed or as output - of a streaming Dataflow job. - - Fields: - customSourceLocation: The stream is a custom source. - pubsubLocation: The stream is a pubsub stream. - sideInputLocation: The stream is a streaming side input. - streamingStageLocation: The stream is part of another computation within - the current streaming Dataflow job. - """ - - customSourceLocation = _messages.MessageField('CustomSourceLocation', 1) - pubsubLocation = _messages.MessageField('PubsubLocation', 2) - sideInputLocation = _messages.MessageField('StreamingSideInputLocation', 3) - streamingStageLocation = _messages.MessageField('StreamingStageLocation', 4) - - -class StreamingApplianceSnapshotConfig(_messages.Message): - r"""Streaming appliance snapshot configuration. - - Fields: - importStateEndpoint: Indicates which endpoint is used to import appliance - state. - snapshotId: If set, indicates the snapshot id for the snapshot being - performed. - """ - - importStateEndpoint = _messages.StringField(1) - snapshotId = _messages.StringField(2) - - -class StreamingComputationConfig(_messages.Message): - r"""Configuration information for a single streaming computation. - - Messages: - TransformUserNameToStateFamilyValue: Map from user name of stateful - transforms in this stage to their state family. - - Fields: - computationId: Unique identifier for this computation. - instructions: Instructions that comprise the computation. - stageName: Stage name of this computation. - systemName: System defined name for this computation. - transformUserNameToStateFamily: Map from user name of stateful transforms - in this stage to their state family. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class TransformUserNameToStateFamilyValue(_messages.Message): - r"""Map from user name of stateful transforms in this stage to their state - family. - - Messages: - AdditionalProperty: An additional property for a - TransformUserNameToStateFamilyValue object. - - Fields: - additionalProperties: Additional properties of type - TransformUserNameToStateFamilyValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a TransformUserNameToStateFamilyValue - object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - computationId = _messages.StringField(1) - instructions = _messages.MessageField('ParallelInstruction', 2, repeated=True) - stageName = _messages.StringField(3) - systemName = _messages.StringField(4) - transformUserNameToStateFamily = _messages.MessageField('TransformUserNameToStateFamilyValue', 5) - - -class StreamingComputationRanges(_messages.Message): - r"""Describes full or partial data disk assignment information of the - computation ranges. - - Fields: - computationId: The ID of the computation. - rangeAssignments: Data disk assignments for ranges from this computation. - """ - - computationId = _messages.StringField(1) - rangeAssignments = _messages.MessageField('KeyRangeDataDiskAssignment', 2, repeated=True) - - -class StreamingComputationTask(_messages.Message): - r"""A task which describes what action should be performed for the specified - streaming computation ranges. - - Enums: - TaskTypeValueValuesEnum: A type of streaming computation task. - - Fields: - computationRanges: Contains ranges of a streaming computation this task - should apply to. - dataDisks: Describes the set of data disks this task should apply to. - taskType: A type of streaming computation task. - """ - - class TaskTypeValueValuesEnum(_messages.Enum): - r"""A type of streaming computation task. - - Values: - STREAMING_COMPUTATION_TASK_UNKNOWN: The streaming computation task is - unknown, or unspecified. - STREAMING_COMPUTATION_TASK_STOP: Stop processing specified streaming - computation range(s). - STREAMING_COMPUTATION_TASK_START: Start processing specified streaming - computation range(s). - """ - STREAMING_COMPUTATION_TASK_UNKNOWN = 0 - STREAMING_COMPUTATION_TASK_STOP = 1 - STREAMING_COMPUTATION_TASK_START = 2 - - computationRanges = _messages.MessageField('StreamingComputationRanges', 1, repeated=True) - dataDisks = _messages.MessageField('MountedDataDisk', 2, repeated=True) - taskType = _messages.EnumField('TaskTypeValueValuesEnum', 3) - - -class StreamingConfigTask(_messages.Message): - r"""A task that carries configuration information for streaming - computations. - - Messages: - UserStepToStateFamilyNameMapValue: Map from user step names to state - families. - - Fields: - commitStreamChunkSizeBytes: Chunk size for commit streams from the harness - to windmill. - getDataStreamChunkSizeBytes: Chunk size for get data streams from the - harness to windmill. - maxWorkItemCommitBytes: Maximum size for work item commit supported - windmill storage layer. - operationalLimits: Operational limits for the streaming job. Can be used - by the worker to validate outputs sent to the backend. - streamingComputationConfigs: Set of computation configuration information. - streamingEngineStateTagEncodingVersion: Optional. The state tag encoding - format version for streaming engine jobs. - userStepToStateFamilyNameMap: Map from user step names to state families. - userWorkerRunnerV1Settings: Binary encoded proto to control runtime - behavior of the java runner v1 user worker. - userWorkerRunnerV2Settings: Binary encoded proto to control runtime - behavior of the runner v2 user worker. - windmillServiceEndpoint: If present, the worker must use this endpoint to - communicate with Windmill Service dispatchers, otherwise the worker must - continue to use whatever endpoint it had been using. - windmillServicePort: If present, the worker must use this port to - communicate with Windmill Service dispatchers. Only applicable when - windmill_service_endpoint is specified. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UserStepToStateFamilyNameMapValue(_messages.Message): - r"""Map from user step names to state families. - - Messages: - AdditionalProperty: An additional property for a - UserStepToStateFamilyNameMapValue object. - - Fields: - additionalProperties: Additional properties of type - UserStepToStateFamilyNameMapValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UserStepToStateFamilyNameMapValue - object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - commitStreamChunkSizeBytes = _messages.IntegerField(1) - getDataStreamChunkSizeBytes = _messages.IntegerField(2) - maxWorkItemCommitBytes = _messages.IntegerField(3) - operationalLimits = _messages.MessageField('StreamingOperationalLimits', 4) - streamingComputationConfigs = _messages.MessageField('StreamingComputationConfig', 5, repeated=True) - streamingEngineStateTagEncodingVersion = _messages.IntegerField(6, variant=_messages.Variant.INT32) - userStepToStateFamilyNameMap = _messages.MessageField('UserStepToStateFamilyNameMapValue', 7) - userWorkerRunnerV1Settings = _messages.BytesField(8) - userWorkerRunnerV2Settings = _messages.BytesField(9) - windmillServiceEndpoint = _messages.StringField(10) - windmillServicePort = _messages.IntegerField(11) - - -class StreamingOperationalLimits(_messages.Message): - r"""Operational limits imposed on streaming jobs by the backend. - - Fields: - maxBagElementBytes: The maximum size for an element in bag state. - maxGlobalDataBytes: The maximum size for an element in global data. - maxKeyBytes: The maximum size allowed for a key. - maxProductionOutputBytes: The maximum size for a single output element. - maxSortedListElementBytes: The maximum size for an element in sorted list - state. - maxSourceStateBytes: The maximum size for a source state update. - maxTagBytes: The maximum size for a state tag. - maxValueBytes: The maximum size for a value state field. - """ - - maxBagElementBytes = _messages.IntegerField(1) - maxGlobalDataBytes = _messages.IntegerField(2) - maxKeyBytes = _messages.IntegerField(3) - maxProductionOutputBytes = _messages.IntegerField(4) - maxSortedListElementBytes = _messages.IntegerField(5) - maxSourceStateBytes = _messages.IntegerField(6) - maxTagBytes = _messages.IntegerField(7) - maxValueBytes = _messages.IntegerField(8) - - -class StreamingScalingReport(_messages.Message): - r"""Contains per-user worker telemetry used in streaming autoscaling. - - Fields: - activeBundleCount: A integer attribute. - activeThreadCount: Current acive thread count. - maximumBundleCount: Maximum bundle count. - maximumBytes: Maximum bytes. - maximumBytesCount: A integer attribute. - maximumThreadCount: Maximum thread count limit. - outstandingBundleCount: Current outstanding bundle count. - outstandingBytes: Current outstanding bytes. - outstandingBytesCount: A integer attribute. - """ - - activeBundleCount = _messages.IntegerField(1, variant=_messages.Variant.INT32) - activeThreadCount = _messages.IntegerField(2, variant=_messages.Variant.INT32) - maximumBundleCount = _messages.IntegerField(3, variant=_messages.Variant.INT32) - maximumBytes = _messages.IntegerField(4) - maximumBytesCount = _messages.IntegerField(5, variant=_messages.Variant.INT32) - maximumThreadCount = _messages.IntegerField(6, variant=_messages.Variant.INT32) - outstandingBundleCount = _messages.IntegerField(7, variant=_messages.Variant.INT32) - outstandingBytes = _messages.IntegerField(8) - outstandingBytesCount = _messages.IntegerField(9, variant=_messages.Variant.INT32) - - -class StreamingScalingReportResponse(_messages.Message): - r"""Contains per-user-worker streaming scaling recommendation from the - backend. - - Fields: - maximumThreadCount: Maximum thread count limit; - """ - - maximumThreadCount = _messages.IntegerField(1, variant=_messages.Variant.INT32) - - -class StreamingSetupTask(_messages.Message): - r"""A task which initializes part of a streaming Dataflow job. - - Fields: - drain: The user has requested drain. - receiveWorkPort: The TCP port on which the worker should listen for - messages from other streaming computation workers. - snapshotConfig: Configures streaming appliance snapshot. - streamingComputationTopology: The global topology of the streaming - Dataflow job. - workerHarnessPort: The TCP port used by the worker to communicate with the - Dataflow worker harness. - """ - - drain = _messages.BooleanField(1) - receiveWorkPort = _messages.IntegerField(2, variant=_messages.Variant.INT32) - snapshotConfig = _messages.MessageField('StreamingApplianceSnapshotConfig', 3) - streamingComputationTopology = _messages.MessageField('TopologyConfig', 4) - workerHarnessPort = _messages.IntegerField(5, variant=_messages.Variant.INT32) - - -class StreamingSideInputLocation(_messages.Message): - r"""Identifies the location of a streaming side input. - - Fields: - stateFamily: Identifies the state family where this side input is stored. - tag: Identifies the particular side input within the streaming Dataflow - job. - """ - - stateFamily = _messages.StringField(1) - tag = _messages.StringField(2) - - -class StreamingStageLocation(_messages.Message): - r"""Identifies the location of a streaming computation stage, for stage-to- - stage communication. - - Fields: - streamId: Identifies the particular stream within the streaming Dataflow - job. - """ - - streamId = _messages.StringField(1) - - -class StreamingStragglerInfo(_messages.Message): - r"""Information useful for streaming straggler identification and debugging. - - Fields: - dataWatermarkLag: The event-time watermark lag at the time of the - straggler detection. - endTime: End time of this straggler. - startTime: Start time of this straggler. - systemWatermarkLag: The system watermark lag at the time of the straggler - detection. - workerName: Name of the worker where the straggler was detected. - """ - - dataWatermarkLag = _messages.StringField(1) - endTime = _messages.StringField(2) - startTime = _messages.StringField(3) - systemWatermarkLag = _messages.StringField(4) - workerName = _messages.StringField(5) - - -class StringList(_messages.Message): - r"""A metric value representing a list of strings. - - Fields: - elements: Elements of the list. - """ - - elements = _messages.StringField(1, repeated=True) - - -class StructuredMessage(_messages.Message): - r"""A rich message format, including a human readable string, a key for - identifying the message, and structured data associated with the message for - programmatic consumption. - - Fields: - messageKey: Identifier for this message type. Used by external systems to - internationalize or personalize message. - messageText: Human-readable version of message. - parameters: The structured data associated with this message. - """ - - messageKey = _messages.StringField(1) - messageText = _messages.StringField(2) - parameters = _messages.MessageField('Parameter', 3, repeated=True) - - -class TaskRunnerSettings(_messages.Message): - r"""Taskrunner configuration settings. - - Fields: - alsologtostderr: Whether to also send taskrunner log info to stderr. - baseTaskDir: The location on the worker for task-specific subdirectories. - baseUrl: The base URL for the taskrunner to use when accessing Google - Cloud APIs. When workers access Google Cloud APIs, they logically do so - via relative URLs. If this field is specified, it supplies the base URL - to use for resolving these relative URLs. The normative algorithm used - is defined by RFC 1808, "Relative Uniform Resource Locators". If not - specified, the default value is "http://www.googleapis.com/" - commandlinesFileName: The file to store preprocessing commands in. - continueOnException: Whether to continue taskrunner if an exception is - hit. - dataflowApiVersion: The API version of endpoint, e.g. "v1b3" - harnessCommand: The command to launch the worker harness. - languageHint: The suggested backend language. - logDir: The directory on the VM to store logs. - logToSerialconsole: Whether to send taskrunner log info to Google Compute - Engine VM serial console. - logUploadLocation: Indicates where to put logs. If this is not specified, - the logs will not be uploaded. The supported resource type is: Google - Cloud Storage: storage.googleapis.com/{bucket}/{object} - bucket.storage.googleapis.com/{object} - oauthScopes: The OAuth2 scopes to be requested by the taskrunner in order - to access the Cloud Dataflow API. - parallelWorkerSettings: The settings to pass to the parallel worker - harness. - streamingWorkerMainClass: The streaming worker main class name. - taskGroup: The UNIX group ID on the worker VM to use for tasks launched by - taskrunner; e.g. "wheel". - taskUser: The UNIX user ID on the worker VM to use for tasks launched by - taskrunner; e.g. "root". - tempStoragePrefix: The prefix of the resources the taskrunner should use - for temporary storage. The supported resource type is: Google Cloud - Storage: storage.googleapis.com/{bucket}/{object} - bucket.storage.googleapis.com/{object} - vmId: The ID string of the VM. - workflowFileName: The file to store the workflow in. - """ - - alsologtostderr = _messages.BooleanField(1) - baseTaskDir = _messages.StringField(2) - baseUrl = _messages.StringField(3) - commandlinesFileName = _messages.StringField(4) - continueOnException = _messages.BooleanField(5) - dataflowApiVersion = _messages.StringField(6) - harnessCommand = _messages.StringField(7) - languageHint = _messages.StringField(8) - logDir = _messages.StringField(9) - logToSerialconsole = _messages.BooleanField(10) - logUploadLocation = _messages.StringField(11) - oauthScopes = _messages.StringField(12, repeated=True) - parallelWorkerSettings = _messages.MessageField('WorkerSettings', 13) - streamingWorkerMainClass = _messages.StringField(14) - taskGroup = _messages.StringField(15) - taskUser = _messages.StringField(16) - tempStoragePrefix = _messages.StringField(17) - vmId = _messages.StringField(18) - workflowFileName = _messages.StringField(19) - - -class TemplateMetadata(_messages.Message): - r"""Metadata describing a template. - - Fields: - defaultStreamingMode: Optional. Indicates the default streaming mode for a - streaming template. Only valid if both supports_at_least_once and - supports_exactly_once are true. Possible values: UNSPECIFIED, - EXACTLY_ONCE and AT_LEAST_ONCE - description: Optional. A description of the template. - name: Required. The name of the template. - parameters: The parameters for the template. - streaming: Optional. Indicates if the template is streaming or not. - supportsAtLeastOnce: Optional. Indicates if the streaming template - supports at least once mode. - supportsExactlyOnce: Optional. Indicates if the streaming template - supports exactly once mode. - yamlDefinition: Optional. For future use. - """ - - defaultStreamingMode = _messages.StringField(1) - description = _messages.StringField(2) - name = _messages.StringField(3) - parameters = _messages.MessageField('ParameterMetadata', 4, repeated=True) - streaming = _messages.BooleanField(5) - supportsAtLeastOnce = _messages.BooleanField(6) - supportsExactlyOnce = _messages.BooleanField(7) - yamlDefinition = _messages.StringField(8) - - -class TopologyConfig(_messages.Message): - r"""Global topology of the streaming Dataflow job, including all - computations and their sharded locations. - - Messages: - UserStageToComputationNameMapValue: Maps user stage names to stable - computation names. - - Fields: - computations: The computations associated with a streaming Dataflow job. - dataDiskAssignments: The disks assigned to a streaming Dataflow job. - forwardingKeyBits: The size (in bits) of keys that will be assigned to - source messages. - persistentStateVersion: Version number for persistent state. - userStageToComputationNameMap: Maps user stage names to stable computation - names. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class UserStageToComputationNameMapValue(_messages.Message): - r"""Maps user stage names to stable computation names. - - Messages: - AdditionalProperty: An additional property for a - UserStageToComputationNameMapValue object. - - Fields: - additionalProperties: Additional properties of type - UserStageToComputationNameMapValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a UserStageToComputationNameMapValue - object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - computations = _messages.MessageField('ComputationTopology', 1, repeated=True) - dataDiskAssignments = _messages.MessageField('DataDiskAssignment', 2, repeated=True) - forwardingKeyBits = _messages.IntegerField(3, variant=_messages.Variant.INT32) - persistentStateVersion = _messages.IntegerField(4, variant=_messages.Variant.INT32) - userStageToComputationNameMap = _messages.MessageField('UserStageToComputationNameMapValue', 5) - - -class TransformSummary(_messages.Message): - r"""Description of the type, names/ids, and input/outputs for a transform. - - Enums: - KindValueValuesEnum: Type of transform. - - Fields: - displayData: Transform-specific display data. - id: SDK generated id of this transform instance. - inputCollectionName: User names for all collection inputs to this - transform. - kind: Type of transform. - name: User provided name for this transform instance. - outputCollectionName: User names for all collection outputs to this - transform. - """ - - class KindValueValuesEnum(_messages.Enum): - r"""Type of transform. - - Values: - UNKNOWN_KIND: Unrecognized transform type. - PAR_DO_KIND: ParDo transform. - GROUP_BY_KEY_KIND: Group By Key transform. - FLATTEN_KIND: Flatten transform. - READ_KIND: Read transform. - WRITE_KIND: Write transform. - CONSTANT_KIND: Constructs from a constant value, such as with Create.of. - SINGLETON_KIND: Creates a Singleton view of a collection. - SHUFFLE_KIND: Opening or closing a shuffle session, often as part of a - GroupByKey. - """ - UNKNOWN_KIND = 0 - PAR_DO_KIND = 1 - GROUP_BY_KEY_KIND = 2 - FLATTEN_KIND = 3 - READ_KIND = 4 - WRITE_KIND = 5 - CONSTANT_KIND = 6 - SINGLETON_KIND = 7 - SHUFFLE_KIND = 8 - - displayData = _messages.MessageField('DisplayData', 1, repeated=True) - id = _messages.StringField(2) - inputCollectionName = _messages.StringField(3, repeated=True) - kind = _messages.EnumField('KindValueValuesEnum', 4) - name = _messages.StringField(5) - outputCollectionName = _messages.StringField(6, repeated=True) - - -class WorkItem(_messages.Message): - r"""WorkItem represents basic information about a WorkItem to be executed in - the cloud. - - Fields: - configuration: Work item-specific configuration as an opaque blob. - id: Identifies this WorkItem. - initialReportIndex: The initial index to use when reporting the status of - the WorkItem. - jobId: Identifies the workflow job this WorkItem belongs to. - leaseExpireTime: Time when the lease on this Work will expire. - mapTask: Additional information for MapTask WorkItems. - packages: Any required packages that need to be fetched in order to - execute this WorkItem. - projectId: Identifies the cloud project this WorkItem belongs to. - reportStatusInterval: Recommended reporting interval. - seqMapTask: Additional information for SeqMapTask WorkItems. - shellTask: Additional information for ShellTask WorkItems. - sourceOperationTask: Additional information for source operation - WorkItems. - streamingComputationTask: Additional information for - StreamingComputationTask WorkItems. - streamingConfigTask: Additional information for StreamingConfigTask - WorkItems. - streamingSetupTask: Additional information for StreamingSetupTask - WorkItems. - """ - - configuration = _messages.StringField(1) - id = _messages.IntegerField(2) - initialReportIndex = _messages.IntegerField(3) - jobId = _messages.StringField(4) - leaseExpireTime = _messages.StringField(5) - mapTask = _messages.MessageField('MapTask', 6) - packages = _messages.MessageField('Package', 7, repeated=True) - projectId = _messages.StringField(8) - reportStatusInterval = _messages.StringField(9) - seqMapTask = _messages.MessageField('SeqMapTask', 10) - shellTask = _messages.MessageField('ShellTask', 11) - sourceOperationTask = _messages.MessageField('SourceOperationRequest', 12) - streamingComputationTask = _messages.MessageField('StreamingComputationTask', 13) - streamingConfigTask = _messages.MessageField('StreamingConfigTask', 14) - streamingSetupTask = _messages.MessageField('StreamingSetupTask', 15) - - -class WorkItemDetails(_messages.Message): - r"""Information about an individual work item execution. - - Enums: - StateValueValuesEnum: State of this work item. - - Fields: - attemptId: Attempt ID of this work item - endTime: End time of this work item attempt. If the work item is - completed, this is the actual end time of the work item. Otherwise, it - is the predicted end time. - metrics: Metrics for this work item. - progress: Progress of this work item. - startTime: Start time of this work item attempt. - state: State of this work item. - stragglerInfo: Information about straggler detections for this work item. - taskId: Name of this work item. - """ - - class StateValueValuesEnum(_messages.Enum): - r"""State of this work item. - - Values: - EXECUTION_STATE_UNKNOWN: The component state is unknown or unspecified. - EXECUTION_STATE_NOT_STARTED: The component is not yet running. - EXECUTION_STATE_RUNNING: The component is currently running. - EXECUTION_STATE_SUCCEEDED: The component succeeded. - EXECUTION_STATE_FAILED: The component failed. - EXECUTION_STATE_CANCELLED: Execution of the component was cancelled. - """ - EXECUTION_STATE_UNKNOWN = 0 - EXECUTION_STATE_NOT_STARTED = 1 - EXECUTION_STATE_RUNNING = 2 - EXECUTION_STATE_SUCCEEDED = 3 - EXECUTION_STATE_FAILED = 4 - EXECUTION_STATE_CANCELLED = 5 - - attemptId = _messages.StringField(1) - endTime = _messages.StringField(2) - metrics = _messages.MessageField('MetricUpdate', 3, repeated=True) - progress = _messages.MessageField('ProgressTimeseries', 4) - startTime = _messages.StringField(5) - state = _messages.EnumField('StateValueValuesEnum', 6) - stragglerInfo = _messages.MessageField('StragglerInfo', 7) - taskId = _messages.StringField(8) - - -class WorkItemServiceState(_messages.Message): - r"""The Dataflow service's idea of the current state of a WorkItem being - processed by a worker. - - Messages: - HarnessDataValue: Other data returned by the service, specific to the - particular worker harness. - - Fields: - completeWorkStatus: If set, a request to complete the work item with the - given status. This will not be set to OK, unless supported by the - specific kind of WorkItem. It can be used for the backend to indicate a - WorkItem must terminate, e.g., for aborting work. - harnessData: Other data returned by the service, specific to the - particular worker harness. - hotKeyDetection: A hot key is a symptom of poor data distribution in which - there are enough elements mapped to a single key to impact pipeline - performance. When present, this field includes metadata associated with - any hot key. - leaseExpireTime: Time at which the current lease will expire. - metricShortId: The short ids that workers should use in subsequent metric - updates. Workers should strive to use short ids whenever possible, but - it is ok to request the short_id again if a worker lost track of it - (e.g. if the worker is recovering from a crash). NOTE: it is possible - that the response may have short ids for a subset of the metrics. - nextReportIndex: The index value to use for the next report sent by the - worker. Note: If the report call fails for whatever reason, the worker - should reuse this index for subsequent report attempts. - reportStatusInterval: New recommended reporting interval. - splitRequest: The progress point in the WorkItem where the Dataflow - service suggests that the worker truncate the task. - suggestedStopPoint: DEPRECATED in favor of split_request. - suggestedStopPosition: Obsolete, always empty. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class HarnessDataValue(_messages.Message): - r"""Other data returned by the service, specific to the particular worker - harness. - - Messages: - AdditionalProperty: An additional property for a HarnessDataValue - object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a HarnessDataValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - completeWorkStatus = _messages.MessageField('Status', 1) - harnessData = _messages.MessageField('HarnessDataValue', 2) - hotKeyDetection = _messages.MessageField('HotKeyDetection', 3) - leaseExpireTime = _messages.StringField(4) - metricShortId = _messages.MessageField('MetricShortId', 5, repeated=True) - nextReportIndex = _messages.IntegerField(6) - reportStatusInterval = _messages.StringField(7) - splitRequest = _messages.MessageField('ApproximateSplitRequest', 8) - suggestedStopPoint = _messages.MessageField('ApproximateProgress', 9) - suggestedStopPosition = _messages.MessageField('Position', 10) - - -class WorkItemStatus(_messages.Message): - r"""Conveys a worker's progress through the work described by a WorkItem. - - Fields: - completed: True if the WorkItem was completed (successfully or - unsuccessfully). - counterUpdates: Worker output counters for this WorkItem. - dynamicSourceSplit: See documentation of stop_position. - errors: Specifies errors which occurred during processing. If errors are - provided, and completed = true, then the WorkItem is considered to have - failed. - metricUpdates: DEPRECATED in favor of counter_updates. - progress: DEPRECATED in favor of reported_progress. - reportIndex: The report index. When a WorkItem is leased, the lease will - contain an initial report index. When a WorkItem's status is reported to - the system, the report should be sent with that report index, and the - response will contain the index the worker should use for the next - report. Reports received with unexpected index values will be rejected - by the service. In order to preserve idempotency, the worker should not - alter the contents of a report, even if the worker must submit the same - report multiple times before getting back a response. The worker should - not submit a subsequent report until the response for the previous - report had been received from the service. - reportedProgress: The worker's progress through this WorkItem. - requestedLeaseDuration: Amount of time the worker requests for its lease. - sourceFork: DEPRECATED in favor of dynamic_source_split. - sourceOperationResponse: If the work item represented a - SourceOperationRequest, and the work is completed, contains the result - of the operation. - stopPosition: A worker may split an active map task in two parts, - "primary" and "residual", continuing to process the primary part and - returning the residual part into the pool of available work. This event - is called a "dynamic split" and is critical to the dynamic work - rebalancing feature. The two obtained sub-tasks are called "parts" of - the split. The parts, if concatenated, must represent the same input as - would be read by the current task if the split did not happen. The exact - way in which the original task is decomposed into the two parts is - specified either as a position demarcating them (stop_position), or - explicitly as two DerivedSources, if this task consumes a user-defined - source type (dynamic_source_split). The "current" task is adjusted as a - result of the split: after a task with range [A, B) sends a - stop_position update at C, its range is considered to be [A, C), e.g.: * - Progress should be interpreted relative to the new range, e.g. "75% - completed" means "75% of [A, C) completed" * The worker should interpret - proposed_stop_position relative to the new range, e.g. "split at 68%" - should be interpreted as "split at 68% of [A, C)". * If the worker - chooses to split again using stop_position, only stop_positions in [A, - C) will be accepted. * Etc. dynamic_source_split has similar semantics: - e.g., if a task with source S splits using dynamic_source_split into {P, - R} (where P and R must be together equivalent to S), then subsequent - progress and proposed_stop_position should be interpreted relative to P, - and in a potential subsequent dynamic_source_split into {P', R'}, P' and - R' must be together equivalent to P, etc. - totalThrottlerWaitTimeSeconds: Total time the worker spent being throttled - by external systems. - workItemId: Identifies the WorkItem. - """ - - completed = _messages.BooleanField(1) - counterUpdates = _messages.MessageField('CounterUpdate', 2, repeated=True) - dynamicSourceSplit = _messages.MessageField('DynamicSourceSplit', 3) - errors = _messages.MessageField('Status', 4, repeated=True) - metricUpdates = _messages.MessageField('MetricUpdate', 5, repeated=True) - progress = _messages.MessageField('ApproximateProgress', 6) - reportIndex = _messages.IntegerField(7) - reportedProgress = _messages.MessageField('ApproximateReportedProgress', 8) - requestedLeaseDuration = _messages.StringField(9) - sourceFork = _messages.MessageField('SourceFork', 10) - sourceOperationResponse = _messages.MessageField('SourceOperationResponse', 11) - stopPosition = _messages.MessageField('Position', 12) - totalThrottlerWaitTimeSeconds = _messages.FloatField(13) - workItemId = _messages.StringField(14) - - -class WorkerDetails(_messages.Message): - r"""Information about a worker - - Fields: - workItems: Work items processed by this worker, sorted by time. - workerName: Name of this worker - """ - - workItems = _messages.MessageField('WorkItemDetails', 1, repeated=True) - workerName = _messages.StringField(2) - - -class WorkerHealthReport(_messages.Message): - r"""WorkerHealthReport contains information about the health of a worker. - The VM should be identified by the labels attached to the WorkerMessage that - this health ping belongs to. - - Messages: - PodsValueListEntry: A PodsValueListEntry object. - - Fields: - msg: Message describing any unusual health reports. - pods: The pods running on the worker. See: - http://kubernetes.io/v1.1/docs/api-reference/v1/definitions.html#_v1_pod - This field is used by the worker to send the status of the indvidual - containers running on each worker. - reportInterval: The interval at which the worker is sending health - reports. The default value of 0 should be interpreted as the field is - not being explicitly set by the worker. - vmBrokenCode: Code to describe a specific reason, if known, that a VM has - reported broken state. - vmIsBroken: Whether the VM is in a permanently broken state. Broken VMs - should be abandoned or deleted ASAP to avoid assigning or completing any - work. - vmIsHealthy: Whether the VM is currently healthy. - vmStartupTime: The time the VM was booted. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class PodsValueListEntry(_messages.Message): - r"""A PodsValueListEntry object. - - Messages: - AdditionalProperty: An additional property for a PodsValueListEntry - object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a PodsValueListEntry object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - msg = _messages.StringField(1) - pods = _messages.MessageField('PodsValueListEntry', 2, repeated=True) - reportInterval = _messages.StringField(3) - vmBrokenCode = _messages.StringField(4) - vmIsBroken = _messages.BooleanField(5) - vmIsHealthy = _messages.BooleanField(6) - vmStartupTime = _messages.StringField(7) - - -class WorkerHealthReportResponse(_messages.Message): - r"""WorkerHealthReportResponse contains information returned to the worker - in response to a health ping. - - Fields: - reportInterval: A positive value indicates the worker should change its - reporting interval to the specified value. The default value of zero - means no change in report rate is requested by the server. - """ - - reportInterval = _messages.StringField(1) - - -class WorkerLifecycleEvent(_messages.Message): - r"""A report of an event in a worker's lifecycle. The proto contains one - event, because the worker is expected to asynchronously send each message - immediately after the event. Due to this asynchrony, messages may arrive out - of order (or missing), and it is up to the consumer to interpret. The - timestamp of the event is in the enclosing WorkerMessage proto. - - Enums: - EventValueValuesEnum: The event being reported. - - Messages: - MetadataValue: Other stats that can accompany an event. E.g. { - "downloaded_bytes" : "123456" } - - Fields: - containerStartTime: The start time of this container. All events will - report this so that events can be grouped together across container/VM - restarts. - event: The event being reported. - metadata: Other stats that can accompany an event. E.g. { - "downloaded_bytes" : "123456" } - """ - - class EventValueValuesEnum(_messages.Enum): - r"""The event being reported. - - Values: - UNKNOWN_EVENT: Invalid event. - OS_START: The time the VM started. - CONTAINER_START: Our container code starts running. Multiple containers - could be distinguished with WorkerMessage.labels if desired. - NETWORK_UP: The worker has a functional external network connection. - STAGING_FILES_DOWNLOAD_START: Started downloading staging files. - STAGING_FILES_DOWNLOAD_FINISH: Finished downloading all staging files. - SDK_INSTALL_START: For applicable SDKs, started installation of SDK and - worker packages. - SDK_INSTALL_FINISH: Finished installing SDK. - """ - UNKNOWN_EVENT = 0 - OS_START = 1 - CONTAINER_START = 2 - NETWORK_UP = 3 - STAGING_FILES_DOWNLOAD_START = 4 - STAGING_FILES_DOWNLOAD_FINISH = 5 - SDK_INSTALL_START = 6 - SDK_INSTALL_FINISH = 7 - - @encoding.MapUnrecognizedFields('additionalProperties') - class MetadataValue(_messages.Message): - r"""Other stats that can accompany an event. E.g. { "downloaded_bytes" : - "123456" } - - Messages: - AdditionalProperty: An additional property for a MetadataValue object. - - Fields: - additionalProperties: Additional properties of type MetadataValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a MetadataValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - containerStartTime = _messages.StringField(1) - event = _messages.EnumField('EventValueValuesEnum', 2) - metadata = _messages.MessageField('MetadataValue', 3) - - -class WorkerMessage(_messages.Message): - r"""WorkerMessage provides information to the backend about a worker. - - Messages: - LabelsValue: Labels are used to group WorkerMessages. For example, a - worker_message about a particular container might have the labels: { - "JOB_ID": "2015-04-22", "WORKER_ID": "wordcount-vm-2015..." - "CONTAINER_TYPE": "worker", "CONTAINER_ID": "ac1234def"} Label tags - typically correspond to Label enum values. However, for ease of - development other strings can be used as tags. LABEL_UNSPECIFIED should - not be used here. - - Fields: - dataSamplingReport: Optional. Contains metrics related to go/dataflow- - data-sampling-telemetry. - labels: Labels are used to group WorkerMessages. For example, a - worker_message about a particular container might have the labels: { - "JOB_ID": "2015-04-22", "WORKER_ID": "wordcount-vm-2015..." - "CONTAINER_TYPE": "worker", "CONTAINER_ID": "ac1234def"} Label tags - typically correspond to Label enum values. However, for ease of - development other strings can be used as tags. LABEL_UNSPECIFIED should - not be used here. - perWorkerMetrics: System defined metrics for this worker. - streamingScalingReport: Contains per-user worker telemetry used in - streaming autoscaling. - time: The timestamp of the worker_message. - workerHealthReport: The health of a worker. - workerLifecycleEvent: Record of worker lifecycle events. - workerMessageCode: A worker message code. - workerMetrics: Resource metrics reported by workers. - workerShutdownNotice: Shutdown notice by workers. - workerThreadScalingReport: Thread scaling information reported by workers. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class LabelsValue(_messages.Message): - r"""Labels are used to group WorkerMessages. For example, a worker_message - about a particular container might have the labels: { "JOB_ID": - "2015-04-22", "WORKER_ID": "wordcount-vm-2015..." "CONTAINER_TYPE": - "worker", "CONTAINER_ID": "ac1234def"} Label tags typically correspond to - Label enum values. However, for ease of development other strings can be - used as tags. LABEL_UNSPECIFIED should not be used here. - - Messages: - AdditionalProperty: An additional property for a LabelsValue object. - - Fields: - additionalProperties: Additional properties of type LabelsValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a LabelsValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - dataSamplingReport = _messages.MessageField('DataSamplingReport', 1) - labels = _messages.MessageField('LabelsValue', 2) - perWorkerMetrics = _messages.MessageField('PerWorkerMetrics', 3) - streamingScalingReport = _messages.MessageField('StreamingScalingReport', 4) - time = _messages.StringField(5) - workerHealthReport = _messages.MessageField('WorkerHealthReport', 6) - workerLifecycleEvent = _messages.MessageField('WorkerLifecycleEvent', 7) - workerMessageCode = _messages.MessageField('WorkerMessageCode', 8) - workerMetrics = _messages.MessageField('ResourceUtilizationReport', 9) - workerShutdownNotice = _messages.MessageField('WorkerShutdownNotice', 10) - workerThreadScalingReport = _messages.MessageField('WorkerThreadScalingReport', 11) - - -class WorkerMessageCode(_messages.Message): - r"""A message code is used to report status and error messages to the - service. The message codes are intended to be machine readable. The service - will take care of translating these into user understandable messages if - necessary. Example use cases: 1. Worker processes reporting successful - startup. 2. Worker processes reporting specific errors (e.g. package staging - failure). - - Messages: - ParametersValue: Parameters contains specific information about the code. - This is a struct to allow parameters of different types. Examples: 1. - For a "HARNESS_STARTED" message parameters might provide the name of the - worker and additional data like timing information. 2. For a - "GCS_DOWNLOAD_ERROR" parameters might contain fields listing the Cloud - Storage objects being downloaded and fields containing errors. In - general complex data structures should be avoided. If a worker needs to - send a specific and complicated data structure then please consider - defining a new proto and adding it to the data oneof in - WorkerMessageResponse. Conventions: Parameters should only be used for - information that isn't typically passed as a label. hostname and other - worker identifiers should almost always be passed as labels since they - will be included on most messages. - - Fields: - code: The code is a string intended for consumption by a machine that - identifies the type of message being sent. Examples: 1. - "HARNESS_STARTED" might be used to indicate the worker harness has - started. 2. "GCS_DOWNLOAD_ERROR" might be used to indicate an error - downloading a Cloud Storage file as part of the boot process of one of - the worker containers. This is a string and not an enum to make it easy - to add new codes without waiting for an API change. - parameters: Parameters contains specific information about the code. This - is a struct to allow parameters of different types. Examples: 1. For a - "HARNESS_STARTED" message parameters might provide the name of the - worker and additional data like timing information. 2. For a - "GCS_DOWNLOAD_ERROR" parameters might contain fields listing the Cloud - Storage objects being downloaded and fields containing errors. In - general complex data structures should be avoided. If a worker needs to - send a specific and complicated data structure then please consider - defining a new proto and adding it to the data oneof in - WorkerMessageResponse. Conventions: Parameters should only be used for - information that isn't typically passed as a label. hostname and other - worker identifiers should almost always be passed as labels since they - will be included on most messages. - """ - - @encoding.MapUnrecognizedFields('additionalProperties') - class ParametersValue(_messages.Message): - r"""Parameters contains specific information about the code. This is a - struct to allow parameters of different types. Examples: 1. For a - "HARNESS_STARTED" message parameters might provide the name of the worker - and additional data like timing information. 2. For a "GCS_DOWNLOAD_ERROR" - parameters might contain fields listing the Cloud Storage objects being - downloaded and fields containing errors. In general complex data - structures should be avoided. If a worker needs to send a specific and - complicated data structure then please consider defining a new proto and - adding it to the data oneof in WorkerMessageResponse. Conventions: - Parameters should only be used for information that isn't typically passed - as a label. hostname and other worker identifiers should almost always be - passed as labels since they will be included on most messages. - - Messages: - AdditionalProperty: An additional property for a ParametersValue object. - - Fields: - additionalProperties: Properties of the object. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a ParametersValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - code = _messages.StringField(1) - parameters = _messages.MessageField('ParametersValue', 2) - - -class WorkerMessageResponse(_messages.Message): - r"""A worker_message response allows the server to pass information to the - sender. - - Fields: - streamingScalingReportResponse: Service's streaming scaling response for - workers. - workerHealthReportResponse: The service's response to a worker's health - report. - workerMetricsResponse: Service's response to reporting worker metrics - (currently empty). - workerShutdownNoticeResponse: Service's response to shutdown notice - (currently empty). - workerThreadScalingReportResponse: Service's thread scaling recommendation - for workers. - """ - - streamingScalingReportResponse = _messages.MessageField('StreamingScalingReportResponse', 1) - workerHealthReportResponse = _messages.MessageField('WorkerHealthReportResponse', 2) - workerMetricsResponse = _messages.MessageField('ResourceUtilizationReportResponse', 3) - workerShutdownNoticeResponse = _messages.MessageField('WorkerShutdownNoticeResponse', 4) - workerThreadScalingReportResponse = _messages.MessageField('WorkerThreadScalingReportResponse', 5) - - -class WorkerPool(_messages.Message): - r"""Describes one particular pool of Cloud Dataflow workers to be - instantiated by the Cloud Dataflow service in order to perform the - computations required by a job. Note that a workflow job may use multiple - pools, in order to match the various computational requirements of the - various stages of the job. - - Enums: - DefaultPackageSetValueValuesEnum: The default package set to install. This - allows the service to select a default set of packages which are useful - to worker harnesses written in a particular language. - IpConfigurationValueValuesEnum: Configuration for VM IPs. - TeardownPolicyValueValuesEnum: Sets the policy for determining when to - turndown worker pool. Allowed values are: `TEARDOWN_ALWAYS`, - `TEARDOWN_ON_SUCCESS`, and `TEARDOWN_NEVER`. `TEARDOWN_ALWAYS` means - workers are always torn down regardless of whether the job succeeds. - `TEARDOWN_ON_SUCCESS` means workers are torn down if the job succeeds. - `TEARDOWN_NEVER` means the workers are never torn down. If the workers - are not torn down by the service, they will continue to run and use - Google Compute Engine VM resources in the user's project until they are - explicitly terminated by the user. Because of this, Google recommends - using the `TEARDOWN_ALWAYS` policy except for small, manually supervised - test jobs. If unknown or unspecified, the service will attempt to choose - a reasonable default. - - Messages: - MetadataValue: Metadata to set on the Google Compute Engine VMs. - PoolArgsValue: Extra arguments for this worker pool. - - Fields: - autoscalingSettings: Settings for autoscaling of this WorkerPool. - dataDisks: Data disks that are used by a VM in this workflow. - defaultPackageSet: The default package set to install. This allows the - service to select a default set of packages which are useful to worker - harnesses written in a particular language. - diskProvisionedIops: Optional. IOPS provisioned for the root disk for VMs. - diskProvisionedThroughputMibps: Optional. Throughput provisioned for the - root disk for VMs. - diskSizeGb: Size of root disk for VMs, in GB. If zero or unspecified, the - service will attempt to choose a reasonable default. - diskSourceImage: Fully qualified source image for disks. - diskType: Type of root disk for VMs. If empty or unspecified, the service - will attempt to choose a reasonable default. - ipConfiguration: Configuration for VM IPs. - kind: The kind of the worker pool; currently only `harness` and `shuffle` - are supported. - machineType: Machine type (e.g. "n1-standard-1"). If empty or unspecified, - the service will attempt to choose a reasonable default. - metadata: Metadata to set on the Google Compute Engine VMs. - network: Network to which VMs will be assigned. If empty or unspecified, - the service will use the network "default". - numThreadsPerWorker: The number of threads per worker harness. If empty or - unspecified, the service will choose a number of threads (according to - the number of cores on the selected machine type for batch, or 1 by - convention for streaming). - numWorkers: Number of Google Compute Engine workers in this pool needed to - execute the job. If zero or unspecified, the service will attempt to - choose a reasonable default. - onHostMaintenance: The action to take on host maintenance, as defined by - the Google Compute Engine API. - packages: Packages to be installed on workers. - poolArgs: Extra arguments for this worker pool. - sdkHarnessContainerImages: Set of SDK harness containers needed to execute - this pipeline. This will only be set in the Fn API path. For non-cross- - language pipelines this should have only one entry. Cross-language - pipelines will have two or more entries. - subnetwork: Subnetwork to which VMs will be assigned, if desired. Expected - to be of the form "regions/REGION/subnetworks/SUBNETWORK". - taskrunnerSettings: Settings passed through to Google Compute Engine - workers when using the standard Dataflow task runner. Users should - ignore this field. - teardownPolicy: Sets the policy for determining when to turndown worker - pool. Allowed values are: `TEARDOWN_ALWAYS`, `TEARDOWN_ON_SUCCESS`, and - `TEARDOWN_NEVER`. `TEARDOWN_ALWAYS` means workers are always torn down - regardless of whether the job succeeds. `TEARDOWN_ON_SUCCESS` means - workers are torn down if the job succeeds. `TEARDOWN_NEVER` means the - workers are never torn down. If the workers are not torn down by the - service, they will continue to run and use Google Compute Engine VM - resources in the user's project until they are explicitly terminated by - the user. Because of this, Google recommends using the `TEARDOWN_ALWAYS` - policy except for small, manually supervised test jobs. If unknown or - unspecified, the service will attempt to choose a reasonable default. - workerHarnessContainerImage: Required. Docker container image that - executes the Cloud Dataflow worker harness, residing in Google Container - Registry. Deprecated for the Fn API path. Use - sdk_harness_container_images instead. - zone: Zone to run the worker pools in. If empty or unspecified, the - service will attempt to choose a reasonable default. - """ - - class DefaultPackageSetValueValuesEnum(_messages.Enum): - r"""The default package set to install. This allows the service to select - a default set of packages which are useful to worker harnesses written in - a particular language. - - Values: - DEFAULT_PACKAGE_SET_UNKNOWN: The default set of packages to stage is - unknown, or unspecified. - DEFAULT_PACKAGE_SET_NONE: Indicates that no packages should be staged at - the worker unless explicitly specified by the job. - DEFAULT_PACKAGE_SET_JAVA: Stage packages typically useful to workers - written in Java. - DEFAULT_PACKAGE_SET_PYTHON: Stage packages typically useful to workers - written in Python. - """ - DEFAULT_PACKAGE_SET_UNKNOWN = 0 - DEFAULT_PACKAGE_SET_NONE = 1 - DEFAULT_PACKAGE_SET_JAVA = 2 - DEFAULT_PACKAGE_SET_PYTHON = 3 - - class IpConfigurationValueValuesEnum(_messages.Enum): - r"""Configuration for VM IPs. - - Values: - WORKER_IP_UNSPECIFIED: The configuration is unknown, or unspecified. - WORKER_IP_PUBLIC: Workers should have public IP addresses. - WORKER_IP_PRIVATE: Workers should have private IP addresses. - """ - WORKER_IP_UNSPECIFIED = 0 - WORKER_IP_PUBLIC = 1 - WORKER_IP_PRIVATE = 2 - - class TeardownPolicyValueValuesEnum(_messages.Enum): - r"""Sets the policy for determining when to turndown worker pool. Allowed - values are: `TEARDOWN_ALWAYS`, `TEARDOWN_ON_SUCCESS`, and - `TEARDOWN_NEVER`. `TEARDOWN_ALWAYS` means workers are always torn down - regardless of whether the job succeeds. `TEARDOWN_ON_SUCCESS` means - workers are torn down if the job succeeds. `TEARDOWN_NEVER` means the - workers are never torn down. If the workers are not torn down by the - service, they will continue to run and use Google Compute Engine VM - resources in the user's project until they are explicitly terminated by - the user. Because of this, Google recommends using the `TEARDOWN_ALWAYS` - policy except for small, manually supervised test jobs. If unknown or - unspecified, the service will attempt to choose a reasonable default. - - Values: - TEARDOWN_POLICY_UNKNOWN: The teardown policy isn't specified, or is - unknown. - TEARDOWN_ALWAYS: Always teardown the resource. - TEARDOWN_ON_SUCCESS: Teardown the resource on success. This is useful - for debugging failures. - TEARDOWN_NEVER: Never teardown the resource. This is useful for - debugging and development. - """ - TEARDOWN_POLICY_UNKNOWN = 0 - TEARDOWN_ALWAYS = 1 - TEARDOWN_ON_SUCCESS = 2 - TEARDOWN_NEVER = 3 - - @encoding.MapUnrecognizedFields('additionalProperties') - class MetadataValue(_messages.Message): - r"""Metadata to set on the Google Compute Engine VMs. - - Messages: - AdditionalProperty: An additional property for a MetadataValue object. - - Fields: - additionalProperties: Additional properties of type MetadataValue - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a MetadataValue object. - - Fields: - key: Name of the additional property. - value: A string attribute. - """ - - key = _messages.StringField(1) - value = _messages.StringField(2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - @encoding.MapUnrecognizedFields('additionalProperties') - class PoolArgsValue(_messages.Message): - r"""Extra arguments for this worker pool. - - Messages: - AdditionalProperty: An additional property for a PoolArgsValue object. - - Fields: - additionalProperties: Properties of the object. Contains field @type - with type URL. - """ - - class AdditionalProperty(_messages.Message): - r"""An additional property for a PoolArgsValue object. - - Fields: - key: Name of the additional property. - value: A extra_types.JsonValue attribute. - """ - - key = _messages.StringField(1) - value = _messages.MessageField('extra_types.JsonValue', 2) - - additionalProperties = _messages.MessageField('AdditionalProperty', 1, repeated=True) - - autoscalingSettings = _messages.MessageField('AutoscalingSettings', 1) - dataDisks = _messages.MessageField('Disk', 2, repeated=True) - defaultPackageSet = _messages.EnumField('DefaultPackageSetValueValuesEnum', 3) - diskProvisionedIops = _messages.IntegerField(4) - diskProvisionedThroughputMibps = _messages.IntegerField(5) - diskSizeGb = _messages.IntegerField(6, variant=_messages.Variant.INT32) - diskSourceImage = _messages.StringField(7) - diskType = _messages.StringField(8) - ipConfiguration = _messages.EnumField('IpConfigurationValueValuesEnum', 9) - kind = _messages.StringField(10) - machineType = _messages.StringField(11) - metadata = _messages.MessageField('MetadataValue', 12) - network = _messages.StringField(13) - numThreadsPerWorker = _messages.IntegerField(14, variant=_messages.Variant.INT32) - numWorkers = _messages.IntegerField(15, variant=_messages.Variant.INT32) - onHostMaintenance = _messages.StringField(16) - packages = _messages.MessageField('Package', 17, repeated=True) - poolArgs = _messages.MessageField('PoolArgsValue', 18) - sdkHarnessContainerImages = _messages.MessageField('SdkHarnessContainerImage', 19, repeated=True) - subnetwork = _messages.StringField(20) - taskrunnerSettings = _messages.MessageField('TaskRunnerSettings', 21) - teardownPolicy = _messages.EnumField('TeardownPolicyValueValuesEnum', 22) - workerHarnessContainerImage = _messages.StringField(23) - zone = _messages.StringField(24) - - -class WorkerSettings(_messages.Message): - r"""Provides data to pass through to the worker harness. - - Fields: - baseUrl: The base URL for accessing Google Cloud APIs. When workers access - Google Cloud APIs, they logically do so via relative URLs. If this field - is specified, it supplies the base URL to use for resolving these - relative URLs. The normative algorithm used is defined by RFC 1808, - "Relative Uniform Resource Locators". If not specified, the default - value is "http://www.googleapis.com/" - reportingEnabled: Whether to send work progress updates to the service. - servicePath: The Cloud Dataflow service path relative to the root URL, for - example, "dataflow/v1b3/projects". - shuffleServicePath: The Shuffle service path relative to the root URL, for - example, "shuffle/v1beta1". - tempStoragePrefix: The prefix of the resources the system should use for - temporary storage. The supported resource type is: Google Cloud Storage: - storage.googleapis.com/{bucket}/{object} - bucket.storage.googleapis.com/{object} - workerId: The ID of the worker running this pipeline. - """ - - baseUrl = _messages.StringField(1) - reportingEnabled = _messages.BooleanField(2) - servicePath = _messages.StringField(3) - shuffleServicePath = _messages.StringField(4) - tempStoragePrefix = _messages.StringField(5) - workerId = _messages.StringField(6) - - -class WorkerShutdownNotice(_messages.Message): - r"""Shutdown notification from workers. This is to be sent by the shutdown - script of the worker VM so that the backend knows that the VM is being shut - down. - - Fields: - reason: The reason for the worker shutdown. Current possible values are: - "UNKNOWN": shutdown reason is unknown. "PREEMPTION": shutdown reason is - preemption. Other possible reasons may be added in the future. - """ - - reason = _messages.StringField(1) - - -class WorkerShutdownNoticeResponse(_messages.Message): - r"""Service-side response to WorkerMessage issuing shutdown notice.""" - - -class WorkerThreadScalingReport(_messages.Message): - r"""Contains information about the thread scaling information of a worker. - - Fields: - currentThreadCount: Current number of active threads in a worker. - """ - - currentThreadCount = _messages.IntegerField(1, variant=_messages.Variant.INT32) - - -class WorkerThreadScalingReportResponse(_messages.Message): - r"""Contains the thread scaling recommendation for a worker from the - backend. - - Fields: - recommendedThreadCount: Recommended number of threads for a worker. - """ - - recommendedThreadCount = _messages.IntegerField(1, variant=_messages.Variant.INT32) - - -class WriteInstruction(_messages.Message): - r"""An instruction that writes records. Takes one input, produces no - outputs. - - Fields: - input: The input. - sink: The sink to write to. - """ - - input = _messages.MessageField('InstructionInput', 1) - sink = _messages.MessageField('Sink', 2) - - -encoding.AddCustomJsonFieldMapping( - StandardQueryParameters, 'f__xgafv', '$.xgafv') -encoding.AddCustomJsonEnumMapping( - StandardQueryParameters.FXgafvValueValuesEnum, '_1', '1') -encoding.AddCustomJsonEnumMapping( - StandardQueryParameters.FXgafvValueValuesEnum, '_2', '2') -encoding.AddCustomJsonFieldMapping( - DataflowProjectsLocationsTemplatesLaunchRequest, 'dynamicTemplate_gcsPath', 'dynamicTemplate.gcsPath') -encoding.AddCustomJsonFieldMapping( - DataflowProjectsLocationsTemplatesLaunchRequest, 'dynamicTemplate_stagingLocation', 'dynamicTemplate.stagingLocation') -encoding.AddCustomJsonFieldMapping( - DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_gcsPath', 'dynamicTemplate.gcsPath') -encoding.AddCustomJsonFieldMapping( - DataflowProjectsTemplatesLaunchRequest, 'dynamicTemplate_stagingLocation', 'dynamicTemplate.stagingLocation') diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers.py deleted file mode 100644 index 5b8753dfab65..000000000000 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers.py +++ /dev/null @@ -1,118 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# pytype: skip-file - -from hamcrest.core.base_matcher import BaseMatcher - -IGNORED = object() - - -class MetricStructuredNameMatcher(BaseMatcher): - """Matches a MetricStructuredName.""" - def __init__(self, name=IGNORED, origin=IGNORED, context=IGNORED): - """Creates a MetricsStructuredNameMatcher. - - Any property not passed in to the constructor will be ignored when matching. - - Args: - name: A string with the metric name. - origin: A string with the metric namespace. - context: A key:value dictionary that will be matched to the - structured name. - """ - if context != IGNORED and not isinstance(context, dict): - raise ValueError('context must be a Python dictionary.') - - self.name = name - self.origin = origin - self.context = context - - def _matches(self, item): - if self.name != IGNORED and item.name != self.name: - return False - if self.origin != IGNORED and item.origin != self.origin: - return False - if self.context != IGNORED: - for key, name in self.context.items(): - if key not in item.context: - return False - if name != IGNORED and item.context[key] != name: - return False - return True - - def describe_to(self, description): - descriptors = [] - if self.name != IGNORED: - descriptors.append('name is {}'.format(self.name)) - if self.origin != IGNORED: - descriptors.append('origin is {}'.format(self.origin)) - if self.context != IGNORED: - descriptors.append('context is ({})'.format(str(self.context))) - - item_description = ' and '.join(descriptors) - description.append(item_description) - - -class MetricUpdateMatcher(BaseMatcher): - """Matches a metrics update protocol buffer.""" - def __init__( - self, cumulative=IGNORED, name=IGNORED, scalar=IGNORED, kind=IGNORED): - """Creates a MetricUpdateMatcher. - - Any property not passed in to the constructor will be ignored when matching. - - Args: - cumulative: A boolean. - name: A MetricStructuredNameMatcher object that matches the name. - scalar: An integer with the metric update. - kind: A string defining the kind of counter. - """ - if name != IGNORED and not isinstance(name, MetricStructuredNameMatcher): - raise ValueError('name must be a MetricStructuredNameMatcher.') - - self.cumulative = cumulative - self.name = name - self.scalar = scalar - self.kind = kind - - def _matches(self, item): - if self.cumulative != IGNORED and item.cumulative != self.cumulative: - return False - if self.name != IGNORED and not self.name._matches(item.name): - return False - if self.kind != IGNORED and item.kind != self.kind: - return False - if self.scalar != IGNORED: - value_property = [ - p for p in item.scalar.object_value.properties if p.key == 'value' - ] - int_value = value_property[0].value.integer_value - if self.scalar != int_value: - return False - return True - - def describe_to(self, description): - descriptors = [] - if self.cumulative != IGNORED: - descriptors.append('cumulative is {}'.format(self.cumulative)) - if self.name != IGNORED: - descriptors.append('name is {}'.format(self.name)) - if self.scalar != IGNORED: - descriptors.append('scalar is ({})'.format(str(self.scalar))) - - item_description = ' and '.join(descriptors) - description.append(item_description) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers_test.py b/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers_test.py deleted file mode 100644 index 68dd06681ca0..000000000000 --- a/sdks/python/apache_beam/runners/dataflow/internal/clients/dataflow/message_matchers_test.py +++ /dev/null @@ -1,74 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# pytype: skip-file - -import unittest - -import hamcrest as hc - -import apache_beam.runners.dataflow.internal.clients.dataflow as dataflow -from apache_beam.internal.gcp.json_value import to_json_value -from apache_beam.runners.dataflow.internal.clients.dataflow import message_matchers - -# Protect against environments where apitools library is not available. -# pylint: disable=wrong-import-order, wrong-import-position -try: - from apitools.base.py import base_api -except ImportError: - base_api = None -# pylint: enable=wrong-import-order, wrong-import-position - - -@unittest.skipIf(base_api is None, 'GCP dependencies are not installed') -class TestMatchers(unittest.TestCase): - def test_structured_name_matcher_basic(self): - metric_name = dataflow.MetricStructuredName() - metric_name.name = 'metric1' - metric_name.origin = 'origin2' - - matcher = message_matchers.MetricStructuredNameMatcher( - name='metric1', origin='origin2') - hc.assert_that(metric_name, hc.is_(matcher)) - with self.assertRaises(AssertionError): - matcher = message_matchers.MetricStructuredNameMatcher( - name='metric1', origin='origin1') - hc.assert_that(metric_name, hc.is_(matcher)) - - def test_metric_update_basic(self): - metric_update = dataflow.MetricUpdate() - metric_update.name = dataflow.MetricStructuredName() - metric_update.name.name = 'metric1' - metric_update.name.origin = 'origin1' - - metric_update.cumulative = False - metric_update.kind = 'sum' - metric_update.scalar = to_json_value(1, with_type=True) - - name_matcher = message_matchers.MetricStructuredNameMatcher( - name='metric1', origin='origin1') - matcher = message_matchers.MetricUpdateMatcher( - name=name_matcher, kind='sum', scalar=1) - - hc.assert_that(metric_update, hc.is_(matcher)) - - with self.assertRaises(AssertionError): - matcher.kind = 'suma' - hc.assert_that(metric_update, hc.is_(matcher)) - - -if __name__ == '__main__': - unittest.main() diff --git a/sdks/python/setup.py b/sdks/python/setup.py index a508b4c07fb3..b8fd2f5f8349 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -534,6 +534,7 @@ def get_portability_package_data(): 'google-cloud-datastore>=2.0.0,<3', 'google-cloud-pubsub>=2.1.0,<3', 'google-cloud-storage>=2.18.2,<4', + 'google-cloud-dataflow-client>=0.13.0,<0.14.0', # GCP packages required by tests 'google-cloud-bigquery>=2.0.0,<4', 'google-cloud-bigquery-storage>=2.6.3,<3', From a1a1c6fd8e3f5f9a1fa9d3b4fe63002c361c744f Mon Sep 17 00:00:00 2001 From: Abacn Date: Tue, 2 Jun 2026 17:30:25 +0000 Subject: [PATCH 288/490] Adding release-2.74.0-postrelease to protected branches in .asf.yaml --- .asf.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.asf.yaml b/.asf.yaml index e69dcbc6ff96..b650499326d9 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -51,6 +51,7 @@ github: protected_branches: master: {} + release-2.74.0-postrelease: {} release-2.74: {} release-2.73.0-postrelease: {} release-2.73: {} From 389bbafcc1a6a36f5f91524b25ad74e86dc33dab Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 2 Jun 2026 13:33:21 -0400 Subject: [PATCH 289/490] Update managed-io.md for release 2.74.0-RC1. (#38527) Co-authored-by: Abacn --- .../content/en/documentation/io/managed-io.md | 551 ++++++++++-------- 1 file changed, 295 insertions(+), 256 deletions(-) diff --git a/website/www/site/content/en/documentation/io/managed-io.md b/website/www/site/content/en/documentation/io/managed-io.md index 46f20c5e11ab..ec3bb90183d4 100644 --- a/website/www/site/content/en/documentation/io/managed-io.md +++ b/website/www/site/content/en/documentation/io/managed-io.md @@ -121,23 +121,25 @@ and Beam SQL is invoked via the Managed API under the hood. table (str)
    + autosharding (boolean)
    catalog_name (str)
    catalog_properties (map[str, str])
    config_properties (map[str, str])
    direct_write_byte_limit (int32)
    + distribution_mode (str)
    drop (list[str])
    keep (list[str])
    only (str)
    partition_fields (list[str])
    + sort_fields (list[str])
    table_properties (map[str, str])
    triggering_frequency_seconds (int32)
    - MYSQL + SQLSERVER jdbc_url (str)
    - connection_init_sql (list[str])
    connection_properties (str)
    disable_auto_commit (boolean)
    fetch_size (int32)
    @@ -153,7 +155,6 @@ and Beam SQL is invoked via the Managed API under the hood. jdbc_url (str)
    autosharding (boolean)
    batch_size (int64)
    - connection_init_sql (list[str])
    connection_properties (str)
    location (str)
    password (str)
    @@ -187,9 +188,28 @@ and Beam SQL is invoked via the Managed API under the hood. - SQLSERVER + BIGQUERY + + kms_key (str)
    + query (str)
    + row_restriction (str)
    + fields (list[str])
    + table (str)
    + + + table (str)
    + drop (list[str])
    + keep (list[str])
    + kms_key (str)
    + only (str)
    + triggering_frequency_seconds (int64)
    + + + + MYSQL jdbc_url (str)
    + connection_init_sql (list[str])
    connection_properties (str)
    disable_auto_commit (boolean)
    fetch_size (int32)
    @@ -205,6 +225,7 @@ and Beam SQL is invoked via the Managed API under the hood. jdbc_url (str)
    autosharding (boolean)
    batch_size (int64)
    + connection_init_sql (list[str])
    connection_properties (str)
    location (str)
    password (str)
    @@ -212,24 +233,6 @@ and Beam SQL is invoked via the Managed API under the hood. write_statement (str)
    - - BIGQUERY - - kms_key (str)
    - query (str)
    - row_restriction (str)
    - fields (list[str])
    - table (str)
    - - - table (str)
    - drop (list[str])
    - keep (list[str])
    - kms_key (str)
    - only (str)
    - triggering_frequency_seconds (int64)
    - - @@ -401,7 +404,7 @@ and Beam SQL is invoked via the Managed API under the hood. -### `KAFKA` Write +### `KAFKA` Read

    @@ -418,228 +421,228 @@ and Beam SQL is invoked via the Managed API under the hood. str -
    - A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. | Format: host1:port1,host2:port2,... + A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. This list should be in the form `host1:port1,host2:port2,...`
    - format + topic str - The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO + n/a
    - topic + allow_duplicates - str + boolean - n/a + If the Kafka read allows duplicates.
    - file_descriptor_path + confluent_schema_registry_subject str - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + n/a
    - message_name + confluent_schema_registry_url str - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + n/a
    - producer_config_updates + consumer_config_updates map[str, str] - A list of key-value pairs that act as configuration parameters for Kafka producers. Most of these configurations will not be needed, but if you need to customize your Kafka producer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html + A list of key-value pairs that act as configuration parameters for Kafka consumers. Most of these configurations will not be needed, but if you need to customize your Kafka consumer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html
    - schema + file_descriptor_path str - n/a + The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization.
    -
    - -### `KAFKA` Read - -
    - - - - - - +
    ConfigurationTypeDescription
    - bootstrap_servers + format str - A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. This list should be in the form `host1:port1,host2:port2,...` + The encoding format for the data stored in Kafka. Valid options are: RAW,STRING,AVRO,JSON,PROTO
    - topic + message_name str - n/a + The name of the Protocol Buffer message to be used for schema extraction and data conversion.
    - allow_duplicates + offset_deduplication boolean - If the Kafka read allows duplicates. + If the redistribute is using offset deduplication mode.
    - confluent_schema_registry_subject + redistribute_by_record_key - str + boolean - n/a + If the redistribute keys by the Kafka record key.
    - confluent_schema_registry_url + redistribute_num_keys - str + int32 - n/a + The number of keys for redistributing Kafka inputs.
    - consumer_config_updates + redistributed - map[str, str] + boolean - A list of key-value pairs that act as configuration parameters for Kafka consumers. Most of these configurations will not be needed, but if you need to customize your Kafka consumer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html + If the Kafka read should be redistributed.
    - file_descriptor_path + schema str - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + The schema in which the data is encoded in the Kafka topic. For AVRO data, this is a schema defined with AVRO schema syntax (https://avro.apache.org/docs/1.10.2/spec.html#schemas). For JSON data, this is a schema defined with JSON-schema syntax (https://json-schema.org/). If a URL to Confluent Schema Registry is provided, then this field is ignored, and the schema is fetched from Confluent Schema Registry.
    +
    + +### `KAFKA` Write + +
    + + + + + + @@ -650,7 +653,7 @@ and Beam SQL is invoked via the Managed API under the hood. str
    ConfigurationTypeDescription
    - format + bootstrap_servers str - The encoding format for the data stored in Kafka. Valid options are: RAW,STRING,AVRO,JSON,PROTO + A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. | Format: host1:port1,host2:port2,...
    - message_name + format str - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO
    - offset_deduplication + topic - boolean + str - If the redistribute is using offset deduplication mode. + n/a
    - redistribute_by_record_key + file_descriptor_path - boolean + str - If the redistribute keys by the Kafka record key. + The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization.
    - redistribute_num_keys + message_name - int32 + str - The number of keys for redistributing Kafka inputs. + The name of the Protocol Buffer message to be used for schema extraction and data conversion.
    - redistributed + producer_config_updates - boolean + map[str, str] - If the Kafka read should be redistributed. + A list of key-value pairs that act as configuration parameters for Kafka producers. Most of these configurations will not be needed, but if you need to customize your Kafka producer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html
    - The schema in which the data is encoded in the Kafka topic. For AVRO data, this is a schema defined with AVRO schema syntax (https://avro.apache.org/docs/1.10.2/spec.html#schemas). For JSON data, this is a schema defined with JSON-schema syntax (https://json-schema.org/). If a URL to Confluent Schema Registry is provided, then this field is ignored, and the schema is fetched from Confluent Schema Registry. + n/a
    @@ -765,6 +768,17 @@ and Beam SQL is invoked via the Managed API under the hood. A fully-qualified table identifier. You may also provide a template to write to multiple dynamic destinations, for example: `dataset.my_{col1}_{col2.nested}_table`. + + + autosharding + + + boolean + + + Enables dynamic sharding to automatically adjust the number of parallel writers based on data volume. It handles data skew by further sub-dividing partitions into multiple shards to prevent bottlenecks during high-throughput writes. Only available with 'hash' distribution mode. + + catalog_name @@ -809,6 +823,19 @@ and Beam SQL is invoked via the Managed API under the hood. For a streaming pipeline, sets the limit for lifting bundles into the direct write path. + + + distribution_mode + + + str + + + Defines distribution of write data. Supported distributions: +- none: don't shuffle rows (default) +- hash: shuffle rows by partition key before writing data + + drop @@ -864,6 +891,18 @@ and Beam SQL is invoked via the Managed API under the hood. For more information on partition transforms, please visit https://iceberg.apache.org/spec/#partition-transforms. + + + sort_fields + + + list[str] + + + Fields used to set the table's sort order, applied when the table is created. Each entry has the form ` [asc|desc] [nulls first|nulls last]`, where `` is a field name or one of the partition transforms (e.g. `bucket(col, 4)`, `day(ts)`). Direction defaults to ascending; null order defaults to nulls-first for ascending and nulls-last for descending. Note: this sets the table's declared sort order as metadata; it does not cause Beam to physically sort records before writing. +For more information on sort orders, please visit https://iceberg.apache.org/spec/#sort-orders. + + table_properties @@ -890,7 +929,7 @@ For more information on table properties, please visit https://iceberg.apache.or
    -### `MYSQL` Read +### `SQLSERVER` Read
    @@ -910,17 +949,6 @@ For more information on table properties, please visit https://iceberg.apache.or Connection URL for the JDBC source. - - - - -
    - connection_init_sql - - list[str] - - Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this. -
    connection_properties @@ -1034,7 +1062,7 @@ For more information on table properties, please visit https://iceberg.apache.or
    -### `MYSQL` Write +### `SQLSERVER` Write
    @@ -1076,17 +1104,6 @@ For more information on table properties, please visit https://iceberg.apache.or n/a - - - - -
    - connection_init_sql - - list[str] - - Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this. -
    connection_properties @@ -1145,7 +1162,7 @@ For more information on table properties, please visit https://iceberg.apache.or
    -### `POSTGRES` Write +### `POSTGRES` Read
    @@ -1162,173 +1179,173 @@ For more information on table properties, please visit https://iceberg.apache.or str -
    - Connection URL for the JDBC sink. + Connection URL for the JDBC source.
    - autosharding + connection_properties - boolean + str - If true, enables using a dynamically determined number of shards to write. + Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;".
    - batch_size + fetch_size - int64 + int32 - n/a + This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors.
    - connection_properties + location str - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". + Name of the table to read from.
    - location + num_partitions - str + int32 - Name of the table to write to. + The number of partitions
    - password + output_parallelization - str + boolean - Password for the JDBC source. + Whether to reshuffle the resulting PCollection so results are distributed to all workers.
    - username + partition_column str - Username for the JDBC source. + Name of a column of numeric type that will be used for partitioning.
    - write_statement + password str - SQL query used to insert records into the JDBC sink. + Password for the JDBC source.
    -
    - -### `POSTGRES` Read - -
    - - - - - - +
    ConfigurationTypeDescription
    - jdbc_url + read_query str - Connection URL for the JDBC source. + SQL query used to query the JDBC source.
    - connection_properties + username str - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". + Username for the JDBC source.
    +
    + +### `POSTGRES` Write + +
    + + + + + + @@ -1344,30 +1361,30 @@ For more information on table properties, please visit https://iceberg.apache.or
    ConfigurationTypeDescription
    - fetch_size + jdbc_url - int32 + str - This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors. + Connection URL for the JDBC sink.
    - location + autosharding - str + boolean - Name of the table to read from. + If true, enables using a dynamically determined number of shards to write.
    - num_partitions + batch_size - int32 + int64 - The number of partitions + n/a
    - output_parallelization + connection_properties - boolean + str - Whether to reshuffle the resulting PCollection so results are distributed to all workers. + Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;".
    - partition_column + location str - Name of a column of numeric type that will be used for partitioning. + Name of the table to write to.
    - read_query + username str - SQL query used to query the JDBC source. + Username for the JDBC source.
    - username + write_statement str - Username for the JDBC source. + SQL query used to insert records into the JDBC sink.
    -### `SQLSERVER` Read +### `BIGQUERY` Write
    @@ -1378,129 +1395,141 @@ For more information on table properties, please visit https://iceberg.apache.or +
    - jdbc_url + table str - Connection URL for the JDBC source. + The bigquery table to write to. Format: [${PROJECT}:]${DATASET}.${TABLE}
    - connection_properties + drop - str + list[str] - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". + A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'.
    - disable_auto_commit + keep - boolean + list[str] - Whether to disable auto commit on read. Defaults to true if not provided. The need for this config varies depending on the database platform. Informix requires this to be set to false while Postgres requires this to be set to true. + A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'.
    - fetch_size + kms_key - int32 + str - This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors. + Use this Cloud KMS key to encrypt your data
    - location + only str - Name of the table to read from. + The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'.
    - num_partitions + triggering_frequency_seconds - int32 + int64 - The number of partitions + Determines how often to 'commit' progress into BigQuery. Default is every 5 seconds.
    +
    + +### `BIGQUERY` Read + +
    + + + + + +
    ConfigurationTypeDescription
    - output_parallelization + kms_key - boolean + str - Whether to reshuffle the resulting PCollection so results are distributed to all workers. + Use this Cloud KMS key to encrypt your data
    - partition_column + query str - Name of a column of numeric type that will be used for partitioning. + The SQL query to be executed to read from the BigQuery table.
    - password + row_restriction str - Password for the JDBC source. + Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query.
    - read_query + fields - str + list[str] - SQL query used to query the JDBC source. + Read only the specified fields (columns) from a BigQuery table. Fields may not be returned in the order specified. If no value is specified, then all fields are returned. Example: "col1, col2, col3"
    - username + table str - Username for the JDBC source. + The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE}
    -### `SQLSERVER` Write +### `MYSQL` Read
    @@ -1517,29 +1546,18 @@ For more information on table properties, please visit https://iceberg.apache.or str - - - - - @@ -1555,119 +1573,107 @@ For more information on table properties, please visit https://iceberg.apache.or -
    - Connection URL for the JDBC sink. -
    - autosharding - - boolean - - If true, enables using a dynamically determined number of shards to write. + Connection URL for the JDBC source.
    - batch_size + connection_init_sql - int64 + list[str] - n/a + Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this.
    - location + disable_auto_commit - str + boolean - Name of the table to write to. + Whether to disable auto commit on read. Defaults to true if not provided. The need for this config varies depending on the database platform. Informix requires this to be set to false while Postgres requires this to be set to true.
    - password + fetch_size - str + int32 - Password for the JDBC source. + This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors.
    - username + location str - Username for the JDBC source. + Name of the table to read from.
    - write_statement + num_partitions - str + int32 - SQL query used to insert records into the JDBC sink. + The number of partitions
    -
    - -### `BIGQUERY` Read - -
    - - - - - -
    ConfigurationTypeDescription
    - kms_key + output_parallelization - str + boolean - Use this Cloud KMS key to encrypt your data + Whether to reshuffle the resulting PCollection so results are distributed to all workers.
    - query + partition_column str - The SQL query to be executed to read from the BigQuery table. + Name of a column of numeric type that will be used for partitioning.
    - row_restriction + password str - Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query. + Password for the JDBC source.
    - fields + read_query - list[str] + str - Read only the specified fields (columns) from a BigQuery table. Fields may not be returned in the order specified. If no value is specified, then all fields are returned. Example: "col1, col2, col3" + SQL query used to query the JDBC source.
    - table + username str - The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE} + Username for the JDBC source.
    -### `BIGQUERY` Write +### `MYSQL` Write
    @@ -1678,68 +1684,101 @@ For more information on table properties, please visit https://iceberg.apache.or + + + + + + + + + + + + + + +
    - table + jdbc_url str - The bigquery table to write to. Format: [${PROJECT}:]${DATASET}.${TABLE} + Connection URL for the JDBC sink.
    - drop + autosharding - list[str] + boolean - A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. + If true, enables using a dynamically determined number of shards to write.
    - keep + batch_size + + int64 + + n/a +
    + connection_init_sql list[str] - A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'. + Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this.
    - kms_key + connection_properties str - Use this Cloud KMS key to encrypt your data + Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;".
    - only + location str - The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'. + Name of the table to write to.
    - triggering_frequency_seconds + password - int64 + str - Determines how often to 'commit' progress into BigQuery. Default is every 5 seconds. + Password for the JDBC source. +
    + username + + str + + Username for the JDBC source. +
    + write_statement + + str + + SQL query used to insert records into the JDBC sink.
    From 994011b610b0c01ff8b3f51f79800df021f8ac52 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Tue, 2 Jun 2026 22:59:34 +0400 Subject: [PATCH 290/490] Update Beam website to release 2.74.0 (#38536) * Update Beam website to release 2.74.0 * empty placeholder * Update release date * Fix typos --------- Co-authored-by: Yi Hu --- CHANGES.md | 6 +- website/www/site/config.toml | 2 +- .../www/site/content/en/blog/beam-2.74.0.md | 70 +++++++++++++++++++ .../site/content/en/get-started/downloads.md | 22 ++++-- 4 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 website/www/site/content/en/blog/beam-2.74.0.md diff --git a/CHANGES.md b/CHANGES.md index 669cabd7c5a1..b8b10c352fae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -82,6 +82,7 @@ ## Bugfixes * Fixed X (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). +* Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). ## Security Fixes @@ -92,7 +93,7 @@ [comment]: # ( When updating known issues after release, make sure also update website blog in website/www/site/content/blog.) * ([#X](https://github.com/apache/beam/issues/X)). -# [2.74.0] - 2026-XX-xx +# [2.74.0] - 2026-06-02 ## Highlights @@ -101,7 +102,7 @@ ## I/Os * IcebergIO: support declaring a table's sort order on dynamic table creation via the new `sort_fields` config ([#38269](https://github.com/apache/beam/issues/38269)). -* IcebergIO: support writing with hash distribution mode, and with autosharding ([#38061](https://github.com/apache/beam/issues/38061))). +* IcebergIO: support writing with hash distribution mode, and with autosharding ([#38061](https://github.com/apache/beam/issues/38061)). ## New Features / Improvements @@ -131,7 +132,6 @@ * Fixed BigQueryEnrichmentHandler batch mode dropping earlier requests when multiple requests share the same enrichment key (Python) ([#38035](https://github.com/apache/beam/issues/38035)). * Added `max_batch_duration_secs` passthrough support in Python Enrichment BigQuery and CloudSQL handlers so batching duration can be forwarded to `BatchElements` ([#38243](https://github.com/apache/beam/issues/38243)). -* Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). # [2.73.0] - 2026-04-29 diff --git a/website/www/site/config.toml b/website/www/site/config.toml index 6b171bf63284..9b6caf2dd654 100644 --- a/website/www/site/config.toml +++ b/website/www/site/config.toml @@ -104,7 +104,7 @@ github_project_repo = "https://github.com/apache/beam" [params] description = "Apache Beam is an open source, unified model and set of language-specific SDKs for defining and executing data processing workflows, and also data ingestion and integration flows, supporting Enterprise Integration Patterns (EIPs) and Domain Specific Languages (DSLs). Dataflow pipelines simplify the mechanics of large-scale batch and streaming data processing and can run on a number of runtimes like Apache Flink, Apache Spark, and Google Cloud Dataflow (a cloud service). Beam also brings DSL in different languages, allowing users to easily implement their data integration processes." -release_latest = "2.73.0" +release_latest = "2.74.0" # The repository and branch where the files live in Github or Colab. This is used # to serve and stage from your local branch, but publish to the master branch. # e.g. https://github.com/{{< param branch_repo >}}/path/to/notebook.ipynb diff --git a/website/www/site/content/en/blog/beam-2.74.0.md b/website/www/site/content/en/blog/beam-2.74.0.md new file mode 100644 index 000000000000..651ce4891800 --- /dev/null +++ b/website/www/site/content/en/blog/beam-2.74.0.md @@ -0,0 +1,70 @@ +--- +title: "Apache Beam 2.74.0" +date: 2026-06-02 14:00:00 -0500 +categories: + - blog + - release +authors: + - vterentev + - yhu +--- + + +We are happy to present the new 2.74.0 release of Beam. +This release includes both improvements and new functionality. +See the [download page](/get-started/downloads/#2740-2026-06-02) for this release. + + + +For more information on changes in 2.74.0, check out the [detailed release notes](https://github.com/apache/beam/milestone/42). + +## Highlights + +* Spark 4 runner support for Java SDK ([#38255](https://github.com/apache/beam/issues/38255)). + +### I/Os + +* IcebergIO: support declaring a table's sort order on dynamic table creation via the new `sort_fields` config ([#38269](https://github.com/apache/beam/issues/38269)). +* IcebergIO: support writing with hash distribution mode, and with autosharding ([#38061](https://github.com/apache/beam/issues/38061)). + +### New Features / Improvements + +* Capability introduces an indicator for aggregations and timers firing during a pipeline drain, allowing users and sinks to recognize and appropriately handle potentially incomplete or partial data ([#36884](https://github.com/apache/beam/issues/36884)). +* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Go/Python) ([#38349](https://github.com/apache/beam/issues/38349)). +* TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to + encode finished bitset. SentinelBitSetCoder and BitSetCoder are state + compatible. Both coders can decode encoded bytes from the other coder + ([#38139](https://github.com/apache/beam/issues/38139)). +* (Python) Added type alias for with_exception_handling to be used for typehints. ([#38173](https://github.com/apache/beam/issues/38173)). +* (Java) BatchElements transform for Java SDK ([#38369](https://github.com/apache/beam/issues/38369)) +* Added plugin mechanism to support different Lineage implementations (Java) ([#36790](https://github.com/apache/beam/issues/36790)). +* (Python) Supported Python user type in Beam SQL. For example, SQL statements like `SELECT some_field from PCOLLECTION` can now operate a PCollection of Beam Row containing pickable Python user type ([#20738](https://github.com/apache/beam/issues/20738)). +* (Python) Introduced `beam.coders.registry.register_row` as preferred API to register a named tuple or dataclass with a Beam Row. At pipelne runtime, the original type associated with the registered row are preserved across the serialization boundary ([#38108](https://github.com/apache/beam/issues/38108)). + +### Breaking Changes + +* (Python) Made Beartype the default fallback type checking tool. This can be disabled with the `--disable_beartype` pipeline option. ([#38275](https://github.com/apache/beam/issues/38275)) + +### Deprecations + +* Dropped Java 8 support ([#31678](https://github.com/apache/beam/issues/31678)). +* Removed Samza Runner support ([#35448](https://github.com/apache/beam/issues/35448)). + +### Bugfixes + +* Fixed BigQueryEnrichmentHandler batch mode dropping earlier requests when multiple requests share the same enrichment key (Python) ([#38035](https://github.com/apache/beam/issues/38035)). +* Added `max_batch_duration_secs` passthrough support in Python Enrichment BigQuery and CloudSQL handlers so batching duration can be forwarded to `BatchElements` ([#38243](https://github.com/apache/beam/issues/38243)). + +According to git shortlog, the following people contributed to the 2.74.0 release. Thank you to all contributors! + +Abdelrahman Ibrahim, Ahmed Abualsaud, Andrew Crites, Andrew Kabas, Arran Cudbard-Bell, Arun Pandian, Asish Kumar, Bentsi Leviav, Blake Jones, Bruno Volpato, Chris Jordan, Danny McCormick, Deji Ibrahim, Derrick Williams, Elia LIU, Ganesh Sivakumar, Jack McCluskey, Kenneth Knowles, Lalit Yadav, M Junaid Shaukat, Matej Aleksandrov, Prabhnoor Singh, Radek Stankiewicz, Radosław Stankiewicz, Reuven Lax, RuiLong J., Sam Whittle, Shunping Huang, Subramanya V, Tarun Annapareddy, Tobias Kaymak, TongruiLi, Valentyn Tymofieiev, Vitaly Terentyev, XQ Hu, Yi Hu, ZIHAN DAI, apanich, bambadiouf1, chenxuesdu, claudevdm, harshadkhetpal, johnjcasey, parveensania, tianz101 diff --git a/website/www/site/content/en/get-started/downloads.md b/website/www/site/content/en/get-started/downloads.md index c3a3eff7f533..61cc6bfe464b 100644 --- a/website/www/site/content/en/get-started/downloads.md +++ b/website/www/site/content/en/get-started/downloads.md @@ -95,23 +95,31 @@ versions denoted `0.x.y`. ### Current release -#### 2.73.0 (2026-04-29) +#### 2.74.0 (2026-06-02) -Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.73.0/apache-beam-2.73.0-source-release.zip). -[SHA-512](https://downloads.apache.org/beam/2.73.0/apache-beam-2.73.0-source-release.zip.sha512). -[signature](https://downloads.apache.org/beam/2.73.0/apache-beam-2.73.0-source-release.zip.asc). +Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.74.0/apache-beam-2.74.0-source-release.zip). +[SHA-512](https://downloads.apache.org/beam/2.74.0/apache-beam-2.74.0-source-release.zip.sha512). +[signature](https://downloads.apache.org/beam/2.74.0/apache-beam-2.74.0-source-release.zip.asc). -[Release notes](https://github.com/apache/beam/releases/tag/v2.71.0) +[Release notes](https://github.com/apache/beam/releases/tag/v2.74.0) ### Archived releases +#### 2.73.0 (2026-04-29) + +Official [source code download](https://archive.apache.org/dist/beam/2.73.0/apache-beam-2.73.0-source-release.zip). +[SHA-512](https://archive.apache.org/dist/beam/2.73.0/apache-beam-2.73.0-source-release.zip.sha512). +[signature](https://archive.apache.org/dist/beam/2.73.0/apache-beam-2.73.0-source-release.zip.asc). + +[Release notes](https://github.com/apache/beam/releases/tag/v2.73.0) + #### 2.72.0 (2026-03-30) Official [source code download](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip). [SHA-512](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip.sha512). [signature](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip.asc). -[Release notes](https://github.com/apache/beam/releases/tag/v2.70.0) +[Release notes](https://github.com/apache/beam/releases/tag/v2.72.0) #### 2.71.0 (2026-01-22) @@ -119,7 +127,7 @@ Official [source code download](https://archive.apache.org/dist/beam/2.71.0/apac [SHA-512](https://archive.apache.org/dist/beam/2.71.0/apache-beam-2.71.0-source-release.zip.sha512). [signature](https://archive.apache.org/dist/beam/2.71.0/apache-beam-2.71.0-source-release.zip.asc). -[Release notes](https://github.com/apache/beam/releases/tag/v2.70.0) +[Release notes](https://github.com/apache/beam/releases/tag/v2.71.0) #### 2.70.0 (2025-12-16) From 00b55c17fd7319307f3b663a0e5522fd0e3a5874 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:37:48 -0400 Subject: [PATCH 291/490] Update Python Dependencies (#38775) Co-authored-by: jrmccluskey --- .../ml/py310/base_image_requirements.txt | 31 ++++++----- .../ml/py310/gpu_image_requirements.txt | 51 +++++++++-------- .../ml/py311/base_image_requirements.txt | 35 ++++++------ .../ml/py311/gpu_image_requirements.txt | 55 ++++++++++--------- .../ml/py312/base_image_requirements.txt | 32 ++++++----- .../ml/py312/gpu_image_requirements.txt | 55 ++++++++++--------- .../ml/py313/base_image_requirements.txt | 32 ++++++----- .../py310/base_image_requirements.txt | 31 ++++++----- .../py311/base_image_requirements.txt | 35 ++++++------ .../py312/base_image_requirements.txt | 32 ++++++----- .../py313/base_image_requirements.txt | 32 ++++++----- .../py314/base_image_requirements.txt | 30 +++++----- 12 files changed, 242 insertions(+), 209 deletions(-) diff --git a/sdks/python/container/ml/py310/base_image_requirements.txt b/sdks/python/container/ml/py310/base_image_requirements.txt index 1ef9fe59430d..33b5587dc86b 100644 --- a/sdks/python/container/ml/py310/base_image_requirements.txt +++ b/sdks/python/container/ml/py310/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -43,10 +43,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -65,16 +65,17 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -90,15 +91,15 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -112,7 +113,7 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 @@ -121,7 +122,6 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 @@ -157,8 +157,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -173,7 +174,6 @@ pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pyjsparser==2.7.1 pymongo==4.17.0 PyMySQL==1.2.0 pyparsing==3.3.2 @@ -186,6 +186,8 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 @@ -202,8 +204,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 @@ -221,7 +223,6 @@ transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 -tzlocal==5.3.1 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/ml/py310/gpu_image_requirements.txt b/sdks/python/container/ml/py310/gpu_image_requirements.txt index 70ef4e441661..d909ca3142ee 100644 --- a/sdks/python/container/ml/py310/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py310/gpu_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 @@ -47,16 +47,16 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 cloudpickle==3.1.2 compressed-tensors==0.10.2 crcmod==1.7 cryptography==47.0.0 -cuda-bindings==13.2.0 -cuda-pathfinder==1.5.4 +cuda-bindings==13.3.1 +cuda-pathfinder==1.5.5 cuda-toolkit==13.0.2 -cupy-cuda12x==14.0.1 -Cython==3.2.4 +cupy-cuda12x==14.1.1 +Cython==3.2.5 depyf==0.19.0 detect-installer==0.1.0 dill==0.3.1.1 @@ -70,9 +70,9 @@ email-validator==2.3.0 envoy-data-plane==0.2.6 exceptiongroup==1.3.1 execnet==2.1.2 -fastapi==0.136.1 +fastapi==0.136.3 fastapi-cli==0.0.24 -fastapi-cloud-cli==0.18.0 +fastapi-cloud-cli==0.19.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 @@ -85,16 +85,17 @@ future==1.0.0 gast==0.7.0 gguf==0.19.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -110,15 +111,15 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -128,12 +129,12 @@ hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 -httptools==0.7.1 +httptools==0.8.0 httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 importlib_metadata==9.0.0 iniconfig==2.3.0 interegular==0.3.3 @@ -223,9 +224,10 @@ parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 -prometheus-fastapi-instrumentator==7.1.0 +portalocker==3.2.0 +prometheus-fastapi-instrumentator==8.0.0 prometheus_client==0.25.0 propcache==0.5.2 proto-plus==1.28.0 @@ -257,11 +259,12 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-json-logger==4.1.0 -python-multipart==0.0.29 +python-multipart==0.0.30 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 pyzmq==27.1.0 +qdrant-client==1.18.0 ray==2.55.1 referencing==0.37.0 regex==2026.5.9 @@ -278,7 +281,7 @@ scipy==1.15.3 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.60.0 +sentry-sdk==2.61.1 setproctitle==1.3.7 setuptools==81.0.0 shellingham==1.5.4 @@ -286,12 +289,12 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soundfile==0.13.1 -soupsieve==2.8.3 +soupsieve==2.8.4 soxr==1.1.0 -SQLAlchemy==2.0.49 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -starlette==0.52.1 +starlette==1.2.1 sympy==1.14.0 tenacity==9.1.4 tensorboard==2.20.0 @@ -310,13 +313,13 @@ torchvision==0.22.1 tqdm==4.67.3 transformers==4.55.4 triton==3.3.1 -typer==0.25.1 +typer==0.26.6 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 uritemplate==4.2.0 urllib3==2.7.0 -uvicorn==0.47.0 +uvicorn==0.48.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 diff --git a/sdks/python/container/ml/py311/base_image_requirements.txt b/sdks/python/container/ml/py311/base_image_requirements.txt index c7ffd60a9da3..a72c2814fd2e 100644 --- a/sdks/python/container/ml/py311/base_image_requirements.txt +++ b/sdks/python/container/ml/py311/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -42,10 +42,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -63,16 +63,17 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -88,16 +89,16 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -111,7 +112,7 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 @@ -120,7 +121,6 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 @@ -156,8 +156,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -172,7 +173,6 @@ pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pyjsparser==2.7.1 pymongo==4.17.0 PyMySQL==1.2.0 pyparsing==3.3.2 @@ -185,12 +185,14 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 rich==15.0.0 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.7.0 scikit-learn==1.7.2 @@ -201,8 +203,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 @@ -219,7 +221,6 @@ transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 -tzlocal==5.3.1 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/ml/py311/gpu_image_requirements.txt b/sdks/python/container/ml/py311/gpu_image_requirements.txt index 547cfd308fe0..49e997701548 100644 --- a/sdks/python/container/ml/py311/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py311/gpu_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 @@ -46,16 +46,16 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 cloudpickle==3.1.2 compressed-tensors==0.10.2 crcmod==1.7 cryptography==47.0.0 -cuda-bindings==13.2.0 -cuda-pathfinder==1.5.4 +cuda-bindings==13.3.1 +cuda-pathfinder==1.5.5 cuda-toolkit==13.0.2 -cupy-cuda12x==14.0.1 -Cython==3.2.4 +cupy-cuda12x==14.1.1 +Cython==3.2.5 depyf==0.19.0 detect-installer==0.1.0 dill==0.3.1.1 @@ -68,9 +68,9 @@ einops==0.8.2 email-validator==2.3.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastapi==0.136.1 +fastapi==0.136.3 fastapi-cli==0.0.24 -fastapi-cloud-cli==0.18.0 +fastapi-cloud-cli==0.19.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 @@ -83,16 +83,17 @@ future==1.0.0 gast==0.7.0 gguf==0.19.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -108,16 +109,16 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -127,12 +128,12 @@ hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 -httptools==0.7.1 +httptools==0.8.0 httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 importlib_metadata==9.0.0 iniconfig==2.3.0 interegular==0.3.3 @@ -222,9 +223,10 @@ parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 -prometheus-fastapi-instrumentator==7.1.0 +portalocker==3.2.0 +prometheus-fastapi-instrumentator==8.0.0 prometheus_client==0.25.0 propcache==0.5.2 proto-plus==1.28.0 @@ -256,11 +258,12 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-json-logger==4.1.0 -python-multipart==0.0.29 +python-multipart==0.0.30 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 pyzmq==27.1.0 +qdrant-client==1.18.0 ray==2.55.1 referencing==0.37.0 regex==2026.5.9 @@ -269,7 +272,7 @@ requests-mock==1.12.1 rich==15.0.0 rich-toolkit==0.19.10 rignore==0.7.6 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.7.0 scikit-learn==1.7.2 @@ -277,7 +280,7 @@ scipy==1.17.1 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.60.0 +sentry-sdk==2.61.1 setproctitle==1.3.7 setuptools==81.0.0 shellingham==1.5.4 @@ -285,12 +288,12 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soundfile==0.13.1 -soupsieve==2.8.3 +soupsieve==2.8.4 soxr==1.1.0 -SQLAlchemy==2.0.49 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -starlette==0.52.1 +starlette==1.2.1 sympy==1.14.0 tenacity==9.1.4 tensorboard==2.20.0 @@ -308,13 +311,13 @@ torchvision==0.22.1 tqdm==4.67.3 transformers==4.55.4 triton==3.3.1 -typer==0.25.1 +typer==0.26.6 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 uritemplate==4.2.0 urllib3==2.7.0 -uvicorn==0.47.0 +uvicorn==0.48.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index 9ab5d88a0efa..ad3c161a2c69 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -41,10 +41,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -62,16 +62,17 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -87,16 +88,16 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -110,7 +111,7 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 @@ -153,8 +154,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -181,12 +183,14 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 rich==15.0.0 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.7.0 scikit-learn==1.7.2 @@ -197,8 +201,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index 3c4d570820a5..d5cfda651ff4 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 @@ -45,16 +45,16 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 cloudpickle==3.1.2 compressed-tensors==0.10.2 crcmod==1.7 cryptography==47.0.0 -cuda-bindings==13.2.0 -cuda-pathfinder==1.5.4 +cuda-bindings==13.3.1 +cuda-pathfinder==1.5.5 cuda-toolkit==13.0.2 -cupy-cuda12x==14.0.1 -Cython==3.2.4 +cupy-cuda12x==14.1.1 +Cython==3.2.5 depyf==0.19.0 detect-installer==0.1.0 dill==0.3.1.1 @@ -67,9 +67,9 @@ einops==0.8.2 email-validator==2.3.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastapi==0.136.1 +fastapi==0.136.3 fastapi-cli==0.0.24 -fastapi-cloud-cli==0.18.0 +fastapi-cloud-cli==0.19.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 @@ -82,16 +82,17 @@ future==1.0.0 gast==0.7.0 gguf==0.19.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -107,16 +108,16 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -126,12 +127,12 @@ hf-xet==1.5.0 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 -httptools==0.7.1 +httptools==0.8.0 httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 iniconfig==2.3.0 interegular==0.3.3 jaraco.classes==3.4.0 @@ -220,9 +221,10 @@ parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 -prometheus-fastapi-instrumentator==7.1.0 +portalocker==3.2.0 +prometheus-fastapi-instrumentator==8.0.0 prometheus_client==0.25.0 propcache==0.5.2 proto-plus==1.28.0 @@ -254,11 +256,12 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-json-logger==4.1.0 -python-multipart==0.0.29 +python-multipart==0.0.30 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 pyzmq==27.1.0 +qdrant-client==1.18.0 ray==2.55.1 referencing==0.37.0 regex==2026.5.9 @@ -267,7 +270,7 @@ requests-mock==1.12.1 rich==15.0.0 rich-toolkit==0.19.10 rignore==0.7.6 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.7.0 scikit-learn==1.7.2 @@ -275,7 +278,7 @@ scipy==1.17.1 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.60.0 +sentry-sdk==2.61.1 setproctitle==1.3.7 setuptools==79.0.1 shellingham==1.5.4 @@ -283,12 +286,12 @@ six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soundfile==0.13.1 -soupsieve==2.8.3 +soupsieve==2.8.4 soxr==1.1.0 -SQLAlchemy==2.0.49 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -starlette==0.52.1 +starlette==1.2.1 sympy==1.14.0 tenacity==9.1.4 tensorboard==2.20.0 @@ -306,13 +309,13 @@ torchvision==0.22.1 tqdm==4.67.3 transformers==4.55.4 triton==3.3.1 -typer==0.25.1 +typer==0.26.6 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 uritemplate==4.2.0 urllib3==2.7.0 -uvicorn==0.47.0 +uvicorn==0.48.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 diff --git a/sdks/python/container/ml/py313/base_image_requirements.txt b/sdks/python/container/ml/py313/base_image_requirements.txt index bf8152d8027a..1d129a09fedf 100644 --- a/sdks/python/container/ml/py313/base_image_requirements.txt +++ b/sdks/python/container/ml/py313/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -41,10 +41,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -62,16 +62,17 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.35 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -86,16 +87,16 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-pasta==0.2.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -109,7 +110,7 @@ httpx==0.28.1 huggingface_hub==0.36.2 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 @@ -152,8 +153,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -180,12 +182,14 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 rich==15.0.0 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.7.0 scikit-learn==1.7.2 @@ -196,8 +200,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 diff --git a/sdks/python/container/py310/base_image_requirements.txt b/sdks/python/container/py310/base_image_requirements.txt index e40d90c79472..c44d8ef0bf5c 100644 --- a/sdks/python/container/py310/base_image_requirements.txt +++ b/sdks/python/container/py310/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -41,10 +41,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -59,16 +59,17 @@ freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -84,14 +85,14 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -102,7 +103,7 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 @@ -111,7 +112,6 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 @@ -137,8 +137,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -153,7 +154,6 @@ pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pyjsparser==2.7.1 pymongo==4.17.0 PyMySQL==1.2.0 pyparsing==3.3.2 @@ -166,6 +166,8 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 @@ -180,8 +182,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 @@ -192,7 +194,6 @@ tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 -tzlocal==5.3.1 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/py311/base_image_requirements.txt b/sdks/python/container/py311/base_image_requirements.txt index 8362e22e172f..790825a8b2af 100644 --- a/sdks/python/container/py311/base_image_requirements.txt +++ b/sdks/python/container/py311/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -40,10 +40,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -57,16 +57,17 @@ freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -82,15 +83,15 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -101,7 +102,7 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 importlib_metadata==9.0.0 iniconfig==2.3.0 jaraco.classes==3.4.0 @@ -110,7 +111,6 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -Js2Py==0.74 jsonpickle==3.4.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 @@ -136,8 +136,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -152,7 +153,6 @@ pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 -pyjsparser==2.7.1 pymongo==4.17.0 PyMySQL==1.2.0 pyparsing==3.3.2 @@ -165,11 +165,13 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 scipy==1.17.1 @@ -179,8 +181,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 @@ -190,7 +192,6 @@ tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 -tzlocal==5.3.1 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/py312/base_image_requirements.txt b/sdks/python/container/py312/base_image_requirements.txt index f0ca11af6e4b..151705811fdd 100644 --- a/sdks/python/container/py312/base_image_requirements.txt +++ b/sdks/python/container/py312/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -39,10 +39,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -56,16 +56,17 @@ freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -81,15 +82,15 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -100,7 +101,7 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 @@ -133,8 +134,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -161,11 +163,13 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 scipy==1.17.1 @@ -175,8 +179,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 diff --git a/sdks/python/container/py313/base_image_requirements.txt b/sdks/python/container/py313/base_image_requirements.txt index e0b9bfb26df1..68ad2a9530c2 100644 --- a/sdks/python/container/py313/base_image_requirements.txt +++ b/sdks/python/container/py313/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -39,10 +39,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -56,16 +56,17 @@ freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 google-api-core==2.30.3 -google-api-python-client==2.196.0 +google-api-python-client==2.197.0 google-apitools==0.5.35 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -80,15 +81,15 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -99,7 +100,7 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 @@ -132,8 +133,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -160,11 +162,13 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 scipy==1.17.1 @@ -174,8 +178,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 diff --git a/sdks/python/container/py314/base_image_requirements.txt b/sdks/python/container/py314/base_image_requirements.txt index 2ff940c92856..7b9225c5e68d 100644 --- a/sdks/python/container/py314/base_image_requirements.txt +++ b/sdks/python/container/py314/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.13.5 +aiohttp==3.14.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -39,10 +39,10 @@ certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 -cloud-sql-python-connector==1.20.2 +cloud-sql-python-connector==1.20.3 crcmod==1.7 cryptography==47.0.0 -Cython==3.2.4 +Cython==3.2.5 dill==0.3.1.1 distro==1.9.0 dnspython==2.8.0 @@ -59,12 +59,13 @@ google-api-core==2.30.3 google-apitools==0.5.35 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.153.1 +google-cloud-aiplatform==1.154.0 google-cloud-bigquery==3.41.0 google-cloud-bigquery-storage==2.38.0 google-cloud-bigtable==2.38.0 google-cloud-build==3.36.0 google-cloud-core==2.6.0 +google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.24.0 google-cloud-dlp==3.36.0 google-cloud-kms==3.13.0 @@ -79,15 +80,15 @@ google-cloud-storage==3.10.1 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.6.0 +google-genai==2.7.0 google-resumable-media==2.9.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.80.0 -grpcio-status==1.80.0 -grpcio-tools==1.80.0 +grpcio==1.81.0 +grpcio-status==1.81.0 +grpcio-tools==1.81.0 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -98,7 +99,7 @@ httplib2==0.31.2 httpx==0.28.1 hyperframe==6.1.0 hypothesis==6.148.3 -idna==3.16 +idna==3.18 iniconfig==2.3.0 jaraco.classes==3.4.0 jaraco.context==6.1.2 @@ -131,8 +132,9 @@ pandas==2.2.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 -pip==26.1.1 +pip==26.1.2 pluggy==1.6.0 +portalocker==3.2.0 propcache==0.5.2 proto-plus==1.28.0 protobuf==6.33.6 @@ -159,11 +161,13 @@ python-dotenv==1.2.2 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 +qdrant-client==1.18.0 +quickjs-ng==0.15.0.1 referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 -rpds-py==0.30.0 +rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 scipy==1.17.1 @@ -173,8 +177,8 @@ setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soupsieve==2.8.3 -SQLAlchemy==2.0.49 +soupsieve==2.8.4 +SQLAlchemy==2.0.50 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 From 4d5bd2c8a09535c75498b74e20d22cae15f42354 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:33:52 -0400 Subject: [PATCH 292/490] Fix internal test configuration for Dataflow client (#38792) --- sdks/python/apache_beam/runners/dataflow/internal/apiclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 531c076ffdf0..fdf2e0ea03e3 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -520,7 +520,7 @@ def __init__(self, options, root_staging_location=None): transport = None if self.google_cloud_options.dataflow_endpoint: endpoint = self.google_cloud_options.dataflow_endpoint - if 'localhost' in endpoint: + if 'localhost' in endpoint or 'sandbox' in endpoint: transport = 'rest' else: endpoint = re.sub('^https?://', '', endpoint) From 2b52311de184f4727e95a9eef70462a913d0f9dd Mon Sep 17 00:00:00 2001 From: Chamikara Jayalath Date: Wed, 3 Jun 2026 11:20:20 -0700 Subject: [PATCH 293/490] Implements the Delta Lake source with support for splitting (#38706) --- sdks/java/io/delta/build.gradle | 44 ++ .../apache/beam/sdk/io/delta/BeamEngine.java | 55 ++ .../beam/sdk/io/delta/BeamParquetHandler.java | 376 +++++++++ .../sdk/io/delta/CreateReadTasksDoFn.java | 140 ++++ .../org/apache/beam/sdk/io/delta/DeltaIO.java | 92 ++- .../beam/sdk/io/delta/DeltaReadTask.java | 88 +++ .../sdk/io/delta/DeltaReadTaskTracker.java | 56 ++ .../beam/sdk/io/delta/DeltaSourceDoFn.java | 455 +++++++++++ .../beam/sdk/io/delta/SerializableRow.java | 544 +++++++++++++ .../sdk/io/delta/SerializableStructType.java | 69 ++ .../apache/beam/sdk/io/delta/DeltaIOTest.java | 712 +++++++++++++++++- .../sdk/io/delta/SerializableRowTest.java | 498 ++++++++++++ 12 files changed, 3125 insertions(+), 4 deletions(-) create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java create mode 100644 sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java diff --git a/sdks/java/io/delta/build.gradle b/sdks/java/io/delta/build.gradle index 617965b3bc4e..c07aef6981b9 100644 --- a/sdks/java/io/delta/build.gradle +++ b/sdks/java/io/delta/build.gradle @@ -26,6 +26,10 @@ applyJavaNature( description = "Apache Beam :: SDKs :: Java :: IO :: Delta Lake" ext.summary = "Integration with Delta Lake." +// We need to override the GCS bigdataos connector version to prevent conflicts. +def bigdataoss_gcs_connector_version = "4.0.4" + +def parquet_version = "1.16.0" dependencies { implementation project(path: ":sdks:java:core", configuration: "shadow") @@ -35,5 +39,45 @@ dependencies { permitUnusedDeclared library.java.delta_kernel_api permitUnusedDeclared library.java.delta_kernel_defaults + implementation library.java.hadoop_common + implementation library.java.joda_time + // implementation library.java.slf4j_api + implementation "org.apache.parquet:parquet-column:$parquet_version" + implementation "org.apache.parquet:parquet-hadoop:$parquet_version" + + // We need to override the GCS connector version to prevent conflicts with + // latest Hadoop. + implementation "com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version" + implementation "com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version" + implementation "com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version" + implementation "com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version" + permitUnusedDeclared "com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version" + permitUnusedDeclared "com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version" + permitUnusedDeclared "com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version" + permitUnusedDeclared "com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version" + + // For Avro conversions + testImplementation project(":sdks:java:extensions:avro") + + testImplementation library.java.avro testImplementation library.java.junit + testImplementation library.java.hamcrest + testImplementation "org.apache.parquet:parquet-avro:$parquet_version" + testImplementation project(":sdks:java:io:parquet") + testImplementation project(":sdks:java:managed") + testRuntimeOnly "org.yaml:snakeyaml:2.0" + testImplementation project(path: ":runners:direct-java", configuration: "shadow") +} + +configurations.all { + // Exclude conflicting logging frameworks + exclude group: "org.apache.logging.log4j", module: "log4j-slf4j2-impl" + exclude group: "org.apache.logging.log4j", module: "log4j-slf4j-impl" + exclude group: "org.slf4j", module: "slf4j-reload4j" + + // Force overriding for all configurations + resolutionStrategy.force "com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version" + resolutionStrategy.force "com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version" + resolutionStrategy.force "com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version" + resolutionStrategy.force "com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version" } diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java new file mode 100644 index 000000000000..de82d8d01b81 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.engine.Engine; +import io.delta.kernel.engine.ExpressionHandler; +import io.delta.kernel.engine.FileSystemClient; +import io.delta.kernel.engine.JsonHandler; +import io.delta.kernel.engine.ParquetHandler; + +/** A Beam specific {@link Engine} wrapper that provides a custom {@link ParquetHandler}. */ +public class BeamEngine implements Engine { + private final Engine delegate; + private final ParquetHandler parquetHandler; + + public BeamEngine(Engine delegate, ParquetHandler parquetHandler) { + this.delegate = delegate; + this.parquetHandler = parquetHandler; + } + + @Override + public ExpressionHandler getExpressionHandler() { + return delegate.getExpressionHandler(); + } + + @Override + public JsonHandler getJsonHandler() { + return delegate.getJsonHandler(); + } + + @Override + public FileSystemClient getFileSystemClient() { + return delegate.getFileSystemClient(); + } + + @Override + public ParquetHandler getParquetHandler() { + return parquetHandler; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java new file mode 100644 index 000000000000..0327bddac280 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.defaults.internal.parquet.ParquetFileReader.BatchReadSupport; +import io.delta.kernel.engine.FileReadResult; +import io.delta.kernel.engine.ParquetHandler; +import io.delta.kernel.expressions.Column; +import io.delta.kernel.expressions.Predicate; +import io.delta.kernel.types.MetadataColumnSpec; +import io.delta.kernel.types.StructType; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.DataFileStatus; +import io.delta.kernel.utils.FileStatus; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.parquet.column.page.PageReadStore; +import org.apache.parquet.filter2.compat.FilterCompat; +import org.apache.parquet.format.converter.ParquetMetadataConverter; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.api.InitContext; +import org.apache.parquet.hadoop.api.ReadSupport; +import org.apache.parquet.hadoop.metadata.FileMetaData; +import org.apache.parquet.hadoop.metadata.ParquetMetadata; +import org.apache.parquet.hadoop.util.HadoopInputFile; +import org.apache.parquet.io.ColumnIOFactory; +import org.apache.parquet.io.MessageColumnIO; +import org.apache.parquet.io.RecordReader; +import org.apache.parquet.io.api.RecordMaterializer; +import org.apache.parquet.schema.MessageType; + +/** + * A Beam specific {@link ParquetHandler} that delegates row group claiming to a {@link + * DeltaReadTaskTracker}. + */ +public class BeamParquetHandler implements ParquetHandler { + private final Configuration conf; + private final ParquetHandler delegate; + private final RestrictionTracker tracker; + private static final long DEFAULT_START_RG_INDEX = 0L; + + public BeamParquetHandler( + Configuration conf, ParquetHandler delegate, RestrictionTracker tracker) { + this.conf = conf; + this.delegate = delegate; + this.tracker = tracker; + } + + private boolean claimFailed = false; + + /** + * A method that is expected to be called after the first file processing is done. It returns + * whether the last file process resulted in a claim failure. This allows the caller to skip + * trying to read the remaining files of the task which would result in claim failures for each + * row group within them. + * + * @return true, if the last file process resulted in a claim failure. Returns false otherwise. + */ + public boolean hasClaimFailed() { + return claimFailed; + } + + @Override + public CloseableIterator readParquetFiles( + CloseableIterator fileIter, + StructType physicalSchema, + Optional predicate) + throws IOException { + return readParquetFiles(fileIter, physicalSchema, predicate, DEFAULT_START_RG_INDEX); + } + + /** + * Reads Parquet files starting from a given row group index. + * + *

    This takes the {@code RestrictionTracker} referenced by the current {@code ParquetReader} + * into consideration when reading by performing the following. + * + *

    * Skips blocks of the set of files till the given start row group index or the start point + * of the {@code RestrictionTracker}, whatever is higher. * Invokes {@code tryClaim} when reading + * a specific block stops reading if a {@code tryClaim} fails. * Stops reading if the end of the + * range of the {@code RestrictionTracker} is reached. + * + *

    If {@code tryClaim} fails during reading, subsequent {@code hasClaimFailed} calls will + * return {@code true}, so the caller can skip reading subsequent files that are in the range + * being considered for reading. + */ + public CloseableIterator readParquetFiles( + CloseableIterator fileIter, + StructType physicalSchema, + Optional predicate, + long startRgIndex) + throws IOException { + + List> results = new ArrayList<>(); + boolean hasRowIndexCol = physicalSchema.contains(MetadataColumnSpec.ROW_INDEX); + + long currentRgIndex = startRgIndex; + + try { + while (fileIter.hasNext()) { + if (currentRgIndex >= tracker.currentRestriction().getTo()) { + // Skipping all blocks for the remaining files since they are located after the + // end index of the tracker. Since currentRgIndex is monotonically increasing, + // we can break the loop immediately to avoid extremely expensive network I/O. + break; + } + + FileStatus fileStatus = fileIter.next(); + Path hadoopPath = new Path(fileStatus.getPath()); + ParquetMetadata metadata = + ParquetFileReader.readFooter(conf, hadoopPath, ParquetMetadataConverter.NO_FILTER); + long fileBlocks = metadata.getBlocks().size(); + + if (currentRgIndex + fileBlocks <= tracker.currentRestriction().getFrom()) { + // Skipping all blocks for the current file since they are located before the + // start index of the tracker. + currentRgIndex += fileBlocks; + continue; + } + + results.add( + readParquetFileDirect( + fileStatus, + hadoopPath, + metadata, + physicalSchema, + hasRowIndexCol, + currentRgIndex, + fileBlocks)); + + currentRgIndex += fileBlocks; + } + } finally { + fileIter.close(); + } + + return combineResults(results); + } + + // Reads the correct set of blocks that belong to the given Parquet file that + // are within range for the current `RestrictionTracker`. If the current file + // has some blocks that are within the tracker's range and some that are + // outside, + // this will only read the blocks that are within the range. + private CloseableIterator readParquetFileDirect( + FileStatus fileStatus, + Path hadoopPath, + ParquetMetadata metadata, + StructType physicalSchema, + boolean hasRowIndexCol, + long startRgIndex, + long fileBlocks) { + + return new CloseableIterator() { + @javax.annotation.Nullable private ParquetFileReader reader = null; + @javax.annotation.Nullable private BatchReadSupport readSupport = null; + @javax.annotation.Nullable private RecordMaterializer recordConverter = null; + @javax.annotation.Nullable private MessageColumnIO columnIO = null; + + private long currentRgOffset = 0; + @javax.annotation.Nullable private RecordReader currentRecordReader = null; + private long currentRgTotalRows = 0; + private long currentRgRowOffset = 0; + private long currentRgStartingRowIndex = 0; + + @javax.annotation.Nullable private FileReadResult nextResult = null; + private boolean isDone = false; + + private void initReaderIfRequired() throws IOException { + if (reader != null) { + return; + } + HadoopInputFile inputFile = HadoopInputFile.fromPath(hadoopPath, conf); + ParquetFileReader localReader = ParquetFileReader.open(inputFile); + reader = localReader; + + FileMetaData fileMetaData = metadata.getFileMetaData(); + MessageType fileSchema = fileMetaData.getSchema(); + Map> keyValueMetadata = new HashMap<>(); + if (fileMetaData.getKeyValueMetaData() != null) { + for (Map.Entry entry : fileMetaData.getKeyValueMetaData().entrySet()) { + keyValueMetadata.put(entry.getKey(), Collections.singleton(entry.getValue())); + } + } + + BatchReadSupport localReadSupport = new BatchReadSupport(1024, physicalSchema); + readSupport = localReadSupport; + ReadSupport.ReadContext readContext = + localReadSupport.init(new InitContext(conf, keyValueMetadata, fileSchema)); + RecordMaterializer localRecordConverter = + localReadSupport.prepareForRead( + conf, fileMetaData.getKeyValueMetaData(), fileSchema, readContext); + recordConverter = localRecordConverter; + localReader.setRequestedSchema(readContext.getRequestedSchema()); + + ColumnIOFactory columnIOFactory = new ColumnIOFactory(fileMetaData.getCreatedBy()); + columnIO = columnIOFactory.getColumnIO(readContext.getRequestedSchema(), fileSchema, true); + } + + @Override + public boolean hasNext() { + if (isDone) { + return false; + } + if (nextResult != null) { + return true; + } + + try { + initReaderIfRequired(); + ParquetFileReader localReader = reader; + BatchReadSupport localReadSupport = readSupport; + MessageColumnIO localColumnIO = columnIO; + RecordMaterializer localRecordConverter = recordConverter; + if (localReader == null + || localReadSupport == null + || localColumnIO == null + || localRecordConverter == null) { + throw new IllegalStateException("Reader not initialized"); + } + + while (true) { + RecordReader localRecordReader = currentRecordReader; + if (localRecordReader != null && currentRgRowOffset < currentRgTotalRows) { + int batchSize = (int) Math.min(1024L, currentRgTotalRows - currentRgRowOffset); + for (int i = 0; i < batchSize; i++) { + localRecordReader.read(); + long rowIndex = + hasRowIndexCol ? (currentRgStartingRowIndex + currentRgRowOffset + i) : -1L; + localReadSupport.finalizeCurrentRow(rowIndex); + } + currentRgRowOffset += batchSize; + io.delta.kernel.data.ColumnarBatch batch = + localReadSupport.getDataAsColumnarBatch(batchSize); + nextResult = new FileReadResult(batch, fileStatus.getPath()); + return true; + } + + currentRecordReader = null; + if (currentRgOffset >= fileBlocks) { + isDone = true; + return false; + } + + // Checking the range for specific row groups. + long rgIndex = startRgIndex + currentRgOffset; + if (rgIndex < tracker.currentRestriction().getFrom()) { + // Skip till we get to the first block to read. + localReader.skipNextRowGroup(); + currentRgOffset++; + continue; + } else if (rgIndex >= tracker.currentRestriction().getTo()) { + // Once we are past the end index of the tracker we don't have to read any more + // blocks. + isDone = true; + return false; + } + + // We only read the row group if it's within the range for the + // RestrictionTracker. + if (tracker.tryClaim(rgIndex)) { + PageReadStore pages = localReader.readNextRowGroup(); + currentRecordReader = + localColumnIO.getRecordReader(pages, localRecordConverter, FilterCompat.NOOP); + currentRgTotalRows = pages.getRowCount(); + currentRgRowOffset = 0; + currentRgStartingRowIndex = pages.getRowIndexOffset().orElse(0L); + currentRgOffset++; + } else { + // Mark claim failed for the current row group, so we stop processing + // the remaining row groups in the source. + claimFailed = true; + isDone = true; + return false; + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public FileReadResult next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + FileReadResult res = nextResult; + if (res == null) { + throw new NoSuchElementException(); + } + nextResult = null; + return res; + } + + @Override + public void close() throws IOException { + if (reader != null) { + reader.close(); + } + } + }; + } + + @Override + public void writeParquetFileAtomically( + String filePath, CloseableIterator data) throws IOException { + delegate.writeParquetFileAtomically(filePath, data); + } + + @Override + public CloseableIterator writeParquetFiles( + String filePath, CloseableIterator data, List statsColumns) + throws IOException { + return delegate.writeParquetFiles(filePath, data, statsColumns); + } + + private static CloseableIterator combineResults( + List> iterators) { + return new CloseableIterator() { + private int currentIdx = 0; + + @Override + public boolean hasNext() { + while (currentIdx < iterators.size()) { + if (iterators.get(currentIdx).hasNext()) { + return true; + } + currentIdx++; + } + return false; + } + + @Override + public FileReadResult next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return iterators.get(currentIdx).next(); + } + + @Override + public void close() throws IOException { + for (CloseableIterator it : iterators) { + it.close(); + } + } + }; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java new file mode 100644 index 000000000000..36c9a1a47f8c --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.Scan; +import io.delta.kernel.Snapshot; +import io.delta.kernel.Table; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.data.Row; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.internal.InternalScanFileUtils; +import io.delta.kernel.utils.CloseableIterator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.hadoop.conf.Configuration; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A DoFn that reads the Delta log and outputs a list of DeltaReadTask records to read. */ +class CreateReadTasksDoFn extends DoFn { + private static final long MAX_TASK_SIZE_BYTES = 1024L * 1024L * 1024L; // 1 GB + private final @Nullable Map hadoopConfig; + + public CreateReadTasksDoFn(@Nullable Map hadoopConfig) { + this.hadoopConfig = hadoopConfig; + } + + @ProcessElement + public void processElement(@Element String tablePath, OutputReceiver out) + throws Exception { + Configuration conf = new Configuration(); + if (hadoopConfig != null) { + for (Map.Entry entry : hadoopConfig.entrySet()) { + conf.set(entry.getKey(), entry.getValue()); + } + } + Engine engine = DefaultEngine.create(conf); + Table table = Table.forPath(engine, tablePath); + Snapshot snapshot = table.getLatestSnapshot(engine); + Scan scan = snapshot.getScanBuilder().build(); + Row scanState = scan.getScanState(engine); + SerializableRow serializableScanState = new SerializableRow(scanState); + + List currentGroup = new ArrayList<>(); + List> currentGroupRowGroupSizes = new ArrayList<>(); + long currentGroupSize = 0L; + + try (CloseableIterator scanFiles = scan.getScanFiles(engine)) { + while (scanFiles.hasNext()) { + FilteredColumnarBatch batch = scanFiles.next(); + try (CloseableIterator rows = batch.getRows()) { + while (rows.hasNext()) { + Row scanFileRow = rows.next(); + SerializableRow fileRow = new SerializableRow(scanFileRow); + long fileSize = InternalScanFileUtils.getAddFileStatus(fileRow).getSize(); + List fileRowGroupSizes = getRowGroupSizes(fileRow, conf); + + if (fileSize >= MAX_TASK_SIZE_BYTES) { + if (!currentGroup.isEmpty()) { + DeltaReadTask readTask = + new DeltaReadTask( + currentGroup, serializableScanState, currentGroupRowGroupSizes); + out.output(readTask); + currentGroup = new ArrayList<>(); + currentGroupRowGroupSizes = new ArrayList<>(); + currentGroupSize = 0L; + } + + DeltaReadTask readTask = + new DeltaReadTask( + Collections.singletonList(fileRow), + serializableScanState, + Collections.singletonList(fileRowGroupSizes)); + out.output(readTask); + } else { + if (currentGroupSize + fileSize > MAX_TASK_SIZE_BYTES) { + DeltaReadTask readTask = + new DeltaReadTask( + currentGroup, serializableScanState, currentGroupRowGroupSizes); + out.output(readTask); + currentGroup = new ArrayList<>(); + currentGroupRowGroupSizes = new ArrayList<>(); + currentGroup.add(fileRow); + currentGroupRowGroupSizes.add(fileRowGroupSizes); + currentGroupSize = fileSize; + } else { + currentGroup.add(fileRow); + currentGroupRowGroupSizes.add(fileRowGroupSizes); + currentGroupSize += fileSize; + } + } + } + } + } + } + + if (!currentGroup.isEmpty()) { + DeltaReadTask readTask = + new DeltaReadTask(currentGroup, serializableScanState, currentGroupRowGroupSizes); + out.output(readTask); + } + } + + private List getRowGroupSizes(SerializableRow scanFileRow, Configuration conf) { + List sizes = new ArrayList<>(); + String pathStr = InternalScanFileUtils.getAddFileStatus(scanFileRow).getPath(); + try { + org.apache.hadoop.fs.Path hadoopPath = new org.apache.hadoop.fs.Path(pathStr); + org.apache.parquet.hadoop.metadata.ParquetMetadata metadata = + org.apache.parquet.hadoop.ParquetFileReader.readFooter( + conf, + hadoopPath, + org.apache.parquet.format.converter.ParquetMetadataConverter.NO_FILTER); + for (org.apache.parquet.hadoop.metadata.BlockMetaData block : metadata.getBlocks()) { + sizes.add(block.getTotalByteSize()); + } + } catch (java.io.IOException e) { + throw new RuntimeException("Failed to read Parquet footer for " + pathStr, e); + } + return sizes; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java index 6c5df4728b4e..c511a7380dc1 100644 --- a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java @@ -18,12 +18,33 @@ package org.apache.beam.sdk.io.delta; import com.google.auto.value.AutoValue; +import io.delta.kernel.Table; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; import java.util.Map; import org.apache.beam.sdk.annotations.Internal; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PBegin; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; +import org.apache.hadoop.conf.Configuration; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -83,9 +104,74 @@ public ReadRows withConfig(Map config) { @Override public PCollection expand(PBegin input) { - // TODO(https://github.com/apache/beam/issues/38551): Implement expansion for - // Delta Lake ReadRows - throw new UnsupportedOperationException("Not implemented yet."); + String path = getTablePath(); + if (path == null) { + throw new IllegalArgumentException("Table path must be set."); + } + + Configuration conf = new Configuration(); + Map hadoopConfig = getHadoopConfig(); + if (hadoopConfig != null) { + for (Map.Entry entry : hadoopConfig.entrySet()) { + conf.set(entry.getKey(), entry.getValue()); + } + } + Engine engine = DefaultEngine.create(conf); + Table table = Table.forPath(engine, path); + io.delta.kernel.Snapshot snapshot = table.getLatestSnapshot(engine); + StructType deltaSchema = snapshot.getSchema(); + if (deltaSchema == null) { + throw new IllegalStateException("Table schema is null."); + } + Schema beamSchema = convertToBeamSchema(deltaSchema); + + return input + .apply("Create Path", Create.of(path)) + .apply("Plan Files", ParDo.of(new CreateReadTasksDoFn(hadoopConfig))) + .apply("Read Logical Data", ParDo.of(new DeltaSourceDoFn(hadoopConfig))) + .setRowSchema(beamSchema); + } + + static Schema convertToBeamSchema(StructType deltaSchema) { + Schema.Builder builder = Schema.builder(); + for (StructField field : deltaSchema.fields()) { + builder.addField(field.getName(), convertToBeamFieldType(field.getDataType())); + } + return builder.build(); + } + + static Schema.FieldType convertToBeamFieldType(DataType deltaType) { + if (deltaType instanceof StringType) { + return Schema.FieldType.STRING; + } else if (deltaType instanceof IntegerType) { + return Schema.FieldType.INT32; + } else if (deltaType instanceof LongType) { + return Schema.FieldType.INT64; + } else if (deltaType instanceof FloatType) { + return Schema.FieldType.FLOAT; + } else if (deltaType instanceof DoubleType) { + return Schema.FieldType.DOUBLE; + } else if (deltaType instanceof BooleanType) { + return Schema.FieldType.BOOLEAN; + } else if (deltaType instanceof BinaryType) { + return Schema.FieldType.BYTES; + } else if (deltaType instanceof TimestampType) { + return Schema.FieldType.DATETIME; + } else if (deltaType instanceof DateType) { + return Schema.FieldType.DATETIME; + } else if (deltaType instanceof ArrayType) { + DataType elementType = ((ArrayType) deltaType).getElementType(); + return Schema.FieldType.iterable(convertToBeamFieldType(elementType)); + } else if (deltaType instanceof MapType) { + DataType keyType = ((MapType) deltaType).getKeyType(); + DataType valueType = ((MapType) deltaType).getValueType(); + return Schema.FieldType.map( + convertToBeamFieldType(keyType), convertToBeamFieldType(valueType)); + } else if (deltaType instanceof StructType) { + return Schema.FieldType.row(convertToBeamSchema((StructType) deltaType)); + } else { + throw new UnsupportedOperationException("Unsupported Delta type: " + deltaType.getClass()); + } } } } diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java new file mode 100644 index 000000000000..10dbce43a75b --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable task containing the necessary metadata to read a group of files in a Delta table. + * Packs both the {@code scanFileRows} (representing the physical files and deletion vectors) and + * {@code scanStateRow} (containing snapshot-level read schemas, configuration, and options). + */ +public class DeltaReadTask implements Serializable { + private static final long serialVersionUID = 1L; + + private final List scanFileRows; + private final SerializableRow scanStateRow; + private final List> rowGroupSizesPerFile; + + public DeltaReadTask( + List scanFileRows, + SerializableRow scanStateRow, + List> rowGroupSizesPerFile) { + this.scanFileRows = scanFileRows; + this.scanStateRow = scanStateRow; + this.rowGroupSizesPerFile = rowGroupSizesPerFile; + } + + public List getScanFileRows() { + return scanFileRows; + } + + public SerializableRow getScanStateRow() { + return scanStateRow; + } + + public List> getRowGroupSizesPerFile() { + return rowGroupSizesPerFile; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DeltaReadTask)) { + return false; + } + DeltaReadTask that = (DeltaReadTask) o; + return Objects.equals(scanFileRows, that.scanFileRows) + && Objects.equals(scanStateRow, that.scanStateRow) + && Objects.equals(rowGroupSizesPerFile, that.rowGroupSizesPerFile); + } + + @Override + public int hashCode() { + return Objects.hash(scanFileRows, scanStateRow, rowGroupSizesPerFile); + } + + @Override + public String toString() { + return "DeltaReadTask{" + + "scanFileRows=" + + scanFileRows + + ", scanStateRow=" + + scanStateRow + + ", rowGroupSizesPerFile=" + + rowGroupSizesPerFile + + '}'; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java new file mode 100644 index 000000000000..c81a2a231713 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import java.util.List; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker; + +/** + * A {@link org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker} for tracking progress + * across Parquet row groups represented by a {@link DeltaReadTask}. + */ +public class DeltaReadTaskTracker extends OffsetRangeTracker { + private final List rowGroupSizes; + + public DeltaReadTaskTracker(OffsetRange restriction, List rowGroupSizes) { + super(restriction); + this.rowGroupSizes = rowGroupSizes; + } + + @Override + public Progress getProgress() { + long workCompleted = 0L; + long workRemaining = 0L; + long from = range.getFrom(); + long to = range.getTo(); + long attempted = lastAttemptedOffset == null ? (from - 1) : lastAttemptedOffset; + + for (int i = (int) from; i < (int) to; i++) { + // Upper bound of the range is the number of row groups. + if (i < rowGroupSizes.size()) { + if (i <= attempted) { + workCompleted += rowGroupSizes.get(i); + } else { + workRemaining += rowGroupSizes.get(i); + } + } + } + return Progress.from((double) workCompleted, (double) workRemaining); + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java new file mode 100644 index 000000000000..bd53c3c9d045 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java @@ -0,0 +1,455 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.Scan; +import io.delta.kernel.data.ArrayValue; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.ColumnarBatch; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.data.MapValue; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.engine.FileReadResult; +import io.delta.kernel.internal.InternalScanFileUtils; +import io.delta.kernel.internal.data.ScanStateRow; +import io.delta.kernel.internal.util.Utils; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.ByteType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.ShortType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.FileStatus; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.values.Row; +import org.apache.hadoop.conf.Configuration; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A Splittable DoFn that processes {@link DeltaReadTask} elements, performs logical reads, and + * supports dynamic work rebalancing. + */ +@DoFn.BoundedPerElement +class DeltaSourceDoFn extends DoFn { + @Nullable Map hadoopConfig; + private transient @Nullable Engine engine; + private transient @Nullable Configuration conf; + + public DeltaSourceDoFn(@Nullable Map hadoopConfig) { + this.hadoopConfig = hadoopConfig; + } + + private synchronized Configuration getConfiguration() { + Configuration localConf = conf; + if (localConf == null) { + localConf = new Configuration(); + if (hadoopConfig != null) { + for (Map.Entry entry : hadoopConfig.entrySet()) { + localConf.set(entry.getKey(), entry.getValue()); + } + } + conf = localConf; + } + return localConf; + } + + // Returns the sizes of the row groups for a given DeltaReadTask. + private List getRowGroupSizes(DeltaReadTask task) { + List sizes = new ArrayList<>(); + for (List fileSizes : task.getRowGroupSizesPerFile()) { + sizes.addAll(fileSizes); + } + return sizes; + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction(@Element DeltaReadTask task) { + List rowGroupSizes = getRowGroupSizes(task); + // Note that we use the number of row groups, `rowGroupSizes.size()`, here + // as the upper bound not the byte size of row groups. + return new OffsetRange(0L, rowGroupSizes.size()); + } + + @NewTracker + public DeltaReadTaskTracker newTracker( + @Restriction OffsetRange restriction, @Element DeltaReadTask task) { + return new DeltaReadTaskTracker(restriction, getRowGroupSizes(task)); + } + + @Setup + public void setUp() { + engine = DefaultEngine.create(getConfiguration()); + } + + @ProcessElement + public ProcessContinuation processElement( + @Element DeltaReadTask task, + RestrictionTracker tracker, + OutputReceiver out) + throws Exception { + + SerializableRow scanStateRow = task.getScanStateRow(); + StructType physicalSchema = ScanStateRow.getPhysicalDataReadSchema(scanStateRow); + StructType logicalSchema = ScanStateRow.getLogicalSchema(scanStateRow); + Schema beamSchema = DeltaIO.ReadRows.convertToBeamSchema(logicalSchema); + + Engine currentEngine = engine; + if (currentEngine == null) { + throw new IllegalArgumentException("Expected the engine to not be null"); + } + + // `BeamParquetHandler` takes a reference to the `RestrictionTracker` so that it + // can perform `getFrom`, `getTo`, `tryClaim` requests to return the correct set + // of row groups that map to the current restriction. + BeamParquetHandler parquetHandler = + new BeamParquetHandler(getConfiguration(), currentEngine.getParquetHandler(), tracker); + BeamEngine beamEngine = new BeamEngine(currentEngine, parquetHandler); + + long currentStartRgIndex = 0L; + + // We have to go through files in the `DeltaReadTask` in order so that the + // `RestrictionTracker` + // can correctly handle the range of the current split. + List scanFileRows = task.getScanFileRows(); + List> rowGroupSizesPerFile = task.getRowGroupSizesPerFile(); + for (int i = 0; i < scanFileRows.size(); i++) { + if (currentStartRgIndex >= tracker.currentRestriction().getTo()) { + // Breaking early to prevent metadata reads. + break; + } + SerializableRow scanFileRow = scanFileRows.get(i); + FileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow); + + long fileBlocks = rowGroupSizesPerFile.get(i).size(); + + try (CloseableIterator fileReadResults = + parquetHandler.readParquetFiles( + Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + Optional.empty(), + currentStartRgIndex)) { + + // Get the correct set of physical data for the current file that are within the + // range for the current `RestrictionTracker`. + CloseableIterator physicalData = + new CloseableIterator() { + @Override + public void close() throws java.io.IOException {} + + @Override + public boolean hasNext() { + return fileReadResults.hasNext(); + } + + @Override + public ColumnarBatch next() { + return fileReadResults.next().getData(); + } + }; + + // Convert physical data to logical data. + try (CloseableIterator logicalBatches = + Scan.transformPhysicalData(beamEngine, scanStateRow, scanFileRow, physicalData)) { + + while (logicalBatches.hasNext()) { + FilteredColumnarBatch batch = logicalBatches.next(); + try (CloseableIterator logicalRows = batch.getRows()) { + while (logicalRows.hasNext()) { + io.delta.kernel.data.Row deltaRow = logicalRows.next(); + Row beamRow = toBeamRow(deltaRow, beamSchema); + out.output(beamRow); + } + } + } + } + } + + // Advance the total number of blocked handled so far. + currentStartRgIndex += fileBlocks; + + // If the tryClaim failed during processing of the current file, there's no need + // to look at the rest of the files within the task. + if (parquetHandler.hasClaimFailed()) { + break; + } + } + return ProcessContinuation.stop(); + } + + // Convert Delta `Row` to Beam `Row`. + private static Row toBeamRow(io.delta.kernel.data.Row deltaRow, Schema beamSchema) { + Row.Builder builder = Row.withSchema(beamSchema); + StructType deltaSchema = deltaRow.getSchema(); + List fields = deltaSchema.fields(); + for (int i = 0; i < fields.size(); i++) { + StructField field = fields.get(i); + builder.addValue(getFieldValue(deltaRow, i, field.getDataType())); + } + return builder.build(); + } + + // Returns the value at a specific index in a given row. + private static @Nullable Object getFieldValue( + io.delta.kernel.data.Row row, int index, DataType type) { + if (row.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return row.getBoolean(index); + } else if (type instanceof ByteType) { + return (int) row.getByte(index); + } else if (type instanceof ShortType) { + return (int) row.getShort(index); + } else if (type instanceof IntegerType) { + return row.getInt(index); + } else if (type instanceof LongType) { + return row.getLong(index); + } else if (type instanceof FloatType) { + return row.getFloat(index); + } else if (type instanceof DoubleType) { + return row.getDouble(index); + } else if (type instanceof StringType) { + return row.getString(index); + } else if (type instanceof BinaryType) { + return row.getBinary(index); + } else if (type instanceof TimestampType) { + long microSeconds = row.getLong(index); + return new org.joda.time.Instant(microSeconds / 1000L); + } else if (type instanceof DateType) { + int daysSinceEpoch = row.getInt(index); + return new org.joda.time.Instant(daysSinceEpoch * 86400000L); + } else if (type instanceof ArrayType) { + ArrayValue arrayValue = row.getArray(index); + int size = arrayValue.getSize(); + ColumnVector elements = arrayValue.getElements(); + DataType elementType = ((ArrayType) type).getElementType(); + List<@Nullable Object> list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(getVectorValue(elements, i, elementType)); + } + return list; + } else if (type instanceof MapType) { + MapValue mapValue = row.getMap(index); + int size = mapValue.getSize(); + ColumnVector keys = mapValue.getKeys(); + ColumnVector values = mapValue.getValues(); + DataType keyType = ((MapType) type).getKeyType(); + DataType valueType = ((MapType) type).getValueType(); + Map map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object key = getVectorValue(keys, i, keyType); + if (key != null) { + map.put(key, getVectorValue(values, i, valueType)); + } + } + return map; + } else if (type instanceof StructType) { + io.delta.kernel.data.Row nestedRow = row.getStruct(index); + Schema nestedBeamSchema = DeltaIO.ReadRows.convertToBeamSchema((StructType) type); + return toBeamRow(nestedRow, nestedBeamSchema); + } + throw new UnsupportedOperationException("Unsupported type: " + type.getClass()); + } + + // Returns the value at a specific index in a given column vector. + private static @Nullable Object getVectorValue(ColumnVector vector, int index, DataType type) { + if (vector.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return vector.getBoolean(index); + } else if (type instanceof ByteType) { + return (int) vector.getByte(index); + } else if (type instanceof ShortType) { + return (int) vector.getShort(index); + } else if (type instanceof IntegerType) { + return vector.getInt(index); + } else if (type instanceof LongType) { + return vector.getLong(index); + } else if (type instanceof FloatType) { + return vector.getFloat(index); + } else if (type instanceof DoubleType) { + return vector.getDouble(index); + } else if (type instanceof StringType) { + return vector.getString(index); + } else if (type instanceof BinaryType) { + return vector.getBinary(index); + } else if (type instanceof TimestampType) { + long microSeconds = vector.getLong(index); + return new org.joda.time.Instant(microSeconds / 1000L); + } else if (type instanceof DateType) { + // Convert days since epoch to milliseconds since epoch. + int daysSinceEpoch = vector.getInt(index); + return new org.joda.time.Instant(daysSinceEpoch * 24L * 60L * 60L * 1000L); + } else if (type instanceof ArrayType) { + ArrayValue arrayValue = vector.getArray(index); + int size = arrayValue.getSize(); + ColumnVector elements = arrayValue.getElements(); + DataType elementType = ((ArrayType) type).getElementType(); + List<@Nullable Object> list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(getVectorValue(elements, i, elementType)); + } + return list; + } else if (type instanceof MapType) { + MapValue mapValue = vector.getMap(index); + int size = mapValue.getSize(); + ColumnVector keys = mapValue.getKeys(); + ColumnVector values = mapValue.getValues(); + DataType keyType = ((MapType) type).getKeyType(); + DataType valueType = ((MapType) type).getValueType(); + Map map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object key = getVectorValue(keys, i, keyType); + if (key != null) { + map.put(key, getVectorValue(values, i, valueType)); + } + } + return map; + } else if (type instanceof StructType) { + StructType structType = (StructType) type; + int numFields = structType.fields().size(); + ColumnVector[] childVectors = new ColumnVector[numFields]; + for (int i = 0; i < numFields; i++) { + childVectors[i] = vector.getChild(i); + } + io.delta.kernel.data.Row nestedRow = new VectorRow(structType, childVectors, index); + Schema nestedBeamSchema = DeltaIO.ReadRows.convertToBeamSchema(structType); + return toBeamRow(nestedRow, nestedBeamSchema); + } + throw new UnsupportedOperationException("Unsupported vector type: " + type.getClass()); + } + + // A new new Delta Row type to efficiently convert columnar data from Delta Lake + // to Beam Rows. We use this to store columnar vectors without creating + // additinal memory copies for all fields but return values of the specific + // row index. + private static class VectorRow implements io.delta.kernel.data.Row { + private final StructType schema; + private final ColumnVector[] fields; + private final int rowIndex; + + VectorRow(StructType schema, ColumnVector[] fields, int rowIndex) { + this.schema = schema; + this.fields = fields; + this.rowIndex = rowIndex; + } + + @Override + public StructType getSchema() { + return schema; + } + + @Override + public boolean isNullAt(int ord) { + return fields[ord].isNullAt(rowIndex); + } + + @Override + public boolean getBoolean(int ord) { + return fields[ord].getBoolean(rowIndex); + } + + @Override + public byte getByte(int ord) { + return fields[ord].getByte(rowIndex); + } + + @Override + public short getShort(int ord) { + return fields[ord].getShort(rowIndex); + } + + @Override + public int getInt(int ord) { + return fields[ord].getInt(rowIndex); + } + + @Override + public long getLong(int ord) { + return fields[ord].getLong(rowIndex); + } + + @Override + public float getFloat(int ord) { + return fields[ord].getFloat(rowIndex); + } + + @Override + public double getDouble(int ord) { + return fields[ord].getDouble(rowIndex); + } + + @Override + public String getString(int ord) { + return fields[ord].getString(rowIndex); + } + + @Override + public byte[] getBinary(int ord) { + return fields[ord].getBinary(rowIndex); + } + + @Override + public BigDecimal getDecimal(int ord) { + return fields[ord].getDecimal(rowIndex); + } + + @Override + public io.delta.kernel.data.Row getStruct(int ord) { + StructType childSchema = (StructType) schema.fields().get(ord).getDataType(); + int numFields = childSchema.fields().size(); + ColumnVector[] childFields = new ColumnVector[numFields]; + for (int j = 0; j < numFields; j++) { + childFields[j] = fields[ord].getChild(j); + } + return new VectorRow(childSchema, childFields, rowIndex); + } + + @Override + public ArrayValue getArray(int ord) { + return fields[ord].getArray(rowIndex); + } + + @Override + public MapValue getMap(int ord) { + return fields[ord].getMap(rowIndex); + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java new file mode 100644 index 000000000000..308956383e79 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java @@ -0,0 +1,544 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.data.ArrayValue; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.MapValue; +import io.delta.kernel.data.Row; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.ByteType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DecimalType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.ShortType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable wrapper for Delta {@link Row} that implements the {@link Row} interface itself, + * allowing worker nodes to access serialized Row objects using standard Delta Kernel APIs. + */ +public class SerializableRow implements Row, Serializable { + private static final long serialVersionUID = 1L; + + private final SerializableStructType schema; + private final @Nullable Object[] values; + + public SerializableRow(Row row) { + this.schema = new SerializableStructType(row.getSchema()); + StructType structType = row.getSchema(); + int numFields = structType.fields().size(); + this.values = new Object[numFields]; + for (int i = 0; i < numFields; i++) { + DataType type = structType.fields().get(i).getDataType(); + this.values[i] = getValue(row, i, type); + } + } + + @Override + public StructType getSchema() { + return schema.get(); + } + + @Override + public boolean isNullAt(int ord) { + return values == null || values[ord] == null; + } + + @Override + public boolean getBoolean(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Boolean) val : false; + } + + @Override + public byte getByte(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Byte) val : 0; + } + + @Override + public short getShort(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Short) val : 0; + } + + @Override + public int getInt(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Integer) val : 0; + } + + @Override + public long getLong(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Long) val : 0L; + } + + @Override + public float getFloat(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Float) val : 0.0f; + } + + @Override + public double getDouble(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Double) val : 0.0d; + } + + @Override + @SuppressWarnings("nullness") + public String getString(int ord) { + return (String) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings("nullness") + public byte[] getBinary(int ord) { + return (byte[]) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings("nullness") + public BigDecimal getDecimal(int ord) { + return (BigDecimal) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings("nullness") + public Row getStruct(int ord) { + return (Row) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings({"unchecked", "nullness"}) + public ArrayValue getArray(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + if (val == null) { + return null; + } + DataType elementType = + ((ArrayType) getSchema().fields().get(ord).getDataType()).getElementType(); + return new SerializableArrayValue((List<@Nullable Object>) val, elementType); + } + + @Override + @SuppressWarnings({"unchecked", "nullness"}) + public MapValue getMap(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + if (val == null) { + return null; + } + MapType mapType = (MapType) getSchema().fields().get(ord).getDataType(); + return new SerializableMapValue( + (Map) val, mapType.getKeyType(), mapType.getValueType()); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SerializableRow)) { + return false; + } + SerializableRow that = (SerializableRow) o; + return Objects.equals(schema, that.schema) && java.util.Arrays.deepEquals(values, that.values); + } + + @Override + public int hashCode() { + return Objects.hash(schema, java.util.Arrays.deepHashCode(values)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("SerializableRow{schema=").append(schema).append(", values=["); + if (values != null) { + for (int i = 0; i < values.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(values[i]); + } + } + sb.append("]}"); + return sb.toString(); + } + + private static @Nullable Object getValue(Row row, int index, DataType type) { + if (row.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return row.getBoolean(index); + } else if (type instanceof ByteType) { + return row.getByte(index); + } else if (type instanceof ShortType) { + return row.getShort(index); + } else if (type instanceof IntegerType) { + return row.getInt(index); + } else if (type instanceof LongType) { + return row.getLong(index); + } else if (type instanceof FloatType) { + return row.getFloat(index); + } else if (type instanceof DoubleType) { + return row.getDouble(index); + } else if (type instanceof StringType) { + return row.getString(index); + } else if (type instanceof BinaryType) { + return row.getBinary(index); + } else if (type instanceof DecimalType) { + return row.getDecimal(index); + } else if (type instanceof StructType) { + return new SerializableRow(row.getStruct(index)); + } else if (type instanceof DateType) { + return row.getInt(index); + } else if (type instanceof TimestampType) { + return row.getLong(index); + } else if (type instanceof ArrayType) { + ArrayValue arr = row.getArray(index); + return convertArray(arr, (ArrayType) type); + } else if (type instanceof MapType) { + MapValue map = row.getMap(index); + return convertMap(map, (MapType) type); + } + throw new IllegalArgumentException("Unsupported type: " + type); + } + + private static @Nullable Object getVectorValue(ColumnVector vector, int index, DataType type) { + if (vector.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return vector.getBoolean(index); + } else if (type instanceof ByteType) { + return vector.getByte(index); + } else if (type instanceof ShortType) { + return vector.getShort(index); + } else if (type instanceof IntegerType) { + return vector.getInt(index); + } else if (type instanceof LongType) { + return vector.getLong(index); + } else if (type instanceof FloatType) { + return vector.getFloat(index); + } else if (type instanceof DoubleType) { + return vector.getDouble(index); + } else if (type instanceof StringType) { + return vector.getString(index); + } else if (type instanceof BinaryType) { + return vector.getBinary(index); + } else if (type instanceof DecimalType) { + return vector.getDecimal(index); + } else if (type instanceof StructType) { + StructType structType = (StructType) type; + int numFields = structType.fields().size(); + ColumnVector[] childFields = new ColumnVector[numFields]; + for (int j = 0; j < numFields; j++) { + childFields[j] = vector.getChild(j); + } + return new SerializableRow(new VectorRow(structType, childFields, index)); + } else if (type instanceof DateType) { + return vector.getInt(index); + } else if (type instanceof TimestampType) { + return vector.getLong(index); + } else if (type instanceof ArrayType) { + ArrayValue arr = vector.getArray(index); + return convertArray(arr, (ArrayType) type); + } else if (type instanceof MapType) { + MapValue map = vector.getMap(index); + return convertMap(map, (MapType) type); + } + throw new IllegalArgumentException("Unsupported vector type: " + type); + } + + private static List<@Nullable Object> convertArray(ArrayValue arr, ArrayType arrayType) { + int size = arr.getSize(); + ColumnVector elements = arr.getElements(); + DataType elementType = arrayType.getElementType(); + List<@Nullable Object> result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + result.add(getVectorValue(elements, i, elementType)); + } + return result; + } + + private static Map convertMap(MapValue map, MapType mapType) { + int size = map.getSize(); + ColumnVector keys = map.getKeys(); + ColumnVector values = map.getValues(); + DataType keyType = mapType.getKeyType(); + DataType valueType = mapType.getValueType(); + Map result = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object key = getVectorValue(keys, i, keyType); + if (key != null) { + result.put(key, getVectorValue(values, i, valueType)); + } + } + return result; + } + + private static class VectorRow implements Row { + private final StructType schema; + private final ColumnVector[] fields; + private final int rowIndex; + + VectorRow(StructType schema, ColumnVector[] fields, int rowIndex) { + this.schema = schema; + this.fields = fields; + this.rowIndex = rowIndex; + } + + @Override + public StructType getSchema() { + return schema; + } + + @Override + public boolean isNullAt(int ord) { + return fields[ord].isNullAt(rowIndex); + } + + @Override + public boolean getBoolean(int ord) { + return fields[ord].getBoolean(rowIndex); + } + + @Override + public byte getByte(int ord) { + return fields[ord].getByte(rowIndex); + } + + @Override + public short getShort(int ord) { + return fields[ord].getShort(rowIndex); + } + + @Override + public int getInt(int ord) { + return fields[ord].getInt(rowIndex); + } + + @Override + public long getLong(int ord) { + return fields[ord].getLong(rowIndex); + } + + @Override + public float getFloat(int ord) { + return fields[ord].getFloat(rowIndex); + } + + @Override + public double getDouble(int ord) { + return fields[ord].getDouble(rowIndex); + } + + @Override + public String getString(int ord) { + return fields[ord].getString(rowIndex); + } + + @Override + public byte[] getBinary(int ord) { + return fields[ord].getBinary(rowIndex); + } + + @Override + public BigDecimal getDecimal(int ord) { + return fields[ord].getDecimal(rowIndex); + } + + @Override + public Row getStruct(int ord) { + StructType childSchema = (StructType) schema.fields().get(ord).getDataType(); + int numFields = childSchema.fields().size(); + ColumnVector[] childFields = new ColumnVector[numFields]; + for (int j = 0; j < numFields; j++) { + childFields[j] = fields[ord].getChild(j); + } + return new VectorRow(childSchema, childFields, rowIndex); + } + + @Override + public ArrayValue getArray(int ord) { + return fields[ord].getArray(rowIndex); + } + + @Override + public MapValue getMap(int ord) { + return fields[ord].getMap(rowIndex); + } + } + + private static class ListColumnVector implements ColumnVector { + private final DataType dataType; + private final List<@Nullable Object> list; + + @SuppressWarnings("unchecked") + ListColumnVector(DataType dataType, List list) { + this.dataType = dataType; + this.list = (List<@Nullable Object>) list; + } + + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public boolean isNullAt(int rowId) { + return list.get(rowId) == null; + } + + @Override + public boolean getBoolean(int rowId) { + Object val = list.get(rowId); + return val != null ? (Boolean) val : false; + } + + @Override + public byte getByte(int rowId) { + Object val = list.get(rowId); + return val != null ? (Byte) val : 0; + } + + @Override + public short getShort(int rowId) { + Object val = list.get(rowId); + return val != null ? (Short) val : 0; + } + + @Override + public int getInt(int rowId) { + Object val = list.get(rowId); + return val != null ? (Integer) val : 0; + } + + @Override + public long getLong(int rowId) { + Object val = list.get(rowId); + return val != null ? (Long) val : 0L; + } + + @Override + public float getFloat(int rowId) { + Object val = list.get(rowId); + return val != null ? (Float) val : 0.0f; + } + + @Override + public double getDouble(int rowId) { + Object val = list.get(rowId); + return val != null ? (Double) val : 0.0d; + } + + @Override + @SuppressWarnings("nullness") + public String getString(int rowId) { + return (String) list.get(rowId); + } + + @Override + @SuppressWarnings("nullness") + public byte[] getBinary(int rowId) { + return (byte[]) list.get(rowId); + } + + @Override + @SuppressWarnings("nullness") + public BigDecimal getDecimal(int rowId) { + return (BigDecimal) list.get(rowId); + } + + @Override + public void close() {} + } + + private static class SerializableArrayValue implements ArrayValue { + private final List list; + private final DataType elementType; + + SerializableArrayValue(List list, DataType elementType) { + this.list = list; + this.elementType = elementType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public ColumnVector getElements() { + return new ListColumnVector(elementType, list); + } + } + + private static class SerializableMapValue implements MapValue { + private final Map map; + private final DataType keyType; + private final DataType valueType; + + SerializableMapValue(Map map, DataType keyType, DataType valueType) { + this.map = map; + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public int getSize() { + return map.size(); + } + + @Override + public ColumnVector getKeys() { + return new ListColumnVector(keyType, new ArrayList<>(map.keySet())); + } + + @Override + public ColumnVector getValues() { + return new ListColumnVector(valueType, new ArrayList<>(map.values())); + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java new file mode 100644 index 000000000000..08a27ac55303 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.internal.types.DataTypeJsonSerDe; +import io.delta.kernel.types.StructType; +import java.io.Serializable; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable wrapper for {@link StructType} using the Delta Kernel JSON + * serializer/deserializer. + */ +public class SerializableStructType implements Serializable { + private static final long serialVersionUID = 1L; + + private final String jsonSchema; + private transient StructType structType; + + public SerializableStructType(StructType structType) { + this.structType = Objects.requireNonNull(structType, "structType cannot be null"); + this.jsonSchema = DataTypeJsonSerDe.serializeStructType(structType); + } + + public StructType get() { + if (structType == null) { + structType = DataTypeJsonSerDe.deserializeStructType(jsonSchema); + } + return structType; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SerializableStructType)) { + return false; + } + SerializableStructType that = (SerializableStructType) o; + return Objects.equals(jsonSchema, that.jsonSchema); + } + + @Override + public int hashCode() { + return Objects.hash(jsonSchema); + } + + @Override + public String toString() { + return jsonSchema; + } +} diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java index 4ab932fc5d83..ef6dd660c607 100644 --- a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java @@ -17,18 +17,57 @@ */ package org.apache.beam.sdk.io.delta; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.HashMap; import java.util.Map; +import org.apache.avro.generic.GenericRecord; +import org.apache.beam.sdk.extensions.avro.coders.AvroCoder; +import org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils; +import org.apache.beam.sdk.io.Compression; +import org.apache.beam.sdk.io.FileIO; import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows; +import org.apache.beam.sdk.io.parquet.ParquetIO; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Count; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Unit tests for the {@link DeltaIO}. */ +/** Unit and local integration tests for {@link DeltaIO}. */ @RunWith(JUnit4.class) public class DeltaIOTest { + @Rule public TestPipeline writePipeline = TestPipeline.create(); + @Rule public TestPipeline readPipeline = TestPipeline.create(); + @Rule public TestPipeline filteringPipeline = TestPipeline.create(); + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + @Test public void testReadRowsBuilderAndGetters() { String tablePath = "/path/to/table"; @@ -59,4 +98,675 @@ public void testReadRowsNullDefaults() { Assert.assertNull(readRows.getTimestamp()); Assert.assertNull(readRows.getHadoopConfig()); } + + @Test + public void testPrintScanStateSchema() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-schema"); + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + io.delta.kernel.defaults.engine.DefaultEngine engine = + io.delta.kernel.defaults.engine.DefaultEngine.create( + new org.apache.hadoop.conf.Configuration()); + io.delta.kernel.Table table = io.delta.kernel.Table.forPath(engine, tableDir.getAbsolutePath()); + io.delta.kernel.Snapshot snapshot = table.getLatestSnapshot(engine); + io.delta.kernel.Scan scan = snapshot.getScanBuilder().build(); + + io.delta.kernel.data.Row scanState = scan.getScanState(engine); + System.err.println("SCAN STATE SCHEMA: " + scanState.getSchema().toString()); + + try (io.delta.kernel.utils.CloseableIterator + scanFiles = scan.getScanFiles(engine)) { + while (scanFiles.hasNext()) { + io.delta.kernel.data.FilteredColumnarBatch batch = scanFiles.next(); + try (io.delta.kernel.utils.CloseableIterator rows = + batch.getRows()) { + while (rows.hasNext()) { + io.delta.kernel.data.Row row = rows.next(); + verifySerialization(row); + } + } + } + } + } + + private void verifySerialization(io.delta.kernel.data.Row row) throws Exception { + SerializableRow serializableRow = new SerializableRow(row); + + // Serialize using standard Java Serialization + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + try (java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos)) { + oos.writeObject(serializableRow); + } + + byte[] bytes = baos.toByteArray(); + + // Deserialize + SerializableRow deserializedRow; + java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(bytes); + try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(bais)) { + deserializedRow = (SerializableRow) ois.readObject(); + } + + // Assert equals + org.junit.Assert.assertEquals(serializableRow, deserializedRow); + org.junit.Assert.assertEquals( + row.getSchema().toString(), deserializedRow.getSchema().toString()); + + // Deep verify fields + io.delta.kernel.types.StructType schema = row.getSchema(); + for (int i = 0; i < schema.fields().size(); i++) { + org.junit.Assert.assertEquals(row.isNullAt(i), deserializedRow.isNullAt(i)); + if (!row.isNullAt(i)) { + io.delta.kernel.types.DataType type = schema.fields().get(i).getDataType(); + if (type instanceof io.delta.kernel.types.StringType) { + org.junit.Assert.assertEquals(row.getString(i), deserializedRow.getString(i)); + } else if (type instanceof io.delta.kernel.types.LongType) { + org.junit.Assert.assertEquals(row.getLong(i), deserializedRow.getLong(i)); + } + } + } + } + + @Test + public void testCreateReadTasksDoFn() throws Exception { + File tableDir = tempFolder.newFolder("delta-table"); + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row dummyRow = Row.withSchema(schema).addValues("test-name").build(); + writeParquetFile(new File(tableDir, "part-00000.parquet"), dummyRow); + + PCollection output = + writePipeline + .apply(Create.of(tableDir.getAbsolutePath())) + .apply(ParDo.of(new CreateReadTasksDoFn(null))); + + PCollection paths = + output.apply( + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.strings()) + .via( + task -> + io.delta.kernel.internal.InternalScanFileUtils.getAddFileStatus( + task.getScanFileRows().get(0)) + .getPath())); + + PAssert.that(paths) + .containsInAnyOrder("file:" + tableDir.getAbsolutePath() + "/part-00000.parquet"); + + writePipeline.run().waitUntilFinish(); + } + + @Test + public void testCreateReadTasksDoFnGrouping() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-grouping"); + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00001.parquet\",\"partitionValues\":{},\"size\":400000000,\"modificationTime\":123456789,\"dataChange\":true}}\n" + + "{\"add\":{\"path\":\"part-00002.parquet\",\"partitionValues\":{},\"size\":400000000,\"modificationTime\":123456789,\"dataChange\":true}}\n" + + "{\"add\":{\"path\":\"part-00003.parquet\",\"partitionValues\":{},\"size\":1200000000,\"modificationTime\":123456789,\"dataChange\":true}}\n" + + "{\"add\":{\"path\":\"part-00004.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row dummyRow = Row.withSchema(schema).addValues("test-name").build(); + writeParquetFile(new File(tableDir, "part-00001.parquet"), dummyRow); + writeParquetFile(new File(tableDir, "part-00002.parquet"), dummyRow); + writeParquetFile(new File(tableDir, "part-00003.parquet"), dummyRow); + writeParquetFile(new File(tableDir, "part-00004.parquet"), dummyRow); + + PCollection output = + writePipeline + .apply("Create Grouping Input", Create.of(tableDir.getAbsolutePath())) + .apply("Plan Grouped Files", ParDo.of(new CreateReadTasksDoFn(null))); + + PCollection taskDescriptions = + output.apply( + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.strings()) + .via( + task -> { + StringBuilder sb = new StringBuilder(); + for (SerializableRow row : task.getScanFileRows()) { + if (sb.length() > 0) { + sb.append(","); + } + String fullPath = + io.delta.kernel.internal.InternalScanFileUtils.getAddFileStatus(row) + .getPath(); + String filename = fullPath.substring(fullPath.lastIndexOf('/') + 1); + sb.append(filename); + } + return sb.toString(); + })); + + PAssert.that(taskDescriptions) + .containsInAnyOrder( + "part-00001.parquet,part-00002.parquet", "part-00003.parquet", "part-00004.parquet"); + + writePipeline.run().waitUntilFinish(); + } + + @Test + public void testFullPipelineRead() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-full"); + + // 1. Write a Parquet file using Beam + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + + org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema); + GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema); + + writePipeline + .apply("Create Input", Create.of(record).withCoder(AvroCoder.of(avroSchema))) + .apply( + "Write Parquet", + FileIO.write() + .via(ParquetIO.sink(avroSchema)) + .to(tableDir.getAbsolutePath() + "/") + .withNaming( + (BoundedWindow window, + PaneInfo paneInfo, + int numShards, + int shardIndex, + Compression compression) -> "part-00000.parquet")); + + writePipeline.run().waitUntilFinish(); + + System.out.println("FILES IN TABLE DIR:"); + for (File f : tableDir.listFiles()) { + System.out.println( + " - " + f.getName() + " (size=" + f.length() + ", isDir=" + f.isDirectory() + ")"); + if (f.isDirectory()) { + for (File sub : f.listFiles()) { + System.out.println(" - " + sub.getName() + " (size=" + sub.length() + ")"); + } + } + } + + File parquetFile = new File(tableDir, "part-00000.parquet"); + byte[] fileBytes = Files.readAllBytes(parquetFile.toPath()); + System.out.println("PARQUET FILE LENGTH: " + fileBytes.length); + if (fileBytes.length >= 8) { + System.out.println( + "PARQUET FIRST 4 BYTES: " + + fileBytes[0] + + ", " + + fileBytes[1] + + ", " + + fileBytes[2] + + ", " + + fileBytes[3] + + " ('" + + (char) fileBytes[0] + + (char) fileBytes[1] + + (char) fileBytes[2] + + (char) fileBytes[3] + + "')"); + int len = fileBytes.length; + System.out.println( + "PARQUET LAST 4 BYTES: " + + fileBytes[len - 4] + + ", " + + fileBytes[len - 3] + + ", " + + fileBytes[len - 2] + + ", " + + fileBytes[len - 1] + + " ('" + + (char) fileBytes[len - 4] + + (char) fileBytes[len - 3] + + (char) fileBytes[len - 2] + + (char) fileBytes[len - 1] + + "')"); + } + + // 2. Create the Delta log + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":" + + fileBytes.length + + ",\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + // 3. Read it using DeltaIO + PCollection output = + readPipeline.apply(DeltaIO.readRows().from(tableDir.getAbsolutePath())); + + PAssert.that(output).containsInAnyOrder(row); + + readPipeline.run().waitUntilFinish(); + } + + private byte[] writeParquetFile(File file, Row row) throws Exception { + org.apache.avro.Schema avroSchema = + org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.toAvroSchema(row.getSchema()); + org.apache.avro.generic.GenericRecord record = + org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.toGenericRecord( + row, avroSchema); + org.apache.hadoop.fs.Path path = new org.apache.hadoop.fs.Path(file.getAbsolutePath()); + try (org.apache.parquet.hadoop.ParquetWriter writer = + org.apache.parquet.avro.AvroParquetWriter.builder( + path) + .withSchema(avroSchema) + .withConf(new org.apache.hadoop.conf.Configuration()) + .build()) { + writer.write(record); + } + return java.nio.file.Files.readAllBytes(file.toPath()); + } + + @Test + @org.junit.Ignore("Manual integration test with external local table") + public void testReadingLocalTable() throws Exception { + PCollection output = + readPipeline.apply( + DeltaIO.readRows() + .from("/Users/chamikara/testing/delta_lake/test_repo/test_table_1_gb")); + PCollection counted = output.apply(Count.globally()); + + counted + .apply( + "Convert to String", + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.strings()) + .via(String::valueOf)) + .apply( + "Write to File", + org.apache.beam.sdk.io.TextIO.write() + .to("/Users/chamikara/testing/delta_lake/test_repo_pipeline_output/output") + .withSuffix(".txt") + .withoutSharding()); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testConvertToBeamSchema() { + StructType deltaSchema = + new StructType( + java.util.Arrays.asList( + new StructField("string", StringType.STRING, false), + new StructField("integer", IntegerType.INTEGER, false), + new StructField("long", LongType.LONG, false), + new StructField("float", FloatType.FLOAT, false), + new StructField("double", DoubleType.DOUBLE, false), + new StructField("boolean", BooleanType.BOOLEAN, false), + new StructField("binary", BinaryType.BINARY, false), + new StructField("timestamp", TimestampType.TIMESTAMP, false), + new StructField("date", DateType.DATE, false), + new StructField("array", new ArrayType(StringType.STRING, true), false), + new StructField( + "map", new MapType(StringType.STRING, IntegerType.INTEGER, true), false), + new StructField( + "struct", + new StructType( + java.util.Arrays.asList( + new StructField("nested_string", StringType.STRING, false))), + false))); + + Schema nestedSchema = + Schema.builder().addField("nested_string", Schema.FieldType.STRING).build(); + + Schema expectedSchema = + Schema.builder() + .addField("string", Schema.FieldType.STRING) + .addField("integer", Schema.FieldType.INT32) + .addField("long", Schema.FieldType.INT64) + .addField("float", Schema.FieldType.FLOAT) + .addField("double", Schema.FieldType.DOUBLE) + .addField("boolean", Schema.FieldType.BOOLEAN) + .addField("binary", Schema.FieldType.BYTES) + .addField("timestamp", Schema.FieldType.DATETIME) + .addField("date", Schema.FieldType.DATETIME) + .addField("array", Schema.FieldType.iterable(Schema.FieldType.STRING)) + .addField("map", Schema.FieldType.map(Schema.FieldType.STRING, Schema.FieldType.INT32)) + .addField("struct", Schema.FieldType.row(nestedSchema)) + .build(); + + Schema actualSchema = DeltaIO.ReadRows.convertToBeamSchema(deltaSchema); + org.junit.Assert.assertEquals(expectedSchema, actualSchema); + } + + @Test + public void testDeltaReadTaskTracker() { + java.util.List sizes = java.util.Arrays.asList(100L, 200L, 300L); + org.apache.beam.sdk.io.range.OffsetRange range = + new org.apache.beam.sdk.io.range.OffsetRange(0L, 3L); + DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes); + + org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker.Progress progress = + tracker.getProgress(); + org.junit.Assert.assertEquals(0.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(600.0, progress.getWorkRemaining(), 0.001); + + org.junit.Assert.assertTrue(tracker.tryClaim(0L)); + progress = tracker.getProgress(); + org.junit.Assert.assertEquals(100.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(500.0, progress.getWorkRemaining(), 0.001); + + org.junit.Assert.assertTrue(tracker.tryClaim(1L)); + progress = tracker.getProgress(); + org.junit.Assert.assertEquals(300.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(300.0, progress.getWorkRemaining(), 0.001); + + org.junit.Assert.assertTrue(tracker.tryClaim(2L)); + progress = tracker.getProgress(); + org.junit.Assert.assertEquals(600.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(0.0, progress.getWorkRemaining(), 0.001); + + tracker.checkDone(); + } + + @Test + public void testBeamParquetHandler() { + java.util.List sizes = java.util.Arrays.asList(100L, 200L); + org.apache.beam.sdk.io.range.OffsetRange range = + new org.apache.beam.sdk.io.range.OffsetRange(0L, 2L); + DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes); + + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + io.delta.kernel.engine.ParquetHandler dummyDelegate = + new io.delta.kernel.engine.ParquetHandler() { + @Override + public io.delta.kernel.utils.CloseableIterator + readParquetFiles( + io.delta.kernel.utils.CloseableIterator + fileIter, + io.delta.kernel.types.StructType physicalSchema, + java.util.Optional predicate) + throws java.io.IOException { + return new io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.engine.FileReadResult>() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public io.delta.kernel.engine.FileReadResult next() { + throw new java.util.NoSuchElementException(); + } + + @Override + public void close() {} + }; + } + + @Override + public void writeParquetFileAtomically( + String filePath, + io.delta.kernel.utils.CloseableIterator + data) + throws java.io.IOException {} + + @Override + public io.delta.kernel.utils.CloseableIterator + writeParquetFiles( + String filePath, + io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.data.FilteredColumnarBatch> + data, + java.util.List statsColumns) + throws java.io.IOException { + return new io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.utils.DataFileStatus>() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public io.delta.kernel.utils.DataFileStatus next() { + throw new java.util.NoSuchElementException(); + } + + @Override + public void close() {} + }; + } + }; + + BeamParquetHandler handler = new BeamParquetHandler(conf, dummyDelegate, tracker); + org.junit.Assert.assertNotNull(handler); + + BeamEngine beamEngine = + new BeamEngine(io.delta.kernel.defaults.engine.DefaultEngine.create(conf), handler); + org.junit.Assert.assertEquals(handler, beamEngine.getParquetHandler()); + } + + @Test + public void testBeamParquetHandlerWriteDelegation() throws Exception { + java.util.List sizes = java.util.Arrays.asList(100L); + org.apache.beam.sdk.io.range.OffsetRange range = + new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L); + DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes); + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + + boolean[] flags = new boolean[2]; + io.delta.kernel.engine.ParquetHandler delegate = + new io.delta.kernel.engine.ParquetHandler() { + @Override + public io.delta.kernel.utils.CloseableIterator + readParquetFiles( + io.delta.kernel.utils.CloseableIterator + fileIter, + io.delta.kernel.types.StructType physicalSchema, + java.util.Optional predicate) { + return null; + } + + @Override + public void writeParquetFileAtomically( + String filePath, + io.delta.kernel.utils.CloseableIterator + data) { + flags[0] = true; + } + + @Override + public io.delta.kernel.utils.CloseableIterator + writeParquetFiles( + String filePath, + io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.data.FilteredColumnarBatch> + data, + java.util.List statsColumns) { + flags[1] = true; + return new io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.utils.DataFileStatus>() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public io.delta.kernel.utils.DataFileStatus next() { + throw new java.util.NoSuchElementException(); + } + + @Override + public void close() {} + }; + } + }; + + BeamParquetHandler handler = new BeamParquetHandler(conf, delegate, tracker); + handler.writeParquetFileAtomically("path", null); + org.junit.Assert.assertTrue(flags[0]); + + handler.writeParquetFiles("path", null, java.util.Collections.emptyList()); + org.junit.Assert.assertTrue(flags[1]); + } + + @Test + public void testBeamParquetHandlerReadFiltering() throws Exception { + File tableDir = tempFolder.newFolder("parquet-filtering-test"); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema); + GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema); + + filteringPipeline + .apply("Create Input", Create.of(record).withCoder(AvroCoder.of(avroSchema))) + .apply( + "Write Parquet", + FileIO.write() + .via(ParquetIO.sink(avroSchema)) + .to(tableDir.getAbsolutePath() + "/") + .withNaming((w, p, n, s, c) -> "part-00000.parquet")); + + filteringPipeline.run().waitUntilFinish(); + + File parquetFile = new File(tableDir, "part-00000.parquet"); + io.delta.kernel.utils.FileStatus fileStatus = + io.delta.kernel.utils.FileStatus.of( + parquetFile.getAbsolutePath(), parquetFile.length(), 123456789L); + + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + io.delta.kernel.types.StructType physicalSchema = + new io.delta.kernel.types.StructType( + java.util.Arrays.asList( + new io.delta.kernel.types.StructField( + "name", io.delta.kernel.types.StringType.STRING, true))); + + io.delta.kernel.engine.ParquetHandler dummyDelegate = + new io.delta.kernel.engine.ParquetHandler() { + @Override + public io.delta.kernel.utils.CloseableIterator + readParquetFiles( + io.delta.kernel.utils.CloseableIterator + fileIter, + io.delta.kernel.types.StructType physicalSchema, + java.util.Optional predicate) { + return null; + } + + @Override + public void writeParquetFileAtomically( + String filePath, + io.delta.kernel.utils.CloseableIterator + data) {} + + @Override + public io.delta.kernel.utils.CloseableIterator + writeParquetFiles( + String filePath, + io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.data.FilteredColumnarBatch> + data, + java.util.List statsColumns) { + return null; + } + }; + + // Case A: Out of bounds before (tracker range [10, 20)) + DeltaReadTaskTracker trackerA = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(10L, 20L), + java.util.Collections.singletonList(parquetFile.length())); + BeamParquetHandler handlerA = new BeamParquetHandler(conf, dummyDelegate, trackerA); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerA.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertFalse(iter.hasNext()); + try { + iter.next(); + org.junit.Assert.fail("Expected NoSuchElementException"); + } catch (java.util.NoSuchElementException e) { + // expected + } + } + + // Case B: Out of bounds after (tracker range [0, 0)) + DeltaReadTaskTracker trackerB = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(0L, 0L), + java.util.Collections.singletonList(parquetFile.length())); + BeamParquetHandler handlerB = new BeamParquetHandler(conf, dummyDelegate, trackerB); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerB.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertFalse(iter.hasNext()); + } + + // Case C: Claim fails + DeltaReadTaskTracker trackerC = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L), + java.util.Collections.singletonList(parquetFile.length())) { + @Override + public boolean tryClaim(Long i) { + return false; // Simulate failure to claim + } + }; + BeamParquetHandler handlerC = new BeamParquetHandler(conf, dummyDelegate, trackerC); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerC.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertFalse(iter.hasNext()); + } + + // Case D: Successful claim and read + DeltaReadTaskTracker trackerD = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L), + java.util.Collections.singletonList(parquetFile.length())); + BeamParquetHandler handlerD = new BeamParquetHandler(conf, dummyDelegate, trackerD); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerD.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertTrue(iter.hasNext()); + io.delta.kernel.engine.FileReadResult res = iter.next(); + org.junit.Assert.assertNotNull(res); + org.junit.Assert.assertNotNull(res.getData()); + org.junit.Assert.assertFalse(iter.hasNext()); + try { + iter.next(); + org.junit.Assert.fail("Expected NoSuchElementException"); + } catch (java.util.NoSuchElementException e) { + // expected + } + } + } } diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java new file mode 100644 index 000000000000..69d423032eac --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java @@ -0,0 +1,498 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.data.ArrayValue; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.MapValue; +import io.delta.kernel.data.Row; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.ByteType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DecimalType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.ShortType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SerializableRowTest { + + @Test + public void testAllTypesReadWriteSerialization() throws Exception { + StructType structSchema = + new StructType(Arrays.asList(new StructField("nested_int", IntegerType.INTEGER, false))); + + StructType schema = + new StructType( + Arrays.asList( + new StructField("boolean", BooleanType.BOOLEAN, true), + new StructField("byte", ByteType.BYTE, true), + new StructField("short", ShortType.SHORT, true), + new StructField("integer", IntegerType.INTEGER, true), + new StructField("long", LongType.LONG, true), + new StructField("float", FloatType.FLOAT, true), + new StructField("double", DoubleType.DOUBLE, true), + new StructField("string", StringType.STRING, true), + new StructField("binary", BinaryType.BINARY, true), + new StructField("decimal", new DecimalType(10, 2), true), + new StructField("date", DateType.DATE, true), + new StructField("timestamp", TimestampType.TIMESTAMP, true), + new StructField("struct", structSchema, true), + new StructField("array", new ArrayType(StringType.STRING, true), true), + new StructField( + "map", new MapType(StringType.STRING, IntegerType.INTEGER, true), true))); + + Map values = new LinkedHashMap<>(); + values.put("boolean", true); + values.put("byte", (byte) 1); + values.put("short", (short) 2); + values.put("integer", 3); + values.put("long", 4L); + values.put("float", 5.0f); + values.put("double", 6.0d); + values.put("string", "hello"); + values.put("binary", new byte[] {7, 8}); + values.put("decimal", new BigDecimal("9.20")); + values.put("date", 16543); + values.put("timestamp", 16543000000L); + + Map nestedValues = new LinkedHashMap<>(); + nestedValues.put("nested_int", 42); + values.put("struct", new FakeRow(structSchema, nestedValues)); + + values.put("array", Arrays.asList("a", "b", null)); + + Map mapValues = new LinkedHashMap<>(); + mapValues.put("key1", 100); + mapValues.put("key2", null); + values.put("map", mapValues); + + FakeRow originalRow = new FakeRow(schema, values); + SerializableRow serializableRow = new SerializableRow(originalRow); + + verifyRowContents(serializableRow, schema, values); + + // Test equals and hashCode + SerializableRow serializableRow2 = new SerializableRow(new FakeRow(schema, values)); + Assert.assertEquals(serializableRow, serializableRow2); + Assert.assertEquals(serializableRow.hashCode(), serializableRow2.hashCode()); + Assert.assertNotNull(serializableRow.toString()); + + // Serialize and Deserialize + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(serializableRow); + } + + byte[] bytes = baos.toByteArray(); + SerializableRow deserializedRow; + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + deserializedRow = (SerializableRow) ois.readObject(); + } + + verifyRowContents(deserializedRow, schema, values); + Assert.assertEquals(serializableRow, deserializedRow); + } + + @Test + public void testNullValuesReadWriteSerialization() throws Exception { + StructType structSchema = + new StructType(Arrays.asList(new StructField("nested_int", IntegerType.INTEGER, true))); + + StructType schema = + new StructType( + Arrays.asList( + new StructField("boolean", BooleanType.BOOLEAN, true), + new StructField("byte", ByteType.BYTE, true), + new StructField("short", ShortType.SHORT, true), + new StructField("integer", IntegerType.INTEGER, true), + new StructField("long", LongType.LONG, true), + new StructField("float", FloatType.FLOAT, true), + new StructField("double", DoubleType.DOUBLE, true), + new StructField("string", StringType.STRING, true), + new StructField("binary", BinaryType.BINARY, true), + new StructField("decimal", new DecimalType(10, 2), true), + new StructField("date", DateType.DATE, true), + new StructField("timestamp", TimestampType.TIMESTAMP, true), + new StructField("struct", structSchema, true), + new StructField("array", new ArrayType(StringType.STRING, true), true), + new StructField( + "map", new MapType(StringType.STRING, IntegerType.INTEGER, true), true))); + + Map nullValues = new LinkedHashMap<>(); + for (StructField field : schema.fields()) { + nullValues.put(field.getName(), null); + } + + FakeRow originalRow = new FakeRow(schema, nullValues); + SerializableRow serializableRow = new SerializableRow(originalRow); + + verifyRowContents(serializableRow, schema, nullValues); + + // Serialize and Deserialize + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(serializableRow); + } + + byte[] bytes = baos.toByteArray(); + SerializableRow deserializedRow; + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + deserializedRow = (SerializableRow) ois.readObject(); + } + + verifyRowContents(deserializedRow, schema, nullValues); + Assert.assertEquals(serializableRow, deserializedRow); + } + + private void verifyRowContents(Row row, StructType schema, Map expectedValues) { + Assert.assertEquals(schema.toString(), row.getSchema().toString()); + int i = 0; + for (StructField field : schema.fields()) { + Object expected = expectedValues.get(field.getName()); + Assert.assertEquals(expected == null, row.isNullAt(i)); + if (expected != null) { + DataType type = field.getDataType(); + if (type instanceof BooleanType) { + Assert.assertEquals(expected, row.getBoolean(i)); + } else if (type instanceof ByteType) { + Assert.assertEquals(expected, row.getByte(i)); + } else if (type instanceof ShortType) { + Assert.assertEquals(expected, row.getShort(i)); + } else if (type instanceof IntegerType) { + Assert.assertEquals(expected, row.getInt(i)); + } else if (type instanceof LongType) { + Assert.assertEquals(expected, row.getLong(i)); + } else if (type instanceof FloatType) { + Assert.assertEquals(expected, row.getFloat(i)); + } else if (type instanceof DoubleType) { + Assert.assertEquals(expected, row.getDouble(i)); + } else if (type instanceof StringType) { + Assert.assertEquals(expected, row.getString(i)); + } else if (type instanceof BinaryType) { + Assert.assertArrayEquals((byte[]) expected, row.getBinary(i)); + } else if (type instanceof DecimalType) { + Assert.assertEquals(expected, row.getDecimal(i)); + } else if (type instanceof DateType) { + Assert.assertEquals(expected, row.getInt(i)); + } else if (type instanceof TimestampType) { + Assert.assertEquals(expected, row.getLong(i)); + } else if (type instanceof StructType) { + Row actualStruct = row.getStruct(i); + Assert.assertNotNull(actualStruct); + Row expectedStruct = (Row) expected; + Assert.assertEquals( + expectedStruct.getSchema().toString(), actualStruct.getSchema().toString()); + for (int j = 0; j < expectedStruct.getSchema().fields().size(); j++) { + Assert.assertEquals(expectedStruct.isNullAt(j), actualStruct.isNullAt(j)); + if (!expectedStruct.isNullAt(j)) { + Assert.assertEquals(expectedStruct.getInt(j), actualStruct.getInt(j)); + } + } + } else if (type instanceof ArrayType) { + ArrayValue actualArray = row.getArray(i); + Assert.assertNotNull(actualArray); + List expectedList = (List) expected; + Assert.assertEquals(expectedList.size(), actualArray.getSize()); + ColumnVector vector = actualArray.getElements(); + for (int j = 0; j < expectedList.size(); j++) { + Assert.assertEquals(expectedList.get(j) == null, vector.isNullAt(j)); + if (expectedList.get(j) != null) { + Assert.assertEquals(expectedList.get(j), vector.getString(j)); + } + } + } else if (type instanceof MapType) { + MapValue actualMap = row.getMap(i); + Assert.assertNotNull(actualMap); + Map expectedMap = (Map) expected; + Assert.assertEquals(expectedMap.size(), actualMap.getSize()); + ColumnVector keys = actualMap.getKeys(); + ColumnVector valuesVector = actualMap.getValues(); + int j = 0; + for (Map.Entry entry : expectedMap.entrySet()) { + Assert.assertEquals(entry.getKey(), keys.getString(j)); + Assert.assertEquals(entry.getValue() == null, valuesVector.isNullAt(j)); + if (entry.getValue() != null) { + Assert.assertEquals(entry.getValue(), valuesVector.getInt(j)); + } + j++; + } + } + } + i++; + } + } + + private static class FakeRow implements Row { + private final StructType schema; + private final Map values; + + FakeRow(StructType schema, Map values) { + this.schema = schema; + this.values = values; + } + + @Override + public StructType getSchema() { + return schema; + } + + @Override + public boolean isNullAt(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return values.get(fieldName) == null; + } + + @Override + public boolean getBoolean(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Boolean) values.get(fieldName); + } + + @Override + public byte getByte(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Byte) values.get(fieldName); + } + + @Override + public short getShort(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Short) values.get(fieldName); + } + + @Override + public int getInt(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Integer) values.get(fieldName); + } + + @Override + public long getLong(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Long) values.get(fieldName); + } + + @Override + public float getFloat(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Float) values.get(fieldName); + } + + @Override + public double getDouble(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Double) values.get(fieldName); + } + + @Override + public String getString(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (String) values.get(fieldName); + } + + @Override + public byte[] getBinary(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (byte[]) values.get(fieldName); + } + + @Override + public BigDecimal getDecimal(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (BigDecimal) values.get(fieldName); + } + + @Override + public Row getStruct(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Row) values.get(fieldName); + } + + @Override + public ArrayValue getArray(int ord) { + String fieldName = schema.fields().get(ord).getName(); + List list = (List) values.get(fieldName); + if (list == null) { + return null; + } + DataType elementType = ((ArrayType) schema.fields().get(ord).getDataType()).getElementType(); + return new FakeArrayValue(list, elementType); + } + + @Override + public MapValue getMap(int ord) { + String fieldName = schema.fields().get(ord).getName(); + Map map = (Map) values.get(fieldName); + if (map == null) { + return null; + } + MapType mapType = (MapType) schema.fields().get(ord).getDataType(); + return new FakeMapValue(map, mapType.getKeyType(), mapType.getValueType()); + } + } + + private static class FakeArrayValue implements ArrayValue { + private final List list; + private final DataType elementType; + + FakeArrayValue(List list, DataType elementType) { + this.list = list; + this.elementType = elementType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public ColumnVector getElements() { + return new FakeColumnVector(elementType, list); + } + } + + private static class FakeMapValue implements MapValue { + private final Map map; + private final DataType keyType; + private final DataType valueType; + + FakeMapValue(Map map, DataType keyType, DataType valueType) { + this.map = map; + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public int getSize() { + return map.size(); + } + + @Override + public ColumnVector getKeys() { + return new FakeColumnVector(keyType, new ArrayList<>(map.keySet())); + } + + @Override + public ColumnVector getValues() { + return new FakeColumnVector(valueType, new ArrayList<>(map.values())); + } + } + + private static class FakeColumnVector implements ColumnVector { + private final DataType dataType; + private final List list; + + FakeColumnVector(DataType dataType, List list) { + this.dataType = dataType; + this.list = new ArrayList<>(list); + } + + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public boolean isNullAt(int rowId) { + return list.get(rowId) == null; + } + + @Override + public boolean getBoolean(int rowId) { + return (Boolean) list.get(rowId); + } + + @Override + public byte getByte(int rowId) { + return (Byte) list.get(rowId); + } + + @Override + public short getShort(int rowId) { + return (Short) list.get(rowId); + } + + @Override + public int getInt(int rowId) { + return (Integer) list.get(rowId); + } + + @Override + public long getLong(int rowId) { + return (Long) list.get(rowId); + } + + @Override + public float getFloat(int rowId) { + return (Float) list.get(rowId); + } + + @Override + public double getDouble(int rowId) { + return (Double) list.get(rowId); + } + + @Override + public String getString(int rowId) { + return (String) list.get(rowId); + } + + @Override + public byte[] getBinary(int rowId) { + return (byte[]) list.get(rowId); + } + + @Override + public BigDecimal getDecimal(int rowId) { + return (BigDecimal) list.get(rowId); + } + + @Override + public void close() {} + } +} From 842dcaf823be2604a9e04ebb3bd2b0caf9d2cd9b Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 3 Jun 2026 15:33:54 -0400 Subject: [PATCH 294/490] Fix offlineRepositoryRoot relative to Beam repo root (#38790) --- .../main/groovy/org/apache/beam/gradle/Repositories.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy index 20c878c06a94..e154fc6ea7ac 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy @@ -31,7 +31,7 @@ class Repositories { } project.repositories { - maven { url project.offlineRepositoryRoot } + maven { url project.rootProject.file(project.offlineRepositoryRoot) } // To run gradle in offline mode, one must first invoke // 'updateOfflineRepository' to create an offline repo @@ -86,7 +86,7 @@ class Repositories { maven { url "https://plugins.gradle.org/m2/" } maven { url "https://repo.spring.io/plugins-release" } maven { url "https://packages.confluent.io/maven/" } - maven { url project.offlineRepositoryRoot } + maven { url project.rootProject.file(project.offlineRepositoryRoot) } } includeSources = false includeJavadocs = false From efa09a5ff1adce744015cadb86f7af33114c1f29 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Wed, 3 Jun 2026 22:21:55 +0200 Subject: [PATCH 295/490] Update republish released docker workflow defaults to 2.74.0 (#38796) --- .github/workflows/republish_released_docker_containers.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index 48e9b7101a80..efc1030844f2 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -32,8 +32,8 @@ on: - cron: "0 6 * * 1" env: docker_registry: gcr.io - release: "${{ github.event.inputs.RELEASE || '2.73.0' }}" - rc: "${{ github.event.inputs.RC || '5' }}" + release: "${{ github.event.inputs.RELEASE || '2.74.0' }}" + rc: "${{ github.event.inputs.RC || '3' }}" jobs: From 0d2046fb3bf642b0da891a34d05fce51fd17fb8c Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:30:09 -0400 Subject: [PATCH 296/490] Update beam-master container (#38795) --- sdks/python/apache_beam/runners/dataflow/internal/names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/names.py b/sdks/python/apache_beam/runners/dataflow/internal/names.py index 4ea99f413fb6..b2dee3c7044e 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/names.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/names.py @@ -35,6 +35,6 @@ # Update this tag whenever there is a change that # requires changes to SDK harness container or SDK harness launcher. -BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260526' +BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260603' DATAFLOW_CONTAINER_IMAGE_REPOSITORY = 'gcr.io/cloud-dataflow/v1beta3' From 1bd70ca15a427de1226cd11b9604e8e74409e6f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:34:10 -0400 Subject: [PATCH 297/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#38801) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.102.2 to 1.103.1. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.102.2...service/s3/v1.103.1) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.103.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 22 +++++++++++----------- sdks/go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index ad4629b61f6b..fdb229ad06fd 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,12 +32,12 @@ require ( cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 - github.com/aws/aws-sdk-go-v2 v1.41.9 + github.com/aws/aws-sdk-go-v2 v1.41.11 github.com/aws/aws-sdk-go-v2/config v1.32.20 github.com/aws/aws-sdk-go-v2/credentials v1.19.19 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 - github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 - github.com/aws/smithy-go v1.26.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 + github.com/aws/smithy-go v1.27.0 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 @@ -148,15 +148,15 @@ require ( github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 22a716110ec1..badb633a2033 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -199,12 +199,12 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4= -github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo= +github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= +github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 h1:oRtsqWgxbpeXrOlxOoQStx2M9WNbIkPq4C4Xn1or6bc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12/go.mod h1:Zg0Oe9qT+9wcezlm1a64wGJp2qZdRElVxo/seJf7jYU= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc= @@ -223,38 +223,38 @@ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 h1:eBnBL5sAT9aIg014yU6D github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22/go.mod h1:PjXTcjNr3cv5pfRG+PakZ9Yypx2vLRjCAcm1kEfHSjk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 h1:eaS9vwQ5ym4Y9S6+G/K3d3lgZhxs9Sldcn/YS7cmdKY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28/go.mod h1:oTdbDr+BMs7gAYrNpD0LDTyqQfv6yOYgTDv46+xbwFY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 h1:rFSsqDfCMPAmG70JOsYqFZCHXkyatoGa1K4YEt/BggQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11/go.mod h1:XG68qW+YLLFH0vnSDCou43Cgj5TeAG83O5NRSJgt04Y= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20 h1:yt2fjgev3Hqm33zPw0ZWtki3sZ0SLcr+PkuvXDAAf/8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20/go.mod h1:wnPjCjPJ6x5GBhrER8f0QakaQ2LokfhCVYxmAZBpPjY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 h1:2/pUo42hhVmQcM21ttZoBOLHQymyUH8qEnZGTIuGBT8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27/go.mod h1:p7hwgbwompjCRNTdB3ytlldddNt1rDBgVVMqWEVG1II= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27 h1:JEXSW4wztrl1MoL5EMvJMO7lc/TRZloztrJKNl96SW8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27/go.mod h1:8eL+YgEqy6IYqjwW6PG0Ubn59a2xtCzbz7Pi18JBu04= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 h1:WkX5IXwcxgO/WPTvhEqoSW2L1GB1OyIxk0vuzzdTftc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1/go.mod h1:9Q9ZHyiTItraw8BXpO48pk398Mou0YCSI+xvFcaGgxU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw= github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A= @@ -274,8 +274,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3 github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= -github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= +github.com/aws/smithy-go v1.27.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= From 07bd53cd7581a58a384ef357c74c39bbded86a5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:13:27 -0400 Subject: [PATCH 298/490] Bump github.com/aws/aws-sdk-go-v2/config in /sdks (#38804) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.20 to 1.32.22. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.20...config/v1.32.22) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.22 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 14 +++++++------- sdks/go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index fdb229ad06fd..3889e8daa096 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,8 +33,8 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 github.com/aws/aws-sdk-go-v2 v1.41.11 - github.com/aws/aws-sdk-go-v2/config v1.32.20 - github.com/aws/aws-sdk-go-v2/credentials v1.19.19 + github.com/aws/aws-sdk-go-v2/config v1.32.22 + github.com/aws/aws-sdk-go-v2/credentials v1.19.21 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 github.com/aws/smithy-go v1.27.0 @@ -91,7 +91,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -149,7 +149,7 @@ require ( github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 // indirect @@ -157,9 +157,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index badb633a2033..4721213961b0 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,16 +207,16 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 h1:oRtsqWgxbpeXrOl github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12/go.mod h1:Zg0Oe9qT+9wcezlm1a64wGJp2qZdRElVxo/seJf7jYU= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc= -github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM= +github.com/aws/aws-sdk-go-v2/config v1.32.22 h1:Vfvp7+fYKsVCADcWOEllqEV47aIBXhNchvyDFu1B5fY= +github.com/aws/aws-sdk-go-v2/config v1.32.22/go.mod h1:0+H+0nPKbvWltf5vSIGkApv+hGbaQ4FfwTjGIYQREcw= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU= -github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.21 h1:0+HscFXtNa4+3buV4IlG6v5lnOdzi5TrpicFGjKHgh4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.21/go.mod h1:UE8+9t5zudFwu5k5ShC1PKArVEdOkQQdCXIHQAVNUcU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 h1:BEfN1sjtiKEdikRDxYkjZNE4tyvw/YbGWCbl3xDZgRw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27/go.mod h1:ISGSFNbOHRS+JV/17yStzRTPBUHHqF92kCpRLLyH3Nk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 h1:eBnBL5sAT9aIg014yU6DG4YT/ov8r9jnOPW/t7rr8Ig= @@ -256,22 +256,22 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU66 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 h1:WkX5IXwcxgO/WPTvhEqoSW2L1GB1OyIxk0vuzzdTftc= github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1/go.mod h1:9Q9ZHyiTItraw8BXpO48pk398Mou0YCSI+xvFcaGgxU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 h1:t6U7sowMfOjTeZXtDOtgEJXsoJyX4MDag+sfWGwUM9M= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.3/go.mod h1:WhO1EH3phjFWValQDsExaxncgEWJsHeoTvuyQAj3jwU= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 h1:TUV8oytPCX1PfVgZn0N8/sPZx7T0YasaMCBHox1erlw= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.1/go.mod h1:tEL1hqCrkgwrDVL04HuLxz1SLUXdh+4kKhWv1pXKeiY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 h1:p9+Fizo2sUB6wI5Yb3K5lmykQAGs5JrKLBV/me6613Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4/go.mod h1:0x10Wy0dVS4Gn552xhHY5th2QdYpfJf44EsfyYGV194= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 h1:r/vUkpLilfCA3sxbRnkHbJejaoVHEdj4FEhv+Zva4DU= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.1/go.mod h1:t01JURC8Fe5M+7R1K0vzIZ2NT04HqvZR+FjlHrHDT2A= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= From 29606e4ac2bafe67cbf0ec2e67d5a368301bcdf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Thu, 4 Jun 2026 09:52:27 +0200 Subject: [PATCH 299/490] [OpenTelemetry] FNHarness, Dataflow Runner v1 - set open telemetry settings. (#38785) --- .../dataflow/worker/StreamingDataflowWorker.java | 13 +++++++++++++ .../java/org/apache/beam/fn/harness/FnHarness.java | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java index 4d070da995b3..4c3e58978ec3 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java @@ -115,6 +115,7 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQuerySinkMetrics; import org.apache.beam.sdk.metrics.MetricsEnvironment; import org.apache.beam.sdk.options.ExperimentalOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.util.construction.CoderTranslation; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.auth.MoreCallCredentials; @@ -1042,6 +1043,18 @@ public static void main(String[] args) throws Exception { WindowedValues.FullWindowedValueCoder.setMetadataSupported(); } + SdkHarnessOptions sdkHarnessOptions = options.as(SdkHarnessOptions.class); + Map openTelemetryProperties = sdkHarnessOptions.getOpenTelemetryProperties(); + if (openTelemetryProperties != null && !openTelemetryProperties.isEmpty()) { + openTelemetryProperties.forEach( + (k, v) -> { + if (k != null && v != null) { + System.setProperty(k, v); + } + }); + LOG.info("Enabled Open Telemetry with properties: {}", openTelemetryProperties); + } + LOG.debug("Creating StreamingDataflowWorker from options: {}", options); StreamingDataflowWorker worker = StreamingDataflowWorker.fromOptions(options); diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java index d0724432f3c9..703e726739a0 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java @@ -296,6 +296,17 @@ public static void main( // Register standard file systems. FileSystems.setDefaultPipelineOptions(options); CoderTranslation.verifyModelCodersRegistered(); + SdkHarnessOptions sdkHarnessOptions = options.as(SdkHarnessOptions.class); + Map openTelemetryProperties = sdkHarnessOptions.getOpenTelemetryProperties(); + if (openTelemetryProperties != null && !openTelemetryProperties.isEmpty()) { + openTelemetryProperties.forEach( + (k, v) -> { + if (k != null && v != null) { + System.setProperty(k, v); + } + }); + LOG.info("Enabled Open Telemetry with properties: {}", openTelemetryProperties); + } EnumMap< BeamFnApi.InstructionRequest.RequestCase, ThrowingFunction> From a5c75ff880e418fa6b131b2b7c2b7cc9e0282450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Thu, 4 Jun 2026 09:52:50 +0200 Subject: [PATCH 300/490] [OpenTelemetry] Default open telemetry configuration behind experiment (#38784) --- .../beam/runners/dataflow/DataflowRunner.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 4864f2cf4537..011f60f4fd14 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -1327,6 +1328,21 @@ public DataflowPipelineJob run(Pipeline pipeline) { if (!ExperimentalOptions.hasExperiment(options, "disable_projection_pushdown")) { ProjectionPushdownOptimizer.optimize(pipeline); } + SdkHarnessOptions sdkHarnessOptions = options.as(SdkHarnessOptions.class); + if (ExperimentalOptions.hasExperiment(options, "enable_otel_defaults")) { + Map openTelemetryProperties = sdkHarnessOptions.getOpenTelemetryProperties(); + if (openTelemetryProperties == null) { + openTelemetryProperties = new HashMap<>(); + openTelemetryProperties.put("google.cloud.project", options.getProject()); + openTelemetryProperties.put( + "otel.exporter.otlp.endpoint", "https://telemetry.googleapis.com"); + openTelemetryProperties.put("otel.traces.exporter", "otlp"); + openTelemetryProperties.put("otel.java.global-autoconfigure.enabled", "true"); + openTelemetryProperties.put("otel.traces.sampler.arg", "0.01"); + openTelemetryProperties.put("otel.service.name", options.getAppName()); + sdkHarnessOptions.setOpenTelemetryProperties(openTelemetryProperties); + } + } LOG.info( "Executing pipeline on the Dataflow Service, which will have billing implications " From 8cc3668bf095809fc158d9e3fe02ea4533eb3db5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:10:33 -0400 Subject: [PATCH 301/490] Bump google.golang.org/api from 0.282.0 to 0.283.0 in /sdks (#38809) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.282.0 to 0.283.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.282.0...v0.283.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.283.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 3889e8daa096..d28bf5f6a56d 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 - google.golang.org/api v0.282.0 + google.golang.org/api v0.283.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 diff --git a/sdks/go.sum b/sdks/go.sum index 4721213961b0..b562bdfffbfe 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1324,8 +1324,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= -google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= +google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik= +google.golang.org/api v0.283.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From 2cfd465816a3ba718430830562768d0b5b9317e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:11:01 -0400 Subject: [PATCH 302/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38810) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.22 to 1.22.24. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.22...feature/s3/manager/v1.22.24) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.24 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index d28bf5f6a56d..292e0e371ee6 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.11 github.com/aws/aws-sdk-go-v2/config v1.32.22 github.com/aws/aws-sdk-go-v2/credentials v1.19.21 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.24 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 github.com/aws/smithy-go v1.27.0 github.com/docker/go-connections v0.7.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index b562bdfffbfe..0a06899c18bb 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 h1:BEfN1sjtiKEdikRDxYkjZN github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27/go.mod h1:ISGSFNbOHRS+JV/17yStzRTPBUHHqF92kCpRLLyH3Nk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22 h1:eBnBL5sAT9aIg014yU6DG4YT/ov8r9jnOPW/t7rr8Ig= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.22/go.mod h1:PjXTcjNr3cv5pfRG+PakZ9Yypx2vLRjCAcm1kEfHSjk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.24 h1:eRIrOCSlSjyb/tT6vAPLwUZQO71XWaLkqwtM8DQuQ2s= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.24/go.mod h1:ox5MjGuEKA/oXGNbprU9PW/nc+YALeHjDkIiHfzCUgM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= From 8916ff0446908d252540ad64a8510a162dfc7cd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:11:35 -0400 Subject: [PATCH 303/490] Bump github.com/nats-io/nats-server/v2 from 2.14.1 to 2.14.2 in /sdks (#38811) Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.14.1 to 2.14.2. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md) - [Commits](https://github.com/nats-io/nats-server/compare/v2.14.1...v2.14.2) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 dependency-version: 2.14.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 292e0e371ee6..011b2e0fe8c6 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -46,7 +46,7 @@ require ( github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 github.com/lib/pq v1.12.3 github.com/linkedin/goavro/v2 v2.15.0 - github.com/nats-io/nats-server/v2 v2.14.1 + github.com/nats-io/nats-server/v2 v2.14.2 github.com/nats-io/nats.go v1.52.0 github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 @@ -111,8 +111,8 @@ require ( github.com/moby/go-archive v0.2.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect - github.com/nats-io/jwt/v2 v2.8.1 // indirect - github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/jwt/v2 v2.8.2 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 0a06899c18bb..74dc7d7358ea 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -710,14 +710,14 @@ github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ= github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= -github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.14.1 h1:wXs/a5fw9Hzm3CvuzLxGeIwpjPulSa7gMT3eSuhGkcg= -github.com/nats-io/nats-server/v2 v2.14.1/go.mod h1:4N17zLpuS7WMbG8T9gsE2B7z9hC9PraPyulVBfpK6nU= +github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= +github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= +github.com/nats-io/nats-server/v2 v2.14.2 h1:Q7dRhCY03Y00rETFW3KV+KGaCIajlDfWgWUVgbMxyuk= +github.com/nats-io/nats-server/v2 v2.14.2/go.mod h1:lWpb1bSpRELZfRdlMkdz8E7lbXKKyNe8RIn0vvepIHs= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncw/swift v1.0.52/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= From daf252ccce500bb24334e629bae459e9604146e1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 4 Jun 2026 17:44:03 +0200 Subject: [PATCH 304/490] Upgrade Google Cloud libraries-bom to 26.83.0 (#38799) * Upgrade Google Cloud libraries-bom to 26.83.0 * Add OpenTelemetry 1.57.0 license entries for BOM upgrade --- .../org/apache/beam/gradle/BeamModulePlugin.groovy | 12 ++++++------ .../container/license_scripts/dep_urls_java.yaml | 8 +++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 5ca0de9de846..ed2f91634327 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -609,14 +609,14 @@ class BeamModulePlugin implements Plugin { def dbcp2_version = "2.9.0" def errorprone_version = "2.31.0" // [bomupgrader] determined by: com.google.api:gax, consistent with: google_cloud_platform_libraries_bom - def gax_version = "2.79.0" + def gax_version = "2.80.0" def google_ads_version = "33.0.0" def google_clients_version = "2.0.0" def google_cloud_bigdataoss_version = "3.1.16" def google_code_gson_version = "2.10.1" def google_oauth_clients_version = "1.34.1" // [bomupgrader] determined by: io.grpc:grpc-netty, consistent with: google_cloud_platform_libraries_bom - def grpc_version = "1.80.0" + def grpc_version = "1.81.0" def guava_version = "33.1.0-jre" def hadoop_version = "3.4.2" def hamcrest_version = "2.1" @@ -632,9 +632,9 @@ class BeamModulePlugin implements Plugin { def log4j2_version = "2.25.4" def nemo_version = "0.1" // [bomupgrader] determined by: io.grpc:grpc-netty, consistent with: google_cloud_platform_libraries_bom - def netty_version = "4.1.130.Final" + def netty_version = "4.1.132.Final" // [bomupgrader] determined by: io.opentelemetry:opentelemetry-sdk, consistent with: google_cloud_platform_libraries_bom - def opentelemetry_version = "1.56.0" + def opentelemetry_version = "1.57.0" def opentelemetry_contrib_version = "1.52.0" def postgres_version = "42.6.2" // [bomupgrader] determined by: com.google.protobuf:protobuf-java, consistent with: google_cloud_platform_libraries_bom @@ -761,14 +761,14 @@ class BeamModulePlugin implements Plugin { google_cloud_core_grpc : "com.google.cloud:google-cloud-core-grpc", // google_cloud_platform_libraries_bom sets version google_cloud_datacatalog_v1beta1 : "com.google.cloud:google-cloud-datacatalog", // google_cloud_platform_libraries_bom sets version google_cloud_dataflow_java_proto_library_all: "com.google.cloud.dataflow:google-cloud-dataflow-java-proto-library-all:0.5.160304", - google_cloud_datastore_v1_proto_client : "com.google.cloud.datastore:datastore-v1-proto-client:2.40.0", // [bomupgrader] sets version + google_cloud_datastore_v1_proto_client : "com.google.cloud.datastore:datastore-v1-proto-client:3.0.0", // [bomupgrader] sets version google_cloud_firestore : "com.google.cloud:google-cloud-firestore", // google_cloud_platform_libraries_bom sets version google_cloud_kms : "com.google.cloud:google-cloud-kms", // google_cloud_platform_libraries_bom sets version google_cloud_logging : "com.google.cloud:google-cloud-logging", // google_cloud_platform_libraries_bom sets version google_cloud_pubsub : "com.google.cloud:google-cloud-pubsub", // google_cloud_platform_libraries_bom sets version // [bomupgrader] the BOM version is set by scripts/tools/bomupgrader.py. If update manually, also update // libraries-bom version on sdks/java/container/license_scripts/dep_urls_java.yaml - google_cloud_platform_libraries_bom : "com.google.cloud:libraries-bom:26.80.0", + google_cloud_platform_libraries_bom : "com.google.cloud:libraries-bom:26.83.0", google_cloud_secret_manager : "com.google.cloud:google-cloud-secretmanager", // google_cloud_platform_libraries_bom sets version google_cloud_spanner : "com.google.cloud:google-cloud-spanner", // google_cloud_platform_libraries_bom sets version google_cloud_storage : "com.google.cloud:google-cloud-storage", // google_cloud_platform_libraries_bom sets version diff --git a/sdks/java/container/license_scripts/dep_urls_java.yaml b/sdks/java/container/license_scripts/dep_urls_java.yaml index 29deb7165040..9f80b8c81fcf 100644 --- a/sdks/java/container/license_scripts/dep_urls_java.yaml +++ b/sdks/java/container/license_scripts/dep_urls_java.yaml @@ -46,7 +46,7 @@ jaxen: '1.1.6': type: "3-Clause BSD" libraries-bom: - '26.80.0': + '26.83.0': license: "https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-opensource-java/master/LICENSE" type: "Apache License 2.0" paranamer: @@ -69,10 +69,16 @@ opentelemetry-bom: '1.56.0': license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" type: "Apache License 2.0" + '1.57.0': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.57.0/LICENSE" + type: "Apache License 2.0" opentelemetry-bom-alpha: '1.56.0-alpha': license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" type: "Apache License 2.0" + '1.57.0-alpha': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.57.0/LICENSE" + type: "Apache License 2.0" zstd-jni: '1.5.2-5': license: "https://raw.githubusercontent.com/luben/zstd-jni/master/LICENSE" From 91cbf37ed81c0e9135c319712cb209259d1a06c8 Mon Sep 17 00:00:00 2001 From: Lalit Yadav Date: Thu, 4 Jun 2026 11:02:41 -0500 Subject: [PATCH 305/490] feat: Add LogElements transform to Java SDK (#38533) * feat: Add Java LogElements transform * Addressed LogElements review feedback * Add LogElements tests * Add LogElements level logging test * Refactor LogElements logging test --- .../beam/sdk/transforms/LogElements.java | 234 ++++++++++++++++++ .../beam/sdk/transforms/LogElementsTest.java | 108 ++++++++ 2 files changed, 342 insertions(+) create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/LogElements.java create mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/LogElementsTest.java diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/LogElements.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/LogElements.java new file mode 100644 index 000000000000..75b41d2b0443 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/LogElements.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.transforms; + +import java.util.Objects; +import org.apache.beam.sdk.transforms.display.DisplayData; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +/** + * {@link PTransform} for logging elements of a {@link PCollection}. + * + *

    Each element is logged and then emitted unchanged. + * + *

    {@code
    + * PCollection words = ...;
    + * PCollection loggedWords = words.apply(LogElements.info().withPrefix("word: "));
    + * }
    + * + * @param the element type of the input {@link PCollection} + */ +public class LogElements extends PTransform, PCollection> { + private static final Logger LOG = LoggerFactory.getLogger(LogElements.class); + + private final Level level; + private final String prefix; + private final boolean withTimestamp; + private final boolean withWindow; + private final boolean withPaneInfo; + + /** Returns a {@link LogElements} transform that logs elements at {@link Level#TRACE}. */ + public static LogElements trace() { + return of(Level.TRACE); + } + + /** Returns a {@link LogElements} transform that logs elements at {@link Level#DEBUG}. */ + public static LogElements debug() { + return of(Level.DEBUG); + } + + /** Returns a {@link LogElements} transform that logs elements at {@link Level#INFO}. */ + public static LogElements info() { + return of(Level.INFO); + } + + /** Returns a {@link LogElements} transform that logs elements at {@link Level#WARN}. */ + public static LogElements warn() { + return of(Level.WARN); + } + + /** Returns a {@link LogElements} transform that logs elements at {@link Level#ERROR}. */ + public static LogElements error() { + return of(Level.ERROR); + } + + /** Returns a {@link LogElements} transform that logs elements at the given level. */ + public static LogElements of(Level level) { + return new LogElements<>(level, "", false, false, false); + } + + private LogElements( + Level level, String prefix, boolean withTimestamp, boolean withWindow, boolean withPaneInfo) { + this.level = Objects.requireNonNull(level, "level"); + this.prefix = Objects.requireNonNull(prefix, "prefix"); + this.withTimestamp = withTimestamp; + this.withWindow = withWindow; + this.withPaneInfo = withPaneInfo; + } + + /** Returns a new {@link LogElements} transform with the given prefix before each element. */ + public LogElements withPrefix(String prefix) { + return new LogElements<>(level, prefix, withTimestamp, withWindow, withPaneInfo); + } + + /** Returns a new {@link LogElements} transform that logs each element's timestamp. */ + public LogElements withTimestamp() { + return new LogElements<>(level, prefix, true, withWindow, withPaneInfo); + } + + /** Returns a new {@link LogElements} transform that logs each element's window. */ + public LogElements withWindow() { + return new LogElements<>(level, prefix, withTimestamp, true, withPaneInfo); + } + + /** Returns a new {@link LogElements} transform that logs each element's pane info. */ + public LogElements withPaneInfo() { + return new LogElements<>(level, prefix, withTimestamp, withWindow, true); + } + + @Override + public PCollection expand(PCollection input) { + return input.apply("Log", ParDo.of(new LoggingFn<>(this))); + } + + @Override + public void populateDisplayData(DisplayData.Builder builder) { + super.populateDisplayData(builder); + builder + .add(DisplayData.item("level", level.name()).withLabel("Log Level")) + .addIfNotDefault(DisplayData.item("prefix", prefix).withLabel("Prefix"), "") + .addIfNotDefault( + DisplayData.item("withTimestamp", withTimestamp).withLabel("Log Timestamp"), false) + .addIfNotDefault(DisplayData.item("withWindow", withWindow).withLabel("Log Window"), false) + .addIfNotDefault( + DisplayData.item("withPaneInfo", withPaneInfo).withLabel("Log Pane Info"), false); + } + + static String formatForLogging( + @Nullable Object element, + String prefix, + boolean withTimestamp, + boolean withWindow, + boolean withPaneInfo, + Instant timestamp, + BoundedWindow window, + PaneInfo paneInfo) { + StringBuilder builder = new StringBuilder(prefix).append(element); + if (withTimestamp) { + builder.append(", timestamp=").append(timestamp); + } + if (withWindow) { + builder.append(", window=").append(window); + } + if (withPaneInfo) { + builder.append(", paneInfo=").append(paneInfo); + } + return builder.toString(); + } + + @VisibleForTesting + static void log(Level level, String message) { + switch (level) { + case TRACE: + LOG.trace("{}", message); + break; + case DEBUG: + LOG.debug("{}", message); + break; + case INFO: + LOG.info("{}", message); + break; + case WARN: + LOG.warn("{}", message); + break; + case ERROR: + LOG.error("{}", message); + break; + default: + throw unsupportedLogLevel(level); + } + } + + private static boolean isLoggingEnabled(Level level) { + switch (level) { + case TRACE: + return LOG.isTraceEnabled(); + case DEBUG: + return LOG.isDebugEnabled(); + case INFO: + return LOG.isInfoEnabled(); + case WARN: + return LOG.isWarnEnabled(); + case ERROR: + return LOG.isErrorEnabled(); + default: + throw unsupportedLogLevel(level); + } + } + + private static IllegalArgumentException unsupportedLogLevel(Level level) { + return new IllegalArgumentException("Unsupported log level: " + level); + } + + private static class LoggingFn extends DoFn { + private final Level level; + private final String prefix; + private final boolean withTimestamp; + private final boolean withWindow; + private final boolean withPaneInfo; + + private LoggingFn(LogElements transform) { + this.level = transform.level; + this.prefix = transform.prefix; + this.withTimestamp = transform.withTimestamp; + this.withWindow = transform.withWindow; + this.withPaneInfo = transform.withPaneInfo; + } + + @ProcessElement + public void processElement( + @Element T element, + @DoFn.Timestamp Instant timestamp, + BoundedWindow window, + PaneInfo paneInfo, + OutputReceiver receiver) { + if (isLoggingEnabled(level)) { + log( + level, + formatForLogging( + element, + prefix, + withTimestamp, + withWindow, + withPaneInfo, + timestamp, + window, + paneInfo)); + } + receiver.output(element); + } + } +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/LogElementsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/LogElementsTest.java new file mode 100644 index 000000000000..4d5409e362fb --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/LogElementsTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.transforms; + +import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasDisplayItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +import java.util.Arrays; +import java.util.List; +import org.apache.beam.sdk.testing.ExpectedLogs; +import org.apache.beam.sdk.testing.NeedsRunner; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.display.DisplayData; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.PCollection; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.slf4j.event.Level; + +/** Tests for {@link LogElements}. */ +@RunWith(JUnit4.class) +public class LogElementsTest { + @Rule public final transient TestPipeline pipeline = TestPipeline.create(); + @Rule public ExpectedLogs expectedLogs = ExpectedLogs.none(LogElements.class); + + @Test + @Category(NeedsRunner.class) + public void testLogElementsPreservesElements() { + List elements = Arrays.asList("a", "b", "c"); + + PCollection output = + pipeline.apply(Create.of(elements)).apply(LogElements.info()); + + PAssert.that(output).containsInAnyOrder(elements); + pipeline.run(); + } + + @Test + public void testFormatForLoggingIncludesConfiguredMetadata() { + Instant timestamp = new Instant(0); + IntervalWindow window = new IntervalWindow(timestamp, Duration.standardMinutes(1)); + + String message = + LogElements.formatForLogging( + "a", "row: ", true, true, true, timestamp, window, PaneInfo.NO_FIRING); + + assertThat(message, containsString("row: a")); + assertThat(message, containsString("timestamp=1970-01-01T00:00:00.000Z")); + assertThat( + message, containsString("window=[1970-01-01T00:00:00.000Z..1970-01-01T00:01:00.000Z)")); + assertThat(message, containsString("paneInfo=PaneInfo.NO_FIRING")); + } + + @Test + public void testLogElementsLogsAtConfiguredLevels() { + LogElements.log(Level.TRACE, "trace: trace-element"); + LogElements.log(Level.DEBUG, "debug: debug-element"); + LogElements.log(Level.INFO, "info: info-element"); + LogElements.log(Level.WARN, "warn: warn-element"); + LogElements.log(Level.ERROR, "error: error-element"); + + expectedLogs.verifyTrace("trace: trace-element"); + expectedLogs.verifyDebug("debug: debug-element"); + expectedLogs.verifyInfo("info: info-element"); + expectedLogs.verifyWarn("warn: warn-element"); + expectedLogs.verifyError("error: error-element"); + } + + @Test + public void testDisplayData() { + DisplayData displayData = + DisplayData.from( + LogElements.of(Level.WARN) + .withPrefix("row: ") + .withTimestamp() + .withWindow() + .withPaneInfo()); + + assertThat(displayData, hasDisplayItem("level", "WARN")); + assertThat(displayData, hasDisplayItem("prefix", "row: ")); + assertThat(displayData, hasDisplayItem("withTimestamp", true)); + assertThat(displayData, hasDisplayItem("withWindow", true)); + assertThat(displayData, hasDisplayItem("withPaneInfo", true)); + } +} From 0ca91be257bc33b9dc451ec9b1d9d93b3be2ac2c Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:07:04 -0400 Subject: [PATCH 306/490] Move google-auth lower bounds to >= 2.0.0 (#38778) --- sdks/python/setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdks/python/setup.py b/sdks/python/setup.py index b8fd2f5f8349..aabe0395f6dd 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -526,10 +526,7 @@ def get_portability_package_data(): 'google-api-core>=2.0.0,<3', 'google-apitools>=0.5.31,<0.5.32; python_version < "3.13"', 'google-apitools>=0.5.35; python_version >= "3.13"', - # NOTE: Maintainers, please do not require google-auth>=2.x.x - # Until this issue is closed - # https://github.com/googleapis/google-cloud-python/issues/10566 - 'google-auth>=1.18.0,<3', + 'google-auth>=2.0.0,<3', 'google-auth-httplib2>=0.1.0,<0.3.0', 'google-cloud-datastore>=2.0.0,<3', 'google-cloud-pubsub>=2.1.0,<3', From 30af259b63d322e0576e8d2f0de212e9f010fb68 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 4 Jun 2026 13:15:52 -0400 Subject: [PATCH 307/490] Normalize types in dataclass field type resolving (#38797) Add a pipeline option to allow fallback to Any --- CHANGES.md | 3 +++ .../apache_beam/options/pipeline_options.py | 9 +++++++ sdks/python/apache_beam/typehints/opcodes.py | 4 ++- .../typehints/trivial_inference.py | 26 +++++++++++++++++++ .../typehints/trivial_inference_test.py | 21 ++++++++++++--- .../python/apache_beam/typehints/typehints.py | 1 + 6 files changed, 60 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b8b10c352fae..698d88b01fab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -74,6 +74,9 @@ ## Breaking Changes * X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). +* (Python) Typehints of dataclass fields are honored during type inferences. To restore the behavior of fallback-to-any, + use pipeline option `--exclude_infer_dataclass_field_type` ([#38797](https://github.com/apache/beam/issues/38797)). + However fixing forward is recommended. ## Deprecations diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index e3ab13e25122..a5b66ce28ac5 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -888,6 +888,15 @@ def _add_argparse_args(cls, parser): default=False, action='store_true', help='Disable the use of beartype for type checking.') + parser.add_argument( + '--exclude_infer_dataclass_field_type', + default=False, + action='store_true', + help='Exclude certain typehint inference involving dataclass fields ' + 'and resolve to Any (as in beam<=2.74.0). NOTE: this option is ' + 'for backward compatibility only and the exclusion scenarios are ' + 'subject to change or remove in a future version. For details see: ' + 'https://beam.apache.org/releases/pydoc/current/apache_beam.typehints.trivial_inference.html#apache_beam.typehints.trivial_inference.resolve_dataclass_field_type') # pylint: disable=line-too-long parser.add_argument( '--runtime_type_check', default=False, diff --git a/sdks/python/apache_beam/typehints/opcodes.py b/sdks/python/apache_beam/typehints/opcodes.py index 963b5e0850b6..53eabdadc4af 100644 --- a/sdks/python/apache_beam/typehints/opcodes.py +++ b/sdks/python/apache_beam/typehints/opcodes.py @@ -42,6 +42,7 @@ from apache_beam.typehints.trivial_inference import Const from apache_beam.typehints.trivial_inference import element_type from apache_beam.typehints.trivial_inference import key_value_types +from apache_beam.typehints.trivial_inference import resolve_dataclass_field_type from apache_beam.typehints.trivial_inference import union from apache_beam.typehints.typehints import Any from apache_beam.typehints.typehints import Dict @@ -451,8 +452,9 @@ def _getattr(o, name): elif inspect.isclass(o) and dataclasses.is_dataclass(o): field = o.__dataclass_fields__.get(name) if field is not None: - return field.type + return resolve_dataclass_field_type(field.type) return Any + else: return Any diff --git a/sdks/python/apache_beam/typehints/trivial_inference.py b/sdks/python/apache_beam/typehints/trivial_inference.py index 68e126a89393..69edfc309281 100644 --- a/sdks/python/apache_beam/typehints/trivial_inference.py +++ b/sdks/python/apache_beam/typehints/trivial_inference.py @@ -774,3 +774,29 @@ def infer_return_type_func(f, input_types, debug=False, depth=0): if debug: print(f, id(f), input_types, '->', result) return result + + +def resolve_dataclass_field_type(x): + """ + Resolve a type to Beam typehint under global pipeline option context. + + Since Beam 2.75.0, typehints of dataclass fields are honored during type + inferences. However, in case of breakage (possible scenarios include + incorrect typehints; non-deterministic or nullable types disallowed by + consumer transform but check disabled by Any; tests rely on Any), + --exclude_infer_dataclass_field_type option to instruct falling back to Any. + Fields of builtin primitives are always respected. + """ + from apache_beam.options.pipeline_options_context import get_pipeline_options + options = get_pipeline_options() + if options: + from apache_beam.options.pipeline_options import TypeOptions + disabled = options.view_as(TypeOptions).exclude_infer_dataclass_field_type + else: + disabled = False + + if not disabled: + return typehints.normalize(x) + if x in (bool, bytes, complex, float, int, str): + return x + return Any diff --git a/sdks/python/apache_beam/typehints/trivial_inference_test.py b/sdks/python/apache_beam/typehints/trivial_inference_test.py index f421819bdcae..dcb0bac97e80 100644 --- a/sdks/python/apache_beam/typehints/trivial_inference_test.py +++ b/sdks/python/apache_beam/typehints/trivial_inference_test.py @@ -24,6 +24,8 @@ import unittest import apache_beam as beam +from apache_beam.options.pipeline_options import PipelineOptions +from apache_beam.options.pipeline_options_context import scoped_pipeline_options from apache_beam.typehints import row_type from apache_beam.typehints import trivial_inference from apache_beam.typehints import typehints @@ -489,15 +491,28 @@ def testPyCallable(self): [int]) def testDataClassFields(self): + @dataclasses.dataclass + class BaseClass: + pass + @dataclasses.dataclass class MyDataClass: id: int name: str + tags: list[str] + custom: BaseClass self.assertReturnType( - typehints.Tuple[int, str], - python_callable.PythonCallableWithSource("lambda x: (x.id, x.name)"), - [MyDataClass]) + typehints.Tuple[int, str, typehints.List[str], BaseClass], + python_callable.PythonCallableWithSource( + "lambda x: (x.id, x.name, x.tags, x.custom)"), [MyDataClass]) + + options = PipelineOptions(['--exclude_infer_dataclass_field_type']) + with scoped_pipeline_options(options): + self.assertReturnType( + typehints.Tuple[int, str, typehints.Any, typehints.Any], + python_callable.PythonCallableWithSource( + "lambda x: (x.id, x.name, x.tags, x.custom)"), [MyDataClass]) if __name__ == '__main__': diff --git a/sdks/python/apache_beam/typehints/typehints.py b/sdks/python/apache_beam/typehints/typehints.py index 6dc88a93dd39..ffef40de6673 100644 --- a/sdks/python/apache_beam/typehints/typehints.py +++ b/sdks/python/apache_beam/typehints/typehints.py @@ -1453,6 +1453,7 @@ def __getitem__(self, type_params): def normalize(x, none_as_type=False): + """Normalize a type to Beam typehint.""" # None is inconsistantly used for Any, unknown, or NoneType. # Avoid circular imports From e5d0b0f0987e4709ec1f721fcc771c8c843e228b Mon Sep 17 00:00:00 2001 From: Anurag Pappula <127645370+Anuragp22@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:53:44 +0530 Subject: [PATCH 308/490] [Python] Honor disableCounterMetrics, disableStringSetMetrics, and disableBoundedTrieMetrics experiments (#38749) * [Python] Honor disableCounterMetrics / disableStringSetMetrics / disableBoundedTrieMetrics experiments Mirrors the Java SDK Metrics.MetricsFlag behavior so high-throughput Python pipelines can opt out of user metric kinds that add pressure to metric backends. Adds a process-wide MetricsFlag singleton initialized once at worker harness startup from pipeline experiments. When a flag is set, the corresponding Delegating* class short-circuits its update path so no MetricCell is touched and no monitoring info is emitted. For #38746. * [Python] Add unit tests for MetricsFlag Ports Java MetricsTest.testMetricsFlag and adds three smoke tests confirming the disabled Counter / StringSet / BoundedTrie short-circuit without raising when called on a no-current-tracker path. For #38746. * [Python] Note disable*Metrics support in CHANGES.md For #38746. * [Python] Use public class attrs on MetricsFlag for hot-path reads Drops the classmethod getter wrapper around each disabled flag and exposes counter_disabled / string_set_disabled / bounded_trie_disabled directly as public class attributes. The gate runs on every metric update, so swapping a descriptor lookup + function call for a single attribute load matters in exactly the high-throughput pipelines these experiments are designed for. Idiomatic Python; java parity is about behavior, not internal API shape. Addresses review feedback. For #38746. * [Python] Address PR review: simplify MetricsFlag pydoc and reset in finally * [Python] Address PR review: assert no MetricCell created when disabled * [Python] Initialize MetricsFlag from Pipeline so in-process runners honor disable* experiments The previous init only ran in the gRPC SDK harness (sdk_worker_main), so DirectRunner and other in-process runners silently ignored the disableCounterMetrics / disableStringSetMetrics / disableBoundedTrieMetrics experiments. Initializing in Pipeline.__init__ matches the pattern of FileSystems.set_options at the same call site and mirrors the Java init point (PipelineRunner.run). --------- Co-authored-by: Yi Hu --- CHANGES.md | 1 + sdks/python/apache_beam/metrics/metric.py | 56 +++++++++- .../python/apache_beam/metrics/metric_test.py | 104 ++++++++++++++++++ sdks/python/apache_beam/pipeline.py | 2 + .../runners/worker/sdk_worker_main.py | 2 + 5 files changed, 161 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 698d88b01fab..ac8215f35494 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,7 @@ ## New Features / Improvements +* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). ## Breaking Changes diff --git a/sdks/python/apache_beam/metrics/metric.py b/sdks/python/apache_beam/metrics/metric.py index b15237cae020..a66eed640be6 100644 --- a/sdks/python/apache_beam/metrics/metric.py +++ b/sdks/python/apache_beam/metrics/metric.py @@ -46,11 +46,13 @@ from apache_beam.metrics.metricbase import Histogram from apache_beam.metrics.metricbase import MetricName from apache_beam.metrics.metricbase import StringSet +from apache_beam.options.pipeline_options import DebugOptions if TYPE_CHECKING: from apache_beam.internal.metrics.metric import MetricLogger from apache_beam.metrics.execution import MetricKey from apache_beam.metrics.metricbase import Metric + from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.utils.histogram import BucketType __all__ = ['Metrics', 'MetricsFilter', 'Lineage'] @@ -58,6 +60,37 @@ _LOGGER = logging.getLogger(__name__) +class MetricsFlag(object): + """Process-wide flags controlling which user metric kinds are emitted.""" + counter_disabled = False + string_set_disabled = False + bounded_trie_disabled = False + _initialized = False + + @classmethod + def set_default_pipeline_options(cls, options: 'PipelineOptions') -> None: + if cls._initialized: + return + debug_options = options.view_as(DebugOptions) + if debug_options.lookup_experiment('disableCounterMetrics'): + cls.counter_disabled = True + _LOGGER.info('Counter metrics are disabled.') + if debug_options.lookup_experiment('disableStringSetMetrics'): + cls.string_set_disabled = True + _LOGGER.info('StringSet metrics are disabled.') + if debug_options.lookup_experiment('disableBoundedTrieMetrics'): + cls.bounded_trie_disabled = True + _LOGGER.info('BoundedTrie metrics are disabled.') + cls._initialized = True + + @classmethod + def reset(cls) -> None: + cls.counter_disabled = False + cls.string_set_disabled = False + cls.bounded_trie_disabled = False + cls._initialized = False + + class Metrics(object): """Lets users create/access metric objects during pipeline execution.""" @staticmethod @@ -204,12 +237,17 @@ class DelegatingCounter(Counter): def __init__( self, metric_name: MetricName, process_wide: bool = False) -> None: super().__init__(metric_name) - self.inc = MetricUpdater( # type: ignore[method-assign] + self._updater = MetricUpdater( cells.CounterCell, metric_name, default_value=1, process_wide=process_wide) + def inc(self, n: int = 1) -> None: + if MetricsFlag.counter_disabled: + return + self._updater(n) + class DelegatingDistribution(Distribution): """Metrics Distribution Delegates functionality to MetricsEnvironment.""" def __init__( @@ -231,13 +269,23 @@ class DelegatingStringSet(StringSet): """Metrics StringSet that Delegates functionality to MetricsEnvironment.""" def __init__(self, metric_name: MetricName) -> None: super().__init__(metric_name) - self.add = MetricUpdater(cells.StringSetCell, metric_name) # type: ignore[method-assign] + self._updater = MetricUpdater(cells.StringSetCell, metric_name) + + def add(self, value: str) -> None: + if MetricsFlag.string_set_disabled: + return + self._updater(value) class DelegatingBoundedTrie(BoundedTrie): - """Metrics StringSet that Delegates functionality to MetricsEnvironment.""" + """Metrics BoundedTrie that Delegates functionality to MetricsEnvironment.""" def __init__(self, metric_name: MetricName) -> None: super().__init__(metric_name) - self.add = MetricUpdater(cells.BoundedTrieCell, metric_name) # type: ignore[method-assign] + self._updater = MetricUpdater(cells.BoundedTrieCell, metric_name) + + def add(self, value) -> None: + if MetricsFlag.bounded_trie_disabled: + return + self._updater(value) class MetricResults(object): diff --git a/sdks/python/apache_beam/metrics/metric_test.py b/sdks/python/apache_beam/metrics/metric_test.py index ae66200737b5..6937236a8aad 100644 --- a/sdks/python/apache_beam/metrics/metric_test.py +++ b/sdks/python/apache_beam/metrics/metric_test.py @@ -32,7 +32,9 @@ from apache_beam.metrics.metric import MetricResults from apache_beam.metrics.metric import Metrics from apache_beam.metrics.metric import MetricsFilter +from apache_beam.metrics.metric import MetricsFlag from apache_beam.metrics.metricbase import MetricName +from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.runners.direct.direct_runner import BundleBasedDirectRunner from apache_beam.runners.worker import statesampler from apache_beam.testing.metric_result_matchers import DistributionMatcher @@ -121,6 +123,108 @@ def test_get_namespace_error(self): with self.assertRaises(ValueError): Metrics.get_namespace(object()) + def test_metrics_flag(self): + MetricsFlag.reset() + try: + self.assertFalse(MetricsFlag.counter_disabled) + self.assertFalse(MetricsFlag.string_set_disabled) + self.assertFalse(MetricsFlag.bounded_trie_disabled) + + options = PipelineOptions(['--experiments=disableCounterMetrics']) + MetricsFlag.set_default_pipeline_options(options) + self.assertTrue(MetricsFlag.counter_disabled) + self.assertFalse(MetricsFlag.string_set_disabled) + self.assertFalse(MetricsFlag.bounded_trie_disabled) + + MetricsFlag.reset() + options = PipelineOptions(['--experiments=disableStringSetMetrics']) + MetricsFlag.set_default_pipeline_options(options) + self.assertFalse(MetricsFlag.counter_disabled) + self.assertTrue(MetricsFlag.string_set_disabled) + self.assertFalse(MetricsFlag.bounded_trie_disabled) + + MetricsFlag.reset() + options = PipelineOptions(['--experiments=disableBoundedTrieMetrics']) + MetricsFlag.set_default_pipeline_options(options) + self.assertFalse(MetricsFlag.counter_disabled) + self.assertFalse(MetricsFlag.string_set_disabled) + self.assertTrue(MetricsFlag.bounded_trie_disabled) + + MetricsFlag.reset() + options = PipelineOptions([ + '--experiments=disableCounterMetrics', + '--experiments=disableStringSetMetrics', + '--experiments=disableBoundedTrieMetrics', + ]) + MetricsFlag.set_default_pipeline_options(options) + self.assertTrue(MetricsFlag.counter_disabled) + self.assertTrue(MetricsFlag.string_set_disabled) + self.assertTrue(MetricsFlag.bounded_trie_disabled) + finally: + MetricsFlag.reset() + + def test_disabled_counter_is_noop(self): + sampler = statesampler.StateSampler('', counters.CounterFactory()) + statesampler.set_current_tracker(sampler) + state = sampler.scoped_state( + 'mystep', 'myState', metrics_container=MetricsContainer('mystep')) + MetricsFlag.reset() + try: + sampler.start() + with state: + container = MetricsEnvironment.current_container() + Metrics.counter('ns', 'baseline').inc() + self.assertEqual(len(container.metrics), 1) + options = PipelineOptions(['--experiments=disableCounterMetrics']) + MetricsFlag.set_default_pipeline_options(options) + Metrics.counter('ns', 'after_disable').inc() + Metrics.counter('ns', 'after_disable').inc(5) + Metrics.counter('ns', 'after_disable').dec() + self.assertEqual(len(container.metrics), 1) + finally: + sampler.stop() + MetricsFlag.reset() + + def test_disabled_string_set_is_noop(self): + sampler = statesampler.StateSampler('', counters.CounterFactory()) + statesampler.set_current_tracker(sampler) + state = sampler.scoped_state( + 'mystep', 'myState', metrics_container=MetricsContainer('mystep')) + MetricsFlag.reset() + try: + sampler.start() + with state: + container = MetricsEnvironment.current_container() + Metrics.string_set('ns', 'baseline').add('seed') + self.assertEqual(len(container.metrics), 1) + options = PipelineOptions(['--experiments=disableStringSetMetrics']) + MetricsFlag.set_default_pipeline_options(options) + Metrics.string_set('ns', 'after_disable').add('value') + self.assertEqual(len(container.metrics), 1) + finally: + sampler.stop() + MetricsFlag.reset() + + def test_disabled_bounded_trie_is_noop(self): + sampler = statesampler.StateSampler('', counters.CounterFactory()) + statesampler.set_current_tracker(sampler) + state = sampler.scoped_state( + 'mystep', 'myState', metrics_container=MetricsContainer('mystep')) + MetricsFlag.reset() + try: + sampler.start() + with state: + container = MetricsEnvironment.current_container() + Metrics.bounded_trie('ns', 'baseline').add(['a']) + self.assertEqual(len(container.metrics), 1) + options = PipelineOptions(['--experiments=disableBoundedTrieMetrics']) + MetricsFlag.set_default_pipeline_options(options) + Metrics.bounded_trie('ns', 'after_disable').add(['a', 'b']) + self.assertEqual(len(container.metrics), 1) + finally: + sampler.stop() + MetricsFlag.reset() + def test_counter_empty_name(self): with self.assertRaises(ValueError): Metrics.counter("namespace", "") diff --git a/sdks/python/apache_beam/pipeline.py b/sdks/python/apache_beam/pipeline.py index 750868f7443a..594660d9bea9 100644 --- a/sdks/python/apache_beam/pipeline.py +++ b/sdks/python/apache_beam/pipeline.py @@ -73,6 +73,7 @@ from apache_beam.coders import typecoders from apache_beam.internal import pickler from apache_beam.io.filesystems import FileSystems +from apache_beam.metrics.metric import MetricsFlag from apache_beam.options.pipeline_options import CrossLanguageOptions from apache_beam.options.pipeline_options import DebugOptions from apache_beam.options.pipeline_options import PipelineOptions @@ -192,6 +193,7 @@ def __init__( self._options = PipelineOptions([]) FileSystems.set_options(self._options) + MetricsFlag.set_default_pipeline_options(self._options) if runner is None: runner = self._options.view_as(StandardOptions).runner diff --git a/sdks/python/apache_beam/runners/worker/sdk_worker_main.py b/sdks/python/apache_beam/runners/worker/sdk_worker_main.py index 754a631eaf33..58beda96d63d 100644 --- a/sdks/python/apache_beam/runners/worker/sdk_worker_main.py +++ b/sdks/python/apache_beam/runners/worker/sdk_worker_main.py @@ -33,6 +33,7 @@ from apache_beam.internal import pickler from apache_beam.io import filesystems +from apache_beam.metrics import metric from apache_beam.options.pipeline_options import DebugOptions from apache_beam.options.pipeline_options import GoogleCloudOptions from apache_beam.options.pipeline_options import PipelineOptions @@ -123,6 +124,7 @@ def create_harness(environment, dry_run=False): RuntimeValueProvider.set_runtime_options(pipeline_options_dict) sdk_pipeline_options = PipelineOptions.from_dictionary(pipeline_options_dict) filesystems.FileSystems.set_options(sdk_pipeline_options) + metric.MetricsFlag.set_default_pipeline_options(sdk_pipeline_options) pickle_library = sdk_pipeline_options.view_as(SetupOptions).pickle_library pickler.set_library(pickle_library) From 6faf62359ceb29c9aebe839491834587d628d9bc Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Fri, 5 Jun 2026 00:45:49 -0700 Subject: [PATCH 309/490] [Dataflow] Fix thread safety of HotKey logger (#38816) --- .../beam/runners/dataflow/worker/HotKeyLogger.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/HotKeyLogger.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/HotKeyLogger.java index 00d93890d9a2..c449f9d6c825 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/HotKeyLogger.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/HotKeyLogger.java @@ -18,6 +18,7 @@ package org.apache.beam.runners.dataflow.worker; import com.google.api.client.util.Clock; +import javax.annotation.concurrent.GuardedBy; import org.apache.beam.runners.dataflow.util.TimeUtil; import org.joda.time.Duration; import org.slf4j.Logger; @@ -33,6 +34,7 @@ public class HotKeyLogger { * The previous time the HotKeyDetection was logged. This is used to throttle logging to every 5 * minutes. */ + @GuardedBy("this") private long prevHotKeyDetectionLogMs = 0; /** Throttles logging the detection to every loggingPeriod */ @@ -83,10 +85,12 @@ public void logHotKeyDetection(String userStepName, Duration hotKeyAge, Object h protected boolean isThrottled() { // Throttle logging the HotKeyDetection to every 5 minutes. long nowMs = clock.currentTimeMillis(); - if (nowMs - prevHotKeyDetectionLogMs < loggingPeriod.getMillis()) { - return true; + synchronized (this) { + if (nowMs - prevHotKeyDetectionLogMs < loggingPeriod.getMillis()) { + return true; + } + prevHotKeyDetectionLogMs = nowMs; } - prevHotKeyDetectionLogMs = nowMs; return false; } } From 13df084e47597ae2187032875c7c6cba69dfa861 Mon Sep 17 00:00:00 2001 From: Akshat <156604423+Akshatsharma2205@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:30:05 +0530 Subject: [PATCH 310/490] Fix BigQuery Storage Write API stream count for bounded writes (#38776) * Fix BigQuery Storage Write API stream count for bounded writes --- .../BigQueryStorageWriteApiSchemaTransformProvider.java | 9 +++++---- .../bigquery/providers/BigQueryWriteConfiguration.java | 9 +++++++-- ...gQueryStorageWriteApiSchemaTransformProviderTest.java | 5 ++++- sdks/python/apache_beam/io/gcp/bigquery.py | 3 +-- .../en/documentation/io/built-in/google-bigquery.md | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProvider.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProvider.java index e3d947235015..1d618ba685ed 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProvider.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProvider.java @@ -179,11 +179,11 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { PCollection inputRows = input.getSinglePCollection(); BigQueryIO.Write write = createStorageWriteApiTransform(inputRows.getSchema()); + int numStreams = configuration.getNumStreams() == null ? 0 : configuration.getNumStreams(); if (inputRows.isBounded() == IsBounded.UNBOUNDED) { Long triggeringFrequency = configuration.getTriggeringFrequencySeconds(); Boolean autoSharding = configuration.getAutoSharding(); - int numStreams = configuration.getNumStreams() == null ? 0 : configuration.getNumStreams(); boolean useAtLeastOnceSemantics = configuration.getUseAtLeastOnceSemantics() != null @@ -197,12 +197,13 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { : Duration.standardSeconds(triggeringFrequency)); } // set num streams if specified, otherwise default to autoSharding - if (numStreams > 0) { - write = write.withNumStorageWriteApiStreams(numStreams); - } else if (autoSharding == null || autoSharding) { + if (numStreams == 0 && (autoSharding == null || autoSharding)) { write = write.withAutoSharding(); } } + if (numStreams > 0) { + write = write.withNumStorageWriteApiStreams(numStreams); + } Schema inputSchema = inputRows.getSchema(); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryWriteConfiguration.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryWriteConfiguration.java index 55d7f7c8d72a..7f09feb245cd 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryWriteConfiguration.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryWriteConfiguration.java @@ -101,6 +101,12 @@ public void validate() { Boolean autoSharding = getAutoSharding(); Integer numStreams = getNumStreams(); + if (numStreams != null) { + checkArgument( + numStreams >= 0, + invalidConfigMessage + "numStreams must be non-negative, but was: %s", + numStreams); + } if (autoSharding != null && autoSharding && numStreams != null) { checkArgument( numStreams == 0, @@ -152,8 +158,7 @@ public static Builder builder() { public abstract Boolean getAutoSharding(); @SchemaFieldDescription( - "Specifies the number of write streams that the Storage API sink will use. " - + "This parameter is only applicable when writing unbounded data.") + "Specifies the number of write streams that the Storage API sink will use.") @Nullable public abstract Integer getNumStreams(); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProviderTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProviderTest.java index 584309778286..81789f784255 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProviderTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/providers/BigQueryStorageWriteApiSchemaTransformProviderTest.java @@ -122,7 +122,10 @@ public void testInvalidConfig() { Arrays.asList( BigQueryWriteConfiguration.builder() .setTable("project:dataset.table") - .setCreateDisposition("INVALID_DISPOSITION")); + .setCreateDisposition("INVALID_DISPOSITION"), + BigQueryWriteConfiguration.builder() + .setTable("project:dataset.table") + .setNumStreams(-1)); for (BigQueryWriteConfiguration.Builder config : invalidConfigs) { assertThrows( diff --git a/sdks/python/apache_beam/io/gcp/bigquery.py b/sdks/python/apache_beam/io/gcp/bigquery.py index d751d60c905f..a2d17f12569e 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery.py +++ b/sdks/python/apache_beam/io/gcp/bigquery.py @@ -2155,8 +2155,7 @@ def __init__( all of FILE_LOADS, STREAMING_INSERTS, and STORAGE_WRITE_API. Only applicable to unbounded input. num_storage_api_streams: Specifies the number of write streams that the - Storage API sink will use. This parameter is only applicable when - writing unbounded data. + Storage API sink will use. ignore_unknown_columns: Accept rows that contain values that do not match the schema. The unknown values are ignored. Default is False, which treats unknown values as errors. This option is only valid for diff --git a/website/www/site/content/en/documentation/io/built-in/google-bigquery.md b/website/www/site/content/en/documentation/io/built-in/google-bigquery.md index 9c205f092663..00cd1ffbcdef 100644 --- a/website/www/site/content/en/documentation/io/built-in/google-bigquery.md +++ b/website/www/site/content/en/documentation/io/built-in/google-bigquery.md @@ -855,7 +855,7 @@ pipeline uses. You can set it explicitly on the transform via [`withNumStorageWriteApiStreams`](https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.Write.html#withNumStorageWriteApiStreams-int-) or provide the `numStorageWriteApiStreams` option to the pipeline as defined in [`BigQueryOptions`](https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.html). -Please note this is only supported for streaming pipelines. +Fixed stream counts can be used with both batch and streaming pipelines. Triggering frequency determines how soon the data is visible for querying in BigQuery. You can explicitly set it via From e11dbe1f46ae458cfc9f307a7abef8c3ce361d00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:03:59 -0400 Subject: [PATCH 311/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38825) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.24 to 1.22.25. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.24...feature/s3/manager/v1.22.25) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.25 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 38 +++++++++++++-------------- sdks/go.sum | 76 ++++++++++++++++++++++++++--------------------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 011b2e0fe8c6..b73232731187 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,12 +32,12 @@ require ( cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.2 - github.com/aws/aws-sdk-go-v2 v1.41.11 - github.com/aws/aws-sdk-go-v2/config v1.32.22 - github.com/aws/aws-sdk-go-v2/credentials v1.19.21 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.24 - github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 - github.com/aws/smithy-go v1.27.0 + github.com/aws/aws-sdk-go-v2 v1.41.12 + github.com/aws/aws-sdk-go-v2/config v1.32.23 + github.com/aws/aws-sdk-go-v2/credentials v1.19.22 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 + github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 + github.com/aws/smithy-go v1.27.1 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 @@ -91,7 +91,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -148,18 +148,18 @@ require ( github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.28 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 74dc7d7358ea..a3bcd79b2bc1 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -199,83 +199,83 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= -github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= +github.com/aws/aws-sdk-go-v2 v1.41.12 h1:DIKX2c31ekm9RA2D9FBj1EWXx++9AdAqRw+e78Tq2Ck= +github.com/aws/aws-sdk-go-v2 v1.41.12/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 h1:oRtsqWgxbpeXrOlxOoQStx2M9WNbIkPq4C4Xn1or6bc= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12/go.mod h1:Zg0Oe9qT+9wcezlm1a64wGJp2qZdRElVxo/seJf7jYU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.22 h1:Vfvp7+fYKsVCADcWOEllqEV47aIBXhNchvyDFu1B5fY= -github.com/aws/aws-sdk-go-v2/config v1.32.22/go.mod h1:0+H+0nPKbvWltf5vSIGkApv+hGbaQ4FfwTjGIYQREcw= +github.com/aws/aws-sdk-go-v2/config v1.32.23 h1:PYDobtcsJXK6bQe9I8RQk6s19Bz3xa3xRU08Hy1Em3Y= +github.com/aws/aws-sdk-go-v2/config v1.32.23/go.mod h1:QID4dqUQVgEOYPKsPWd1sNWCCR2c5g7o3jeEtIXPOZU= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.21 h1:0+HscFXtNa4+3buV4IlG6v5lnOdzi5TrpicFGjKHgh4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.21/go.mod h1:UE8+9t5zudFwu5k5ShC1PKArVEdOkQQdCXIHQAVNUcU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.22 h1:SHfH6wyPsEgG7fVsi5rQxWEt7tuIcN2PGhb1mTFv6tE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.22/go.mod h1:54nO8lKD4aQPOntM/VTWjnR+DYzTwx0YkSMZMhAgewQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 h1:BEfN1sjtiKEdikRDxYkjZNE4tyvw/YbGWCbl3xDZgRw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27/go.mod h1:ISGSFNbOHRS+JV/17yStzRTPBUHHqF92kCpRLLyH3Nk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 h1:b+kcDejJrXc30zU/w8Tc9klISwaO5wh+6T0sMBdDoHM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28/go.mod h1:LnI62O9GnSv6GcuLXxOYqlq0C8EmxMcgnF6m7LdYuOY= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.24 h1:eRIrOCSlSjyb/tT6vAPLwUZQO71XWaLkqwtM8DQuQ2s= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.24/go.mod h1:ox5MjGuEKA/oXGNbprU9PW/nc+YALeHjDkIiHfzCUgM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 h1:xJ3WVH3J0xESIqkavgbNfvQdMzB98bSkGwFXCyM2Tdw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25/go.mod h1:0EOIx2v10rBBcaoTpOsRRNKDxhtRecRbN42YMrJaKZE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 h1:Xf2j7NdVcUKomlZ4iihOP4AZ3Fzlr8h4yKpXeP+OFPg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28/go.mod h1:O8cDo1dW63jU7ki//kRe1z+tLGcpnD1jrouitsQddDw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 h1:KqIfN9kpkKkcBqBbNpNGTIrXO6ExTUvFKvXkC+YAzVo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28/go.mod h1:uxtQiKvLtNS4iXVsH2McVD/ls8FKN/uUhe1hGxPjrw0= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 h1:eaS9vwQ5ym4Y9S6+G/K3d3lgZhxs9Sldcn/YS7cmdKY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28/go.mod h1:oTdbDr+BMs7gAYrNpD0LDTyqQfv6yOYgTDv46+xbwFY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 h1:VkE9FuzTQVjBBrnj4+oCdxCLFIz7aqLYKUCjtvxVcOs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29/go.mod h1:H32Z2Qth9b+9LqjyBsCnozMQ8H2N7YBUDVXwbs0iggg= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 h1:rFSsqDfCMPAmG70JOsYqFZCHXkyatoGa1K4YEt/BggQ= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11/go.mod h1:XG68qW+YLLFH0vnSDCou43Cgj5TeAG83O5NRSJgt04Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20 h1:yt2fjgev3Hqm33zPw0ZWtki3sZ0SLcr+PkuvXDAAf/8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.20/go.mod h1:wnPjCjPJ6x5GBhrER8f0QakaQ2LokfhCVYxmAZBpPjY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21 h1:FsZxbPiVgEHYofziwfylouMki8b1Z7mI4CMU/7bhwBA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21/go.mod h1:Mmm30OV+JLXYQUcbSd84THnv3P5JtjhVDujLwMqRG0U= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 h1:2/pUo42hhVmQcM21ttZoBOLHQymyUH8qEnZGTIuGBT8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27/go.mod h1:p7hwgbwompjCRNTdB3ytlldddNt1rDBgVVMqWEVG1II= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28 h1:axj4mEDletwKmTm/9jR+DkIMmCfcn5vE4jBMAAN+3Vg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28/go.mod h1:3Aaz69M0jqfSHLKqxgolgUBFT4hpwSNc7DzC95orEi8= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27 h1:JEXSW4wztrl1MoL5EMvJMO7lc/TRZloztrJKNl96SW8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.27/go.mod h1:8eL+YgEqy6IYqjwW6PG0Ubn59a2xtCzbz7Pi18JBu04= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.28 h1:li8rTZAAb22g4UsxbjwMdaNVWbgVcDzPqI7nDTI+mF4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.28/go.mod h1:/brXioSGIMEdcBFoubpSdmighSVp6poP+mma/wB7iHA= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1 h1:WkX5IXwcxgO/WPTvhEqoSW2L1GB1OyIxk0vuzzdTftc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.1/go.mod h1:9Q9ZHyiTItraw8BXpO48pk398Mou0YCSI+xvFcaGgxU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 h1:b4ikkRk22T4xYkEgaWc3Voe+3xbt5YbbFhNehOWyUiY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2/go.mod h1:Gp7eHZ0NZ8ZK5RXpoIUp/C8OeAmJqpCgdwEK1D/QOek= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 h1:t6U7sowMfOjTeZXtDOtgEJXsoJyX4MDag+sfWGwUM9M= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.3/go.mod h1:WhO1EH3phjFWValQDsExaxncgEWJsHeoTvuyQAj3jwU= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 h1:YcpVyIPLCbiypN6KSphijN5fC7DDjX114SqA7prnnxg= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.4/go.mod h1:5ZICS++oFTRPfa1GsBqFDWX/8WamZ/QQOcCzIuU/zLw= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 h1:TUV8oytPCX1PfVgZn0N8/sPZx7T0YasaMCBHox1erlw= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.1/go.mod h1:tEL1hqCrkgwrDVL04HuLxz1SLUXdh+4kKhWv1pXKeiY= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 h1:ySNWu7TPmj5fKFIa1GYvX+Ddxd5ccruqC20aMNuyWDM= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.2/go.mod h1:A+U9luAOwFeB1kseyWCITVg7/NntoPebCFR9pQ4ch9A= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 h1:p9+Fizo2sUB6wI5Yb3K5lmykQAGs5JrKLBV/me6613Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4/go.mod h1:0x10Wy0dVS4Gn552xhHY5th2QdYpfJf44EsfyYGV194= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 h1:KSzGGqfk39O+WU3OEyYbx6F7sLDQCqxlOJ+2IksfK6U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5/go.mod h1:ATs88lXDeQB6CZOgQ5BIl9JbYS+EsCWUSDyff6L/oVo= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 h1:r/vUkpLilfCA3sxbRnkHbJejaoVHEdj4FEhv+Zva4DU= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.1/go.mod h1:t01JURC8Fe5M+7R1K0vzIZ2NT04HqvZR+FjlHrHDT2A= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 h1:RTO7mmGyedgnNmcPh3yQizNfc6GKoV5iqfdJavuf9vw= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.2/go.mod h1:fBhUZXDin9YYqhcpOMjIcpdik25rVwWyxLdPH1RZd9s= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= -github.com/aws/smithy-go v1.27.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= From 9e651e98e734579d9239f5f68efd7165231e8245 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:56:22 -0400 Subject: [PATCH 312/490] Bump cloud.google.com/go/storage from 1.62.2 to 1.62.3 in /sdks (#38826) Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.62.2 to 1.62.3. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/storage/v1.62.2...storage/v1.62.3) --- updated-dependencies: - dependency-name: cloud.google.com/go/storage dependency-version: 1.62.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index b73232731187..7cbe318bb8f9 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -31,7 +31,7 @@ require ( cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 - cloud.google.com/go/storage v1.62.2 + cloud.google.com/go/storage v1.62.3 github.com/aws/aws-sdk-go-v2 v1.41.12 github.com/aws/aws-sdk-go-v2/config v1.32.23 github.com/aws/aws-sdk-go-v2/credentials v1.19.22 diff --git a/sdks/go.sum b/sdks/go.sum index a3bcd79b2bc1..0b5cc9db4bac 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -99,8 +99,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4= -cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= +cloud.google.com/go/storage v1.62.3 h1:SZq1t23NCI+e96dH77Dg3PEfsNNEjqO8zE5AnD8gVD0= +cloud.google.com/go/storage v1.62.3/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= From d947b4f812e89a8c0fbb0cc43e4a441d4e8b2237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Fri, 5 Jun 2026 17:14:56 +0200 Subject: [PATCH 313/490] [OpenTelemetry] Add gcp otel auth extension (#38773) * migrate otel auth extension. --- .../beam/gradle/BeamModulePlugin.groovy | 2 + .../build.gradle | 50 + .../gcp/auth/ConfigurableOption.java | 163 +++ ...thAutoConfigurationCustomizerProvider.java | 282 ++++ .../gcp/auth/GoogleAuthException.java | 69 + .../opentelemetry/gcp/auth/package-info.java | 20 + ...re.spi.AutoConfigurationCustomizerProvider | 16 + ...toConfigurationCustomizerProviderTest.java | 1234 +++++++++++++++++ settings.gradle.kts | 1 + 9 files changed, 1837 insertions(+) create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider create mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index ed2f91634327..9f34fb54e47b 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -869,6 +869,8 @@ class BeamModulePlugin implements Plugin { opentelemetry_sdk : "io.opentelemetry:opentelemetry-sdk", // opentelemetry-bom sets version opentelemetry_exporter_otlp : "io.opentelemetry:opentelemetry-exporter-otlp", // opentelemetry-bom sets version opentelemetry_extension_autoconfigure : "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", // opentelemetry-bom sets version + opentelemetry_proto : "io.opentelemetry.proto:opentelemetry-proto:$opentelemetry_version-alpha", + opentelemetry_sdk_testing : "io.opentelemetry:opentelemetry-sdk-testing:$opentelemetry_version", postgres : "org.postgresql:postgresql:$postgres_version", protobuf_java : "com.google.protobuf:protobuf-java:$protobuf_version", protobuf_java_util : "com.google.protobuf:protobuf-java-util:$protobuf_version", diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle new file mode 100644 index 000000000000..1b08b867b291 --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { id 'org.apache.beam.module' } +applyJavaNature( + automaticModuleName: 'org.apache.beam.sdk.extensions.opentelemetry.gcp.auth', +) + +description = "Apache Beam :: SDKs :: Java :: Extensions :: OpenTelemetry GCP Auth" +ext.summary = "OpenTelemetry extension that provides GCP authentication support for OTLP exporters." + +dependencies { + implementation enforcedPlatform(library.java.google_cloud_platform_libraries_bom) + implementation platform(library.java.opentelemetry_bom) + implementation library.java.google_auth_library_oauth2_http + implementation library.java.vendored_guava_32_1_2_jre + compileOnly library.java.opentelemetry_api + compileOnly library.java.opentelemetry_extension_autoconfigure + compileOnly library.java.opentelemetry_exporter_otlp + implementation project(path: ":sdks:java:core", configuration: "shadow") + + testImplementation library.java.junit + testImplementation library.java.mockito_core + testImplementation library.java.mockito_inline + testImplementation library.java.jupiter_api + testImplementation library.java.jupiter_params + testRuntimeOnly library.java.jupiter_engine + testImplementation library.java.truth + testImplementation library.java.opentelemetry_api + testImplementation library.java.opentelemetry_sdk + testImplementation library.java.opentelemetry_exporter_otlp + testImplementation library.java.opentelemetry_sdk_testing + testImplementation library.java.opentelemetry_extension_autoconfigure +} +test { useJUnitPlatform() } diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java new file mode 100644 index 000000000000..e18e5693e3a1 --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth; + +import static java.util.Locale.ROOT; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * An enum representing configurable options for a GCP Authentication Extension. Each option has a + * user-readable name and can be configured using environment variables or system properties. + * + *

    Copied from + * https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java + */ +enum ConfigurableOption { + /** + * Represents the Google Cloud Project ID option. Can be configured using the environment variable + * `GOOGLE_CLOUD_PROJECT` or the system property `google.cloud.project`. + */ + GOOGLE_CLOUD_PROJECT("Google Cloud Project ID"), + + /** + * Represents the Google Cloud Quota Project ID option. Can be configured using the environment + * variable `GOOGLE_CLOUD_QUOTA_PROJECT` or the system property `google.cloud.quota.project`. The + * quota project is the project that is used for quota management and billing for the API usage. + * + *

    The environment variable name is selected to be consistent with the official GCP client + * libraries. + */ + GOOGLE_CLOUD_QUOTA_PROJECT("Google Cloud Quota Project ID"), + + /** + * Specifies a comma-separated list of OpenTelemetry signals for which this authentication + * extension should be active. The authentication mechanisms provided by this extension will only + * be applied to the listed signals. If not set, {@code all} is assumed to be set which means + * authentication is enabled for all supported signals. + * + *

    Valid signal values are: + * + *

      + *
    • {@code metrics} - Enables authentication for metric exports. + *
    • {@code traces} - Enables authentication for trace exports. + *
    • {@code all} - Enables authentication for all exports. + *
    + * + *

    The values are case-sensitive. Whitespace around commas and values is ignored. Can be + * configured using the environment variable `GOOGLE_OTEL_AUTH_TARGET_SIGNALS` or the system + * property `google.otel.auth.target.signals`. + */ + GOOGLE_OTEL_AUTH_TARGET_SIGNALS("Target Signals for Google Authentication Extension"); + + private final String userReadableName; + private final String environmentVariableName; + private final String systemPropertyName; + + ConfigurableOption(String userReadableName) { + this.userReadableName = userReadableName; + this.environmentVariableName = this.name(); + this.systemPropertyName = this.environmentVariableName.toLowerCase(ROOT).replace('_', '.'); + } + + /** + * Returns the environment variable name associated with this option. + * + * @return the environment variable name (e.g., GOOGLE_CLOUD_PROJECT) + */ + String getEnvironmentVariable() { + return this.environmentVariableName; + } + + /** + * Returns the system property name associated with this option. + * + * @return the system property name (e.g., google.cloud.project) + */ + String getSystemProperty() { + return this.systemPropertyName; + } + + /** + * Returns the user readable name associated with this option. + * + * @return the user readable name (e.g., "Google Cloud Quota Project ID") + */ + String getUserReadableName() { + return this.userReadableName; + } + + /** + * Retrieves the configured value for this option. This method checks the environment variable + * first and then the system property. + * + * @return The configured value as a string, or throws an exception if not configured. + * @throws ConfigurationException if neither the environment variable nor the system property is + * set. + */ + String getConfiguredValue(ConfigProperties configProperties) { + String configuredValue = configProperties.getString(this.getSystemProperty()); + if (configuredValue != null && !configuredValue.isEmpty()) { + return configuredValue; + } else { + throw new ConfigurationException( + String.format( + "GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s", + this.userReadableName, this.getEnvironmentVariable(), this.getSystemProperty())); + } + } + + /** + * Retrieves the value for this option, prioritizing environment variables and system properties. + * If neither an environment variable nor a system property is set for this option, the provided + * fallback function is used to determine the value. + * + * @param fallback A {@link Supplier} that provides the default value for the option when it is + * not explicitly configured via an environment variable or system property. + * @return The configured value for the option, obtained from the environment variable, system + * property, or the fallback function, in that order of precedence. + */ + String getConfiguredValueWithFallback( + ConfigProperties configProperties, Supplier fallback) { + try { + return this.getConfiguredValue(configProperties); + } catch (ConfigurationException e) { + return fallback.get(); + } + } + + /** + * Retrieves the value for this option, prioritizing environment variables before system + * properties. If neither an environment variable nor a system property is set for this option, + * then an empty {@link Optional} is returned. + * + * @return The configured value for the option, if set, obtained from the environment variable, + * system property, or empty {@link Optional}, in that order of precedence. + */ + Optional getConfiguredValueAsOptional(ConfigProperties configProperties) { + try { + return Optional.of(this.getConfiguredValue(configProperties)); + } catch (ConfigurationException e) { + return Optional.empty(); + } + } +} diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java new file mode 100644 index 000000000000..ce2b142591bd --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auto.service.AutoService; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.beam.sdk.annotations.Internal; +import org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GoogleAuthException.Reason; + +/** + * An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) OpenTelemetry (OTLP) + * integration. + * + *

    This class is registered as a service provider using {@link AutoService} and is responsible + * for customizing the OpenTelemetry configuration for GCP specific behavior. It retrieves Google + * Application Default Credentials (ADC) and adds them as authorization headers to the configured + * {@link SpanExporter}. It also sets default properties and resource attributes for GCP + * integration. + * + *

    Copied from + * https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java + * + * @see AutoConfigurationCustomizerProvider + * @see GoogleCredentials + */ +@AutoService(AutoConfigurationCustomizerProvider.class) +@Internal +public class GcpAuthAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + static final String QUOTA_USER_PROJECT_HEADER = "x-goog-user-project"; + static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; + + static final String SIGNAL_TYPE_TRACES = "traces"; + static final String SIGNAL_TYPE_METRICS = "metrics"; + static final String SIGNAL_TYPE_ALL = "all"; + + private @Nullable GoogleCredentials credentials; + + /** + * Customizes the provided {@link AutoConfigurationCustomizer} such that authenticated exports to + * GCP Telemetry API are possible from the configured OTLP exporter. + * + *

    This method performs the following: + * + *

      + *
    • Verifies whether the configured OTLP endpoint (base or signal specific) is a known GCP + * endpoint. + *
    • If the configured base OTLP endpoint is a known GCP Telemetry API endpoint, customizes + * both the configured OTLP {@link SpanExporter} and {@link MetricExporter}. + *
    • If the configured signal specific endpoint is a known GCP Telemetry API endpoint, + * customizes only the signal specific exporter. + *
    + * + * The 'customization' performed includes customizing the exporters by adding required headers to + * the export calls made and customizing the resource by adding required resource attributes to + * enable GCP integration. + * + * @param autoConfiguration the AutoConfigurationCustomizer to customize. + */ + @Override + public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) { + autoConfiguration + .addSpanExporterCustomizer(this::customizeSpanExporter) + .addMetricExporterCustomizer(this::customizeMetricExporter) + .addResourceCustomizer(this::customizeResource); + } + + @Override + public int order() { + return Integer.MAX_VALUE - 1; + } + + private synchronized GoogleCredentials getCredentials() { + if (credentials == null) { + try { + credentials = GoogleCredentials.getApplicationDefault(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + } + return credentials; + } + + private SpanExporter customizeSpanExporter( + SpanExporter exporter, ConfigProperties configProperties) { + if (isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties)) { + return addAuthorizationHeaders(exporter, configProperties); + } + return exporter; + } + + private MetricExporter customizeMetricExporter( + MetricExporter exporter, ConfigProperties configProperties) { + if (isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties)) { + return addAuthorizationHeaders(exporter, configProperties); + } + return exporter; + } + + // Checks if the auth extension is configured to target the passed signal for authentication. + private static boolean isSignalTargeted(String checkSignal, ConfigProperties configProperties) { + String endpoint = configProperties.getString("otel.exporter.otlp." + checkSignal + ".endpoint"); + if (endpoint == null) { + endpoint = configProperties.getString("otel.exporter.otlp.endpoint"); + } + if (endpoint == null) { + return false; + } + + try { + java.net.URI uri = new java.net.URI(endpoint); + String host = uri.getHost(); + String scheme = uri.getScheme(); + if (host == null + || scheme == null + || !scheme.equalsIgnoreCase("https") + || (!host.equalsIgnoreCase("telemetry.googleapis.com") + && !host.equalsIgnoreCase("telemetry.mtls.googleapis.com"))) { + return false; + } + } catch (java.net.URISyntaxException e) { + return false; + } + + String userSpecifiedTargetedSignals = + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback( + configProperties, () -> SIGNAL_TYPE_ALL); + return stream(userSpecifiedTargetedSignals.split(",")) + .map(String::trim) + .anyMatch( + targetedSignal -> + targetedSignal.equals(checkSignal) || targetedSignal.equals(SIGNAL_TYPE_ALL)); + } + + private boolean isAnySignalTargeted(ConfigProperties configProperties) { + return isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties) + || isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties); + } + + // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and + // OtlpHttpSpanExporter. + private SpanExporter addAuthorizationHeaders( + SpanExporter exporter, ConfigProperties configProperties) { + if (exporter instanceof OtlpHttpSpanExporter) { + SpanExporter result = + ((OtlpHttpSpanExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(configProperties)) + .build(); + exporter.shutdown(); + return result; + } else if (exporter instanceof OtlpGrpcSpanExporter) { + SpanExporter result = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(configProperties)) + .build(); + exporter.shutdown(); + return result; + } + return exporter; + } + + // Adds authorization headers to the calls made by the OtlpGrpcMetricExporter and + // OtlpHttpMetricExporter. + private MetricExporter addAuthorizationHeaders( + MetricExporter exporter, ConfigProperties configProperties) { + if (exporter instanceof OtlpHttpMetricExporter) { + MetricExporter result = + ((OtlpHttpMetricExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(configProperties)) + .build(); + exporter.shutdown(); + return result; + } else if (exporter instanceof OtlpGrpcMetricExporter) { + MetricExporter result = + ((OtlpGrpcMetricExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(configProperties)) + .build(); + exporter.shutdown(); + return result; + } + return exporter; + } + + private Map getRequiredHeaderMap(ConfigProperties configProperties) { + GoogleCredentials creds = getCredentials(); + Map> gcpHeaders; + try { + // this also refreshes the credentials, if required + gcpHeaders = creds.getRequestMetadata(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); + } + if (gcpHeaders == null) { + return Map.of(); + } + Map flattenedHeaders = + gcpHeaders.entrySet().stream() + .filter(entry -> entry.getKey() != null && entry.getValue() != null) + .collect( + toMap( + Map.Entry::getKey, + entry -> + entry.getValue().stream() + .filter(Objects::nonNull) // Filter nulls + .filter(s -> !s.isEmpty()) // Filter empty strings + .collect(joining(",")))); + // Add quota user project header if not detected by the auth library and user provided it via + // system properties. + if (!flattenedHeaders.containsKey(QUOTA_USER_PROJECT_HEADER)) { + Optional maybeConfiguredQuotaProjectId = + ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueAsOptional( + configProperties); + maybeConfiguredQuotaProjectId.ifPresent( + configuredQuotaProjectId -> + flattenedHeaders.put(QUOTA_USER_PROJECT_HEADER, configuredQuotaProjectId)); + } + return flattenedHeaders; + } + + // Updates the current resource with the attributes required for ingesting OTLP data on GCP. + private Resource customizeResource(Resource resource, ConfigProperties configProperties) { + if (!isAnySignalTargeted(configProperties)) { + return resource; + } + + String gcpProjectId; + try { + gcpProjectId = ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties); + } catch (ConfigurationException e) { + gcpProjectId = getCredentials().getProjectId(); + if (gcpProjectId == null || gcpProjectId.isEmpty()) { + throw e; + } + } + Resource res = Resource.create(Attributes.of(stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId)); + return resource.merge(res); + } +} diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java new file mode 100644 index 000000000000..978b48934067 --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth; + +/** + * An unchecked exception indicating a failure during Google authentication. This exception is + * thrown when there are issues with retrieving or refreshing Google Application Default Credentials + * (ADC). + * + *

    Copied from + * https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GoogleAuthException.java + */ +public class GoogleAuthException extends RuntimeException { + + private static final long serialVersionUID = 149908685226796448L; + + /** + * Constructs a new {@code GoogleAuthException} with the specified reason and cause. + * + * @param reason the reason for the authentication failure. + * @param cause the underlying cause of the exception (e.g., an IOException). + */ + GoogleAuthException(Reason reason, Throwable cause) { + super(reason.message, cause); + } + + /** Enumerates the possible reasons for a Google authentication failure. */ + enum Reason { + /** Indicates a failure to retrieve Google Application Default Credentials. */ + FAILED_ADC_RETRIEVAL("Unable to retrieve Google Application Default Credentials."), + /** Indicates a failure to retrieve Google Application Default Credentials. */ + FAILED_ADC_REFRESH("Unable to refresh Google Application Default Credentials."); + + private final String message; + + /** + * Constructs a new {@code Reason} with the specified message. + * + * @param message the message describing the reason. + */ + Reason(String message) { + this.message = message; + } + + /** + * Returns the message associated with this reason. + * + * @return the message describing the reason. + */ + public String getMessage() { + return message; + } + } +} diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java new file mode 100644 index 000000000000..e389a1a7dc9e --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Google Cloud Platform (GCP) OpenTelemetry (OTLP) authentication extension. */ +package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth; diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider new file mode 100644 index 000000000000..bf1ba2cad985 --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java new file mode 100644 index 000000000000..e1db55c3635b --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java @@ -0,0 +1,1234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth; + +import static com.google.common.truth.Truth.assertThat; +import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY; +import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER; +import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_ALL; +import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_METRICS; +import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_TRACES; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.context.Scope; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +/** + * Copied from open-telemetry. Link: + * https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java + */ +class GcpAuthAutoConfigurationCustomizerProviderTest { + + private static final String DUMMY_GCP_RESOURCE_PROJECT_ID = "my-gcp-resource-project-id"; + private static final String DUMMY_GCP_QUOTA_PROJECT_ID = "my-gcp-quota-project-id"; + private static final Random TEST_RANDOM = new Random(); + + @Mock private GoogleCredentials mockedGoogleCredentials; + + @Captor private ArgumentCaptor>> traceHeaderSupplierCaptor; + @Captor private ArgumentCaptor>> metricHeaderSupplierCaptor; + + private static final ImmutableMap DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER = + ImmutableMap.builder() + .put("otel.exporter.otlp.traces.endpoint", "https://telemetry.googleapis.com/v1/traces") + .put("otel.traces.exporter", "otlp") + .put("otel.metrics.exporter", "none") + .put("otel.logs.exporter", "none") + .put("otel.resource.attributes", "foo=bar") + .build(); + + private static final ImmutableMap DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER = + ImmutableMap.builder() + .put("otel.exporter.otlp.metrics.endpoint", "https://telemetry.googleapis.com/v1/metrics") + .put("otel.traces.exporter", "none") + .put("otel.metrics.exporter", "otlp") + .put("otel.logs.exporter", "none") + .put("otel.resource.attributes", "foo=bar") + .build(); + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void teardown() { + System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty()); + System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty()); + System.clearProperty(ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty()); + } + + // TODO: Use parameterized test for testing traces customizer for http & grpc. + @Test + void testTraceCustomizerOtlpHttp() { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES); + // Prepare mocks + prepareMockBehaviorForGoogleCredentials(); + OtlpHttpSpanExporter mockOtlpHttpSpanExporter = mock(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporterBuilder otlpSpanExporterBuilder = OtlpHttpSpanExporter.builder(); + OtlpHttpSpanExporterBuilder spyOtlpHttpSpanExporterBuilder = + Mockito.spy(otlpSpanExporterBuilder); + when(spyOtlpHttpSpanExporterBuilder.build()).thenReturn(mockOtlpHttpSpanExporter); + + when(mockOtlpHttpSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + List exportedSpans = new ArrayList<>(); + when(mockOtlpHttpSpanExporter.export(any())) + .thenAnswer( + invocationOnMock -> { + exportedSpans.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + Mockito.when(mockOtlpHttpSpanExporter.toBuilder()).thenReturn(spyOtlpHttpSpanExporterBuilder); + + // begin assertions + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpHttpSpanExporter); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + + Mockito.verify(mockOtlpHttpSpanExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpHttpSpanExporterBuilder, Mockito.times(1)) + .setHeaders(traceHeaderSupplierCaptor.capture()); + assertThat(traceHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2); + assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get())) + .isTrue(); + + Mockito.verify(mockOtlpHttpSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); + + assertThat(exportedSpans).isNotEmpty(); + for (SpanData spanData : exportedSpans) { + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), DUMMY_GCP_RESOURCE_PROJECT_ID); + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(spanData.getAttributes().asMap()).containsKey(AttributeKey.longKey("work_loop")); + } + } + } + + @Test + void testTraceCustomizerOtlpGrpc() { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES); + // Prepare mocks + prepareMockBehaviorForGoogleCredentials(); + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(OtlpGrpcSpanExporter.builder()); + List exportedSpans = new ArrayList<>(); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + + // begin assertions + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + + Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) + .setHeaders(traceHeaderSupplierCaptor.capture()); + assertThat(traceHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2); + assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get())) + .isTrue(); + + Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); + + assertThat(exportedSpans).isNotEmpty(); + for (SpanData spanData : exportedSpans) { + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), DUMMY_GCP_RESOURCE_PROJECT_ID); + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(spanData.getAttributes().asMap()).containsKey(AttributeKey.longKey("work_loop")); + } + } + } + + @Test + void testMetricCustomizerOtlpHttp() { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), + SIGNAL_TYPE_METRICS); + // Prepare mocks + prepareMockBehaviorForGoogleCredentials(); + OtlpHttpMetricExporter mockOtlpHttpMetricExporter = Mockito.mock(OtlpHttpMetricExporter.class); + OtlpHttpMetricExporterBuilder otlpMetricExporterBuilder = OtlpHttpMetricExporter.builder(); + OtlpHttpMetricExporterBuilder spyOtlpHttpMetricExporterBuilder = + Mockito.spy(otlpMetricExporterBuilder); + List exportedMetrics = new ArrayList<>(); + configureHttpMockMetricExporter( + mockOtlpHttpMetricExporter, spyOtlpHttpMetricExporterBuilder, exportedMetrics); + + // begin assertions + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpHttpMetricExporter); + generateTestMetric(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + + Mockito.verify(mockOtlpHttpMetricExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpHttpMetricExporterBuilder, Mockito.times(1)) + .setHeaders(metricHeaderSupplierCaptor.capture()); + assertThat(metricHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2); + assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get())) + .isTrue(); + + Mockito.verify(mockOtlpHttpMetricExporter, Mockito.atLeast(1)) + .export(Mockito.anyCollection()); + + assertThat(exportedMetrics).isNotEmpty(); + for (MetricData metricData : exportedMetrics) { + assertThat(metricData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), DUMMY_GCP_RESOURCE_PROJECT_ID); + assertThat(metricData.getResource().getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(metricData.getLongSumData().getPoints()).isNotEmpty(); + for (io.opentelemetry.sdk.metrics.data.PointData pointData : + metricData.getLongSumData().getPoints()) { + assertThat(pointData.getAttributes().asMap()) + .containsKey(AttributeKey.longKey("work_loop")); + } + } + } + } + + @Test + void testMetricCustomizerOtlpGrpc() { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), + SIGNAL_TYPE_METRICS); + // Prepare mocks + prepareMockBehaviorForGoogleCredentials(); + OtlpGrpcMetricExporter mockOtlpGrpcMetricExporter = Mockito.mock(OtlpGrpcMetricExporter.class); + OtlpGrpcMetricExporterBuilder otlpMetricExporterBuilder = OtlpGrpcMetricExporter.builder(); + OtlpGrpcMetricExporterBuilder spyOtlpGrpcMetricExporterBuilder = + Mockito.spy(otlpMetricExporterBuilder); + List exportedMetrics = new ArrayList<>(); + configureGrpcMockMetricExporter( + mockOtlpGrpcMetricExporter, spyOtlpGrpcMetricExporterBuilder, exportedMetrics); + + // begin assertions + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcMetricExporter); + generateTestMetric(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + + Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpGrpcMetricExporterBuilder, Mockito.times(1)) + .setHeaders(metricHeaderSupplierCaptor.capture()); + assertThat(metricHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2); + assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get())) + .isTrue(); + + Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.atLeast(1)) + .export(Mockito.anyCollection()); + + assertThat(exportedMetrics).isNotEmpty(); + for (MetricData metricData : exportedMetrics) { + assertThat(metricData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), DUMMY_GCP_RESOURCE_PROJECT_ID); + assertThat(metricData.getResource().getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(metricData.getLongSumData().getPoints()).isNotEmpty(); + for (io.opentelemetry.sdk.metrics.data.PointData pointData : + metricData.getLongSumData().getPoints()) { + assertThat(pointData.getAttributes().asMap()) + .containsKey(AttributeKey.longKey("work_loop")); + } + } + } + } + + @Test + void testCustomizerFailWithMissingResourceProject() { + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_ALL); + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + assertThrows( + ConfigurationException.class, + () -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter)); + } + } + + @ParameterizedTest + @MethodSource("provideQuotaBehaviorTestCases") + @SuppressWarnings("CannotMockMethod") + void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws IOException { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_ALL); + + // Prepare request metadata + AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now())); + ImmutableMap> mockedRequestMetadata; + if (testCase.getIsQuotaProjectPresentInMetadata()) { + mockedRequestMetadata = + ImmutableMap.of( + "Authorization", + Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue()), + QUOTA_USER_PROJECT_HEADER, + Collections.singletonList(DUMMY_GCP_QUOTA_PROJECT_ID)); + } else { + mockedRequestMetadata = + ImmutableMap.of( + "Authorization", + Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue())); + } + // mock credentials to return the prepared request metadata + Mockito.when(mockedGoogleCredentials.getRequestMetadata()).thenReturn(mockedRequestMetadata); + + // configure environment according to test case + String quotaProjectId = testCase.getUserSpecifiedQuotaProjectId(); // maybe empty string + if (quotaProjectId != null) { + // user specified a quota project id + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty(), quotaProjectId); + } + + // prepare mock exporter + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(OtlpGrpcSpanExporter.builder()); + List exportedSpans = new ArrayList<>(); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + // Export telemetry to capture headers in the export calls + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) + .setHeaders(traceHeaderSupplierCaptor.capture()); + + // assert that the Authorization bearer token header is present + Map exportHeaders = traceHeaderSupplierCaptor.getValue().get(); + assertThat(exportHeaders).containsEntry("Authorization", "Bearer fake"); + + if (testCase.getExpectedQuotaProjectInHeader() == null) { + // there should be no user quota project header + assertThat(exportHeaders).doesNotContainKey(QUOTA_USER_PROJECT_HEADER); + } else { + // there should be user quota project header with expected value + assertThat(exportHeaders) + .containsEntry(QUOTA_USER_PROJECT_HEADER, testCase.getExpectedQuotaProjectInHeader()); + } + } + } + + @ParameterizedTest + @MethodSource("provideProjectIdBehaviorTestCases") + @SuppressWarnings("CannotMockMethod") + void testProjectIdBehavior(ProjectIdTestBehavior testCase) throws IOException { + System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty()); + System.clearProperty(ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty()); + + // configure environment according to test case + String userSpecifiedProjectId = testCase.getUserSpecifiedProjectId(); + if (userSpecifiedProjectId != null) { + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), userSpecifiedProjectId); + } + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES); + + // prepare request metadata (may or may not be called depending on test scenario) + AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now())); + ImmutableMap> mockedRequestMetadata = + ImmutableMap.of( + "Authorization", + Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue())); + Mockito.lenient() + .when(mockedGoogleCredentials.getRequestMetadata()) + .thenReturn(mockedRequestMetadata); + + // only mock getProjectId() if it will be called (i.e., user didn't specify project ID) + boolean shouldFallbackToCredentials = + userSpecifiedProjectId == null || userSpecifiedProjectId.isEmpty(); + if (shouldFallbackToCredentials) { + Mockito.when(mockedGoogleCredentials.getProjectId()) + .thenReturn(testCase.getCredentialsProjectId()); + } + + // prepare mock exporter + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(OtlpGrpcSpanExporter.builder()); + List exportedSpans = new ArrayList<>(); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + if (testCase.getExpectedToThrow()) { + // expect exception to be thrown when project ID is not available + assertThrows( + ConfigurationException.class, + () -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter)); + // verify getProjectId() was called to attempt fallback + Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId(); + } else { + // export telemetry and verify resource attributes contain expected project ID + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + + assertThat(exportedSpans).isNotEmpty(); + for (SpanData spanData : exportedSpans) { + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), + testCase.getExpectedProjectIdInResource()); + } + + // verify whether getProjectId() was called based on whether fallback was needed + if (shouldFallbackToCredentials) { + Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId(); + } else { + Mockito.verify(mockedGoogleCredentials, Mockito.never()).getProjectId(); + } + } + } + } + + @ParameterizedTest + @MethodSource("provideTargetSignalBehaviorTestCases") + void testTargetSignalsBehavior(TargetSignalBehavior testCase) { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + // Prepare mocks + // Prepare mocked credential + prepareMockBehaviorForGoogleCredentials(); + + // Prepare mocked span exporter + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(OtlpGrpcSpanExporter.builder()); + List exportedSpans = new ArrayList<>(); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + + // Prepare mocked metrics exporter + OtlpGrpcMetricExporter mockOtlpGrpcMetricExporter = Mockito.mock(OtlpGrpcMetricExporter.class); + OtlpGrpcMetricExporterBuilder otlpMetricExporterBuilder = OtlpGrpcMetricExporter.builder(); + OtlpGrpcMetricExporterBuilder spyOtlpGrpcMetricExporterBuilder = + Mockito.spy(otlpMetricExporterBuilder); + List exportedMetrics = new ArrayList<>(); + configureGrpcMockMetricExporter( + mockOtlpGrpcMetricExporter, spyOtlpGrpcMetricExporterBuilder, exportedMetrics); + + // configure environment according to test case + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), + testCase.getConfiguredTargetSignals()); + + // Build Autoconfigured OpenTelemetry SDK using the mocks and send signals + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = + buildOpenTelemetrySdkWithExporter( + mockOtlpGrpcSpanExporter, + mockOtlpGrpcMetricExporter, + testCase.getUserSpecifiedOtelProperties()); + generateTestMetric(sdk); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertThat(joinResult.isSuccess()).isTrue(); + + // Check Traces modification conditions + if (testCase.getExpectedIsTraceSignalModified()) { + // If traces signal is expected to be modified, auth headers must be present + Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) + .setHeaders(traceHeaderSupplierCaptor.capture()); + assertThat(traceHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2); + assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get())) + .isTrue(); + } else { + // If traces signals is not expected to be modified then no interaction with the builder + // should be made + Mockito.verifyNoInteractions(spyOtlpGrpcSpanExporterBuilder); + } + + // Check Metric modification conditions + if (testCase.getExpectedIsMetricsSignalModified()) { + // If metrics signal is expected to be modified, auth headers must be present + Mockito.verify(spyOtlpGrpcMetricExporterBuilder, Mockito.times(1)) + .setHeaders(metricHeaderSupplierCaptor.capture()); + assertThat(metricHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2); + assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get())) + .isTrue(); + } else { + // If metrics signals is not expected to be modified then no interaction with the builder + // should be made + Mockito.verifyNoInteractions(spyOtlpGrpcMetricExporterBuilder); + } + } + } + + /** Test cases specifying expected behavior for GOOGLE_OTEL_AUTH_TARGET_SIGNALS. */ + private static Stream provideTargetSignalBehaviorTestCases() { + return Stream.of( + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("traces") + .setUserSpecifiedOtelProperties(DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics") + .setUserSpecifiedOtelProperties(DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all") + .setUserSpecifiedOtelProperties( + ImmutableMap.builder() + .put( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics") + .put( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces") + .put("otel.traces.exporter", "otlp") + .put("otel.metrics.exporter", "otlp") + .put("otel.logs.exporter", "none") + .build()) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, traces") + .setUserSpecifiedOtelProperties( + ImmutableMap.builder() + .put( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics") + .put( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces") + .put("otel.traces.exporter", "otlp") + .put("otel.metrics.exporter", "otlp") + .put("otel.logs.exporter", "none") + .build()) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("") + .setUserSpecifiedOtelProperties( + ImmutableMap.builder() + .put( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics") + .put( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces") + .put("otel.traces.exporter", "otlp") + .put("otel.metrics.exporter", "otlp") + .put("otel.logs.exporter", "none") + .build()) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces", + "otel.traces.exporter", + "none", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metric, trace") + .setUserSpecifiedOtelProperties( + ImmutableMap.builder() + .put( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics") + .put( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces") + .put("otel.traces.exporter", "otlp") + .put("otel.metrics.exporter", "otlp") + .put("otel.logs.exporter", "none") + .build()) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, trace") + .setUserSpecifiedOtelProperties( + ImmutableMap.builder() + .put( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics") + .put( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces") + .put("otel.traces.exporter", "otlp") + .put("otel.metrics.exporter", "otlp") + .put("otel.logs.exporter", "none") + .build()) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(false) + .build())); + } + + /** + * Test cases specifying expected value for the project ID in the resource given the user input + * and the current credentials state. + * + *

    {@code null} for {@link ProjectIdTestBehavior#getUserSpecifiedProjectId()} indicates the + * case of user not specifying the project ID. + * + *

    {@code null} value for {@link ProjectIdTestBehavior#getCredentialsProjectId()} indicates + * that the mocked credentials are not providing a project ID. + * + *

    {@code true} for {@link ProjectIdTestBehavior#getExpectedToThrow()} indicates the + * expectation that an exception should be thrown. + */ + private static Stream provideProjectIdBehaviorTestCases() { + return Stream.of( + // User specified project ID takes precedence + Arguments.of( + ProjectIdTestBehavior.builder() + .setUserSpecifiedProjectId(DUMMY_GCP_RESOURCE_PROJECT_ID) + .setCredentialsProjectId("credentials-project-id") + .setExpectedProjectIdInResource(DUMMY_GCP_RESOURCE_PROJECT_ID) + .setExpectedToThrow(false) + .build()), + // If user specified project ID is empty, fallback to credentials.getProjectId() + Arguments.of( + ProjectIdTestBehavior.builder() + .setUserSpecifiedProjectId("") + .setCredentialsProjectId("credentials-project-id") + .setExpectedProjectIdInResource("credentials-project-id") + .setExpectedToThrow(false) + .build()), + // If user doesn't specify project ID, fallback to credentials.getProjectId() + Arguments.of( + ProjectIdTestBehavior.builder() + .setUserSpecifiedProjectId(null) + .setCredentialsProjectId("credentials-project-id") + .setExpectedProjectIdInResource("credentials-project-id") + .setExpectedToThrow(false) + .build()), + // If user doesn't specify and credentials.getProjectId() returns null, throw exception + Arguments.of( + ProjectIdTestBehavior.builder() + .setUserSpecifiedProjectId(null) + .setCredentialsProjectId(null) + .setExpectedProjectIdInResource(null) + .setExpectedToThrow(true) + .build()), + // If user specified project ID is empty and credentials.getProjectId() returns null, throw + // exception + Arguments.of( + ProjectIdTestBehavior.builder() + .setUserSpecifiedProjectId("") + .setCredentialsProjectId(null) + .setExpectedProjectIdInResource(null) + .setExpectedToThrow(true) + .build()), + // If user specifies empty and credentials returns empty (edge case), throw exception + Arguments.of( + ProjectIdTestBehavior.builder() + .setUserSpecifiedProjectId("") + .setCredentialsProjectId("") + .setExpectedProjectIdInResource(null) + .setExpectedToThrow(true) + .build())); + } + + /** + * Test cases specifying expected value for the user quota project header given the user input and + * the current credentials state. + * + *

    {@code null} for {@link QuotaProjectIdTestBehavior#getUserSpecifiedQuotaProjectId()} + * indicates the case of user not specifying the quota project ID. + * + *

    {@code null} value for {@link QuotaProjectIdTestBehavior#getExpectedQuotaProjectInHeader()} + * indicates the expectation that the QUOTA_USER_PROJECT_HEADER should not be present in the + * export headers. + * + *

    {@code true} for {@link QuotaProjectIdTestBehavior#getIsQuotaProjectPresentInMetadata()} + * indicates that the mocked credentials are configured to provide DUMMY_GCP_QUOTA_PROJECT_ID as + * the quota project ID. + */ + private static Stream provideQuotaBehaviorTestCases() { + return Stream.of( + // If quota project present in metadata, it will be used + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID) + .setIsQuotaProjectPresentInMetadata(true) + .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID) + .build()), + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId("my-custom-quota-project-id") + .setIsQuotaProjectPresentInMetadata(true) + .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID) + .build()), + // If quota project not present in request metadata, then user specified project is used + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID) + .setIsQuotaProjectPresentInMetadata(false) + .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID) + .build()), + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId("my-custom-quota-project-id") + .setIsQuotaProjectPresentInMetadata(false) + .setExpectedQuotaProjectInHeader("my-custom-quota-project-id") + .build()), + // Testing for special edge case inputs + // user-specified quota project is empty + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId("") // user explicitly specifies empty + .setIsQuotaProjectPresentInMetadata(true) + .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID) + .build()), + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId("") + .setIsQuotaProjectPresentInMetadata(false) + .setExpectedQuotaProjectInHeader(null) + .build()), + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId(null) // user omits specifying quota project + .setIsQuotaProjectPresentInMetadata(true) + .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID) + .build()), + Arguments.of( + QuotaProjectIdTestBehavior.builder() + .setUserSpecifiedQuotaProjectId(null) + .setIsQuotaProjectPresentInMetadata(false) + .setExpectedQuotaProjectInHeader(null) + .build())); + } + + // Configure necessary behavior on the gRPC mock span exporters to work. + // Mockito.lenient is used here because this method is used with parameterized tests where based + // on certain inputs, certain stubbings may not be required. + private static void configureGrpcMockSpanExporter( + OtlpGrpcSpanExporter mockGrpcExporter, + OtlpGrpcSpanExporterBuilder spyGrpcExporterBuilder, + List exportedSpanContainer) { + Mockito.lenient().when(spyGrpcExporterBuilder.build()).thenReturn(mockGrpcExporter); + Mockito.lenient() + .when(mockGrpcExporter.shutdown()) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.lenient().when(mockGrpcExporter.toBuilder()).thenReturn(spyGrpcExporterBuilder); + Mockito.lenient() + .when(mockGrpcExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedSpanContainer.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + } + + // Configure necessary behavior on the http mock metric exporters to work. + private static void configureHttpMockMetricExporter( + OtlpHttpMetricExporter mockOtlpHttpMetricExporter, + OtlpHttpMetricExporterBuilder spyOtlpHttpMetricExporterBuilder, + List exportedMetricContainer) { + Mockito.when(spyOtlpHttpMetricExporterBuilder.build()).thenReturn(mockOtlpHttpMetricExporter); + Mockito.when(mockOtlpHttpMetricExporter.shutdown()) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(mockOtlpHttpMetricExporter.toBuilder()) + .thenReturn(spyOtlpHttpMetricExporterBuilder); + Mockito.when(mockOtlpHttpMetricExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedMetricContainer.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + // mock the get default aggregation and aggregation temporality - they're required for valid + // metric collection. + Mockito.when(mockOtlpHttpMetricExporter.getDefaultAggregation(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpHttpMetricExporter.getDefault().getDefaultAggregation(instrumentType); + }); + Mockito.when(mockOtlpHttpMetricExporter.getAggregationTemporality(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpHttpMetricExporter.getDefault() + .getAggregationTemporality(instrumentType); + }); + } + + // Configure necessary behavior on the gRPC mock metrics exporters to work. + // Mockito.lenient is used here because this method is used with parameterized tests where based + // on certain inputs, certain stubbings may not be required. + private static void configureGrpcMockMetricExporter( + OtlpGrpcMetricExporter mockOtlpGrpcMetricExporter, + OtlpGrpcMetricExporterBuilder spyOtlpGrpcMetricExporterBuilder, + List exportedMetricContainer) { + Mockito.lenient() + .when(spyOtlpGrpcMetricExporterBuilder.build()) + .thenReturn(mockOtlpGrpcMetricExporter); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.shutdown()) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.toBuilder()) + .thenReturn(spyOtlpGrpcMetricExporterBuilder); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedMetricContainer.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + // mock the get default aggregation and aggregation temporality - they're required for valid + // metric collection. + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.getDefaultAggregation(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpGrpcMetricExporter.getDefault().getDefaultAggregation(instrumentType); + }); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.getAggregationTemporality(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpGrpcMetricExporter.getDefault() + .getAggregationTemporality(instrumentType); + }); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.getMemoryMode()) + .thenReturn(MemoryMode.IMMUTABLE_DATA); + } + + @AutoValue + abstract static class ProjectIdTestBehavior { + // A null user specified project ID represents the use case where user omits specifying it + @Nullable + abstract String getUserSpecifiedProjectId(); + + // The project ID that credentials.getProjectId() returns (can be null) + @Nullable + abstract String getCredentialsProjectId(); + + // The expected project ID in the resource attributes (null if exception expected) + @Nullable + abstract String getExpectedProjectIdInResource(); + + // Whether an exception is expected to be thrown + abstract boolean getExpectedToThrow(); + + static Builder builder() { + return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_ProjectIdTestBehavior + .Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setUserSpecifiedProjectId(String projectId); + + abstract Builder setCredentialsProjectId(String projectId); + + abstract Builder setExpectedProjectIdInResource(String projectId); + + abstract Builder setExpectedToThrow(boolean expectedToThrow); + + abstract ProjectIdTestBehavior build(); + } + } + + @AutoValue + abstract static class QuotaProjectIdTestBehavior { + // A null user specified quota represents the use case where user omits specifying quota + @Nullable + abstract String getUserSpecifiedQuotaProjectId(); + + abstract boolean getIsQuotaProjectPresentInMetadata(); + + // If expected quota project in header is null, the header entry should not be present in export + @Nullable + abstract String getExpectedQuotaProjectInHeader(); + + static Builder builder() { + return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_QuotaProjectIdTestBehavior + .Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setUserSpecifiedQuotaProjectId(String quotaProjectId); + + abstract Builder setIsQuotaProjectPresentInMetadata(boolean quotaProjectPresentInMetadata); + + /** + * Sets the expected quota project header value for the test case. A null value is allowed, + * and it indicates that the header should not be present in the export request. + * + * @param expectedQuotaProjectInHeader the expected header value to match in the export + * headers. + */ + abstract Builder setExpectedQuotaProjectInHeader(String expectedQuotaProjectInHeader); + + abstract QuotaProjectIdTestBehavior build(); + } + } + + @AutoValue + abstract static class TargetSignalBehavior { + @Nonnull + abstract String getConfiguredTargetSignals(); + + @Nonnull + abstract ImmutableMap getUserSpecifiedOtelProperties(); + + abstract boolean getExpectedIsTraceSignalModified(); + + abstract boolean getExpectedIsMetricsSignalModified(); + + static Builder builder() { + return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_TargetSignalBehavior + .Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setConfiguredTargetSignals(String targetSignals); + + abstract Builder setUserSpecifiedOtelProperties(Map oTelProperties); + + // Set whether the combination of specified OTel properties and configured target signals + // should lead to modification of the OTLP trace exporters. + abstract Builder setExpectedIsTraceSignalModified(boolean expectedModified); + + // Set whether the combination of specified OTel properties and configured target signals + // should lead to modification of the OTLP metrics exporters. + abstract Builder setExpectedIsMetricsSignalModified(boolean expectedModified); + + abstract TargetSignalBehavior build(); + } + } + + // Mockito.lenient is used here because this method is used with parameterized tests where based + @SuppressWarnings("CannotMockMethod") + private void prepareMockBehaviorForGoogleCredentials() { + AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now())); + try { + Mockito.lenient() + .when(mockedGoogleCredentials.getRequestMetadata()) + .thenReturn( + ImmutableMap.of( + "Authorization", + Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue()), + QUOTA_USER_PROJECT_HEADER, + Collections.singletonList(DUMMY_GCP_QUOTA_PROJECT_ID))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) { + return buildOpenTelemetrySdkWithExporter( + spanExporter, OtlpHttpMetricExporter.getDefault(), DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER); + } + + @SuppressWarnings("UnusedMethod") + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( + SpanExporter spanExporter, ImmutableMap customOTelProperties) { + return buildOpenTelemetrySdkWithExporter( + spanExporter, OtlpHttpMetricExporter.getDefault(), customOTelProperties); + } + + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(MetricExporter metricExporter) { + return buildOpenTelemetrySdkWithExporter( + OtlpHttpSpanExporter.getDefault(), metricExporter, DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER); + } + + @SuppressWarnings("UnusedMethod") + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( + MetricExporter metricExporter, ImmutableMap customOtelProperties) { + return buildOpenTelemetrySdkWithExporter( + OtlpHttpSpanExporter.getDefault(), metricExporter, customOtelProperties); + } + + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( + SpanExporter spanExporter, + MetricExporter metricExporter, + ImmutableMap customOtelProperties) { + SpiHelper spiHelper = + SpiHelper.create(GcpAuthAutoConfigurationCustomizerProviderTest.class.getClassLoader()); + AutoConfiguredOpenTelemetrySdkBuilder builder = + AutoConfiguredOpenTelemetrySdk.builder() + .addPropertiesSupplier(() -> customOtelProperties) + .setComponentLoader( + new ComponentLoader() { + @Override + public List load(Class spiClass) { + if (spiClass == ConfigurableSpanExporterProvider.class) { + return Collections.singletonList( + spiClass.cast( + new ConfigurableSpanExporterProvider() { + @Override + public SpanExporter createExporter( + ConfigProperties configProperties) { + return spanExporter; + } + + @Override + public String getName() { + return "otlp"; + } + })); + } + if (spiClass == ConfigurableMetricExporterProvider.class) { + return Collections.singletonList( + spiClass.cast( + new ConfigurableMetricExporterProvider() { + @Override + public MetricExporter createExporter( + ConfigProperties configProperties) { + return metricExporter; + } + + @Override + public String getName() { + return "otlp"; + } + })); + } + return spiHelper.load(spiClass); + } + }); + + return builder.build().getOpenTelemetrySdk(); + } + + private static boolean authHeadersQuotaProjectIsPresent(Map headers) { + Set> headerEntrySet = headers.entrySet(); + return headerEntrySet.contains( + new SimpleEntry<>( + QUOTA_USER_PROJECT_HEADER, + GcpAuthAutoConfigurationCustomizerProviderTest.DUMMY_GCP_QUOTA_PROJECT_ID)) + && headerEntrySet.contains(new SimpleEntry<>("Authorization", "Bearer fake")); + } + + private static void generateTestSpan(OpenTelemetrySdk openTelemetrySdk) { + Span span = openTelemetrySdk.getTracer("test").spanBuilder("sample").startSpan(); + try (Scope ignored = span.makeCurrent()) { + long workOutput = busyloop(); + span.setAttribute("work_loop", workOutput); + } finally { + span.end(); + } + } + + private static void generateTestMetric(OpenTelemetrySdk openTelemetrySdk) { + LongCounter longCounter = + openTelemetrySdk + .getMeter("test") + .counterBuilder("sample") + .setDescription("sample counter") + .setUnit("1") + .build(); + long workOutput = busyloop(); + long randomValue = TEST_RANDOM.nextInt(1000); + longCounter.add(randomValue, Attributes.of(AttributeKey.longKey("work_loop"), workOutput)); + } + + // loop to simulate work done + private static long busyloop() { + Instant start = Instant.now(); + Instant end; + long counter = 0; + do { + counter++; + end = Instant.now(); + } while (Duration.between(start, end).toMillis() < 1000); + return counter; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 603832045f3e..443d9c567752 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -192,6 +192,7 @@ include(":sdks:java:extensions:avro") include(":sdks:java:extensions:euphoria") include(":sdks:java:extensions:kryo") include(":sdks:java:extensions:google-cloud-platform-core") +include(":sdks:java:extensions:opentelemetry-gcp-auth-extension") include(":sdks:java:extensions:jackson") include(":sdks:java:extensions:join-library") include(":sdks:java:extensions:kafka-factories") From e87013e6c1891e76cc7fa5062a0ec95b9b164850 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 5 Jun 2026 17:30:00 +0200 Subject: [PATCH 314/490] =?UTF-8?q?Revert=20"remove=20playground=20annotat?= =?UTF-8?q?ions=20from=20BatchElementsExample=20until=20next=20re=E2=80=A6?= =?UTF-8?q?"=20(#38830)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3f4bcb015708078d55cba3eba30e09bb9fde9c72. --- .../apache/beam/examples/BatchElementsExample.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java index 0103999d9256..79130d7ac138 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/BatchElementsExample.java @@ -29,6 +29,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +// beam-playground: +// name: BatchElements +// description: Demonstration of BatchElements transform usage. +// multifile: false +// default_example: false +// context_line: 47 +// categories: +// - Core Transforms +// complexity: BASIC +// tags: +// - transforms +// - batch + public class BatchElementsExample { public static void main(String[] args) { PipelineOptions options = PipelineOptionsFactory.create(); From bf59744354a1dcd04c3bab2f709c5f9b3a0dffac Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Fri, 5 Jun 2026 19:58:09 +0200 Subject: [PATCH 315/490] Automate OpenTelemetry license entries in bomupgrader (#38815) * Automate OpenTelemetry license entries in bomupgrader * Simplify OpenTelemetry license updates in bomupgrader --- scripts/tools/bomupgrader.py | 53 +++++++++++++++---- .../license_scripts/dep_urls_java.yaml | 6 --- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/scripts/tools/bomupgrader.py b/scripts/tools/bomupgrader.py index bf6e7dfdd31f..4697c46a5568 100644 --- a/scripts/tools/bomupgrader.py +++ b/scripts/tools/bomupgrader.py @@ -27,7 +27,8 @@ 1. preprocessing BeamModulePlugin.groovy to decide the dependencies need to sync 2. generate an empty Maven project to fetch the exact target versions to change 3. Write back to BeamModulePlugin.groovy -4. Update libraries-bom version on sdks/java/container/license_scripts/dep_urls_java.yaml +4. Update libraries-bom and opentelemetry-bom entries on + sdks/java/container/license_scripts/dep_urls_java.yaml There are few reasons we need to declare the version numbers: 1. Sync the dependency that not included in GCP-BOM with those included with BOM @@ -254,17 +255,51 @@ def write_back(self): for line in self.target_lines: fout.write(line) + def _get_opentelemetry_version(self): + for line in self.target_lines: + match = self.VERSION_STRING.match(line) + if match and match.group(1) == 'opentelemetry': + return match.group(2) + logging.warning('opentelemetry_version not found in BeamModulePlugin') + return None + def write_license_script(self): logging.info("-----Update dep_urls_java.yaml-----") - with open(os.path.join(self.project_root, self.LICENSE_SC_PATH), - 'r') as fin: + license_path = os.path.join(self.project_root, self.LICENSE_SC_PATH) + with open(license_path, 'r') as fin: lines = fin.readlines() - with open(os.path.join(self.project_root, self.LICENSE_SC_PATH), - 'w') as fout: - for idx, line in enumerate(lines): - if line.strip() == 'libraries-bom:': - lines[idx + 1] = re.sub( - r'[\'"]\d[\.\d]+[\'"]', f"'{self.bom_version}'", lines[idx + 1]) + + otel_version = self._get_opentelemetry_version() + otel_license_url = ( + 'https://raw.githubusercontent.com/open-telemetry/' + 'opentelemetry-java/v{}/LICENSE'.format(otel_version) + if otel_version else None) + otel_license_line = ' license: "{}"\n'.format(otel_license_url) + + for idx, line in enumerate(lines): + stripped = line.strip() + if stripped == 'libraries-bom:': + lines[idx + 1] = re.sub( + r'[\'"]\d[\d\.]+[\'"]', "'{}'".format(self.bom_version), + lines[idx + 1]) + continue + if otel_version and stripped == 'opentelemetry-bom:': + lines[idx + 1] = re.sub( + r"'[\d\.]+'", "'{}'".format(otel_version), lines[idx + 1]) + lines[idx + 2] = otel_license_line + continue + if otel_version and stripped == 'opentelemetry-bom-alpha:': + lines[idx + 1] = re.sub( + r"'[\d\.]+-alpha'", "'{}-alpha'".format(otel_version), + lines[idx + 1]) + lines[idx + 2] = otel_license_line + + if otel_version: + logging.info( + 'Updated opentelemetry-bom license entries to %s', otel_version) + + with open(license_path, 'w') as fout: + for line in lines: fout.write(line) def run(self): diff --git a/sdks/java/container/license_scripts/dep_urls_java.yaml b/sdks/java/container/license_scripts/dep_urls_java.yaml index 9f80b8c81fcf..589f7ee2d125 100644 --- a/sdks/java/container/license_scripts/dep_urls_java.yaml +++ b/sdks/java/container/license_scripts/dep_urls_java.yaml @@ -66,16 +66,10 @@ org.eclipse.jgit: license: "https://www.eclipse.org/org/documents/edl-v10.html" type: "Eclipse Distribution License - v1.0" opentelemetry-bom: - '1.56.0': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" - type: "Apache License 2.0" '1.57.0': license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.57.0/LICENSE" type: "Apache License 2.0" opentelemetry-bom-alpha: - '1.56.0-alpha': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" - type: "Apache License 2.0" '1.57.0-alpha': license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.57.0/LICENSE" type: "Apache License 2.0" From b30fcb3b1d025b022aeb8aeaab0fb14a538f1310 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Mon, 8 Jun 2026 12:20:20 +0200 Subject: [PATCH 316/490] support generics in from row and to row conversions (#37347) --- .../schemas/FieldValueTypeInformation.java | 13 +- .../beam/sdk/schemas/FromRowUsingCreator.java | 6 +- .../schemas/GetterBasedSchemaProvider.java | 98 +++- .../sdk/schemas/utils/AutoValueUtils.java | 9 +- .../java/org/apache/beam/sdk/values/Row.java | 6 +- .../beam/sdk/values/RowWithGetters.java | 13 +- .../beam/sdk/schemas/AutoValueSchemaTest.java | 426 +++++++++++++++++- .../beam/sdk/schemas/JavaBeanSchemaTest.java | 167 ++++++- .../beam/sdk/schemas/JavaFieldSchemaTest.java | 196 +++++++- .../beam/sdk/schemas/utils/TestJavaBeans.java | 35 ++ .../beam/sdk/schemas/utils/TestPOJOs.java | 25 + .../sdk/extensions/arrow/ArrowConversion.java | 4 +- .../io/aws2/schemas/AwsSchemaProvider.java | 7 +- .../beam/sdk/io/aws2/schemas/AwsTypes.java | 16 + 14 files changed, 952 insertions(+), 69 deletions(-) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java index 43aac6a5e20c..95030eda0988 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java @@ -105,7 +105,11 @@ public abstract static class Builder { public abstract Builder setDescription(@Nullable String fieldDescription); - abstract FieldValueTypeInformation build(); + public abstract FieldValueTypeInformation build(); + } + + public static Builder builder() { + return new AutoValue_FieldValueTypeInformation.Builder(); } public static FieldValueTypeInformation forOneOf( @@ -311,7 +315,8 @@ public FieldValueTypeInformation withName(String name) { return toBuilder().setName(name).build(); } - static @Nullable FieldValueTypeInformation getIterableComponentType(TypeDescriptor valueType) { + public static @Nullable FieldValueTypeInformation getIterableComponentType( + TypeDescriptor valueType) { // TODO: Figure out nullable elements. TypeDescriptor componentType = ReflectUtils.getIterableComponentType(valueType); if (componentType == null) { @@ -331,13 +336,13 @@ public FieldValueTypeInformation withName(String name) { } // If the type is a map type, returns the key type, otherwise returns a null reference. - private static @Nullable FieldValueTypeInformation getMapKeyType( + public static @Nullable FieldValueTypeInformation getMapKeyType( TypeDescriptor typeDescriptor) { return getMapType(typeDescriptor, 0); } // If the type is a map type, returns the value type, otherwise returns a null reference. - private static @Nullable FieldValueTypeInformation getMapValueType( + public static @Nullable FieldValueTypeInformation getMapValueType( TypeDescriptor typeDescriptor) { return getMapType(typeDescriptor, 1); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java index 464dc00cec7d..0f5c47f72591 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java @@ -82,10 +82,10 @@ public T apply(Row row) { return null; } if (row instanceof RowWithGetters) { - Object target = ((RowWithGetters) row).getGetterTarget(); - if (target.getClass().equals(typeDescriptor.getRawType())) { + RowWithGetters rowWithGetters = (RowWithGetters) row; + if (rowWithGetters.getGetterTargetType().equals(typeDescriptor)) { // Efficient path: simply extract the underlying object instead of creating a new one. - return (T) target; + return (T) rowWithGetters.getGetterTarget(); } } if (fieldConverters == null) { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java index e08f193d4072..7fb9e5a5dee5 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java @@ -20,16 +20,20 @@ import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.Schema.LogicalType; import org.apache.beam.sdk.schemas.Schema.TypeName; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; import org.apache.beam.sdk.schemas.logicaltypes.OneOfType; +import org.apache.beam.sdk.schemas.utils.ReflectUtils; import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TypeDescriptor; @@ -117,9 +121,11 @@ private class ToRowWithValueGetters implements SerializableFunction { private final Schema schema; private final Factory>> getterFactory; + private final TypeDescriptor getterTargetType; - public ToRowWithValueGetters(Schema schema) { + public ToRowWithValueGetters(Schema schema, TypeDescriptor getterTargetType) { this.schema = schema; + this.getterTargetType = getterTargetType; // Since we know that this factory is always called from inside the lambda with the same // schema, return a caching factory that caches the first value seen for each class. This // prevents having to lookup the getter list each time createGetters is called. @@ -128,13 +134,13 @@ public ToRowWithValueGetters(Schema schema) { (Factory>>) (typeDescriptor, schema1) -> (List) - GetterBasedSchemaProvider.this.fieldValueGetters( - typeDescriptor, schema1)); + GetterBasedSchemaProvider.this.fieldValueGetters(typeDescriptor, schema1), + GetterBasedSchemaProvider.this::fieldValueTypeInformations); } @Override public Row apply(T input) { - return Row.withSchema(schema).withFieldValueGetters(getterFactory, input); + return Row.withSchema(schema).withFieldValueGetters(getterFactory, input, getterTargetType); } private GetterBasedSchemaProvider getOuter() { @@ -172,7 +178,7 @@ public SerializableFunction toRowFunction(TypeDescriptor typeDesc Verify.verifyNotNull( schemaFor(typeDescriptor), "can't create a ToRowFunction with null schema"); - return new ToRowWithValueGetters<>(schema); + return new ToRowWithValueGetters<>(schema, typeDescriptor); } @Override @@ -194,16 +200,21 @@ public boolean equals(@Nullable Object obj) { private static class RowValueGettersFactory implements Factory>> { private final Factory>> gettersFactory; + private final Factory> typeInfoFactory; private final @NotOnlyInitialized Factory>> cachingGettersFactory; static Factory>> of( - Factory>> gettersFactory) { - return new RowValueGettersFactory<>(gettersFactory).cachingGettersFactory; + Factory>> gettersFactory, + Factory> typeInfoFactory) { + return new RowValueGettersFactory(gettersFactory, typeInfoFactory).cachingGettersFactory; } - RowValueGettersFactory(Factory>> gettersFactory) { + RowValueGettersFactory( + Factory>> gettersFactory, + Factory> typeInfoFactory) { this.gettersFactory = gettersFactory; + this.typeInfoFactory = typeInfoFactory; this.cachingGettersFactory = new CachingFactory<>(this); } @@ -211,9 +222,17 @@ private static class RowValueGettersFactory public List> create( TypeDescriptor typeDescriptor, Schema schema) { List> getters = gettersFactory.create(typeDescriptor, schema); + Map typeInfoByName = + typeInfoFactory.create(typeDescriptor, schema).stream() + .collect(Collectors.toMap(FieldValueTypeInformation::getName, Function.identity())); List> rowGetters = new ArrayList<>(getters.size()); for (int i = 0; i < getters.size(); i++) { - rowGetters.add(rowValueGetter(getters.get(i), schema.getField(i).getType())); + FieldValueGetter getter = Verify.verifyNotNull(getters.get(i)); + rowGetters.add( + rowValueGetter( + getter, + schema.getField(i).getType(), + Verify.verifyNotNull(typeInfoByName.get(getter.name())).getType())); } return rowGetters; } @@ -229,26 +248,49 @@ && needsConversion(Verify.verifyNotNull(type.getCollectionElementType()))) || needsConversion(Verify.verifyNotNull(type.getMapValueType())))); } - FieldValueGetter rowValueGetter(FieldValueGetter base, FieldType type) { + FieldValueGetter rowValueGetter( + FieldValueGetter base, FieldType type, @Nullable TypeDescriptor getterReturnType) { TypeName typeName = type.getTypeName(); if (!needsConversion(type)) { return base; } if (typeName.equals(TypeName.ROW)) { - return new GetRow(base, Verify.verifyNotNull(type.getRowSchema()), cachingGettersFactory); - } else if (typeName.equals(TypeName.ARRAY)) { + return new GetRow( + base, + getterReturnType, + Verify.verifyNotNull(type.getRowSchema()), + cachingGettersFactory); + } else if (typeName.equals(TypeName.ARRAY) || typeName.equals(TypeName.ITERABLE)) { FieldType elementType = Verify.verifyNotNull(type.getCollectionElementType()); - return elementType.getTypeName().equals(TypeName.ROW) - ? new GetEagerCollection(base, converter(elementType)) - : new GetCollection(base, converter(elementType)); - } else if (typeName.equals(TypeName.ITERABLE)) { - return new GetIterable( - base, converter(Verify.verifyNotNull(type.getCollectionElementType()))); + TypeDescriptor elementTypeDescriptor = + Optional.ofNullable(getterReturnType) + .map(ReflectUtils::getIterableComponentType) + .orElse(null); + if (TypeName.ARRAY == typeName) { + return TypeName.ROW == elementType.getTypeName() + ? new GetEagerCollection(base, converter(elementType, elementTypeDescriptor)) + : new GetCollection(base, converter(elementType, elementTypeDescriptor)); + } else { // TypeName.ITERABLE + return new GetIterable(base, converter(elementType, elementTypeDescriptor)); + } } else if (typeName.equals(TypeName.MAP)) { + @Nullable + TypeDescriptor[] resolvedKeyValueTypes = + Optional.ofNullable(getterReturnType) + .<@Nullable TypeDescriptor[]>map( + getterType -> + Arrays.stream(Map.class.getTypeParameters()) + .<@Nullable TypeDescriptor>map( + typeVar -> { + TypeDescriptor resolved = getterType.resolveType(typeVar); + return resolved.hasUnresolvedParameters() ? null : resolved; + }) + .<@Nullable TypeDescriptor>toArray(TypeDescriptor[]::new)) + .orElse(new TypeDescriptor[] {null, null}); return new GetMap( base, - converter(Verify.verifyNotNull(type.getMapKeyType())), - converter(Verify.verifyNotNull(type.getMapValueType()))); + converter(Verify.verifyNotNull(type.getMapKeyType()), resolvedKeyValueTypes[0]), + converter(Verify.verifyNotNull(type.getMapValueType()), resolvedKeyValueTypes[1])); } else if (type.isLogicalType(OneOfType.IDENTIFIER)) { OneOfType oneOfType = type.getLogicalType(OneOfType.class); Schema oneOfSchema = oneOfType.getOneOfSchema(); @@ -258,7 +300,7 @@ FieldValueGetter rowValueGetter(FieldValueGetter base, FieldType type Maps.newHashMapWithExpectedSize(values.size()); for (Map.Entry kv : values.entrySet()) { FieldType fieldType = oneOfSchema.getField(kv.getKey()).getType(); - FieldValueGetter converter = converter(fieldType); + FieldValueGetter converter = converter(fieldType, null); converters.put(kv.getValue(), converter); } @@ -269,27 +311,35 @@ FieldValueGetter rowValueGetter(FieldValueGetter base, FieldType type return base; } - FieldValueGetter converter(FieldType type) { - return rowValueGetter(IDENTITY, type); + FieldValueGetter converter(FieldType type, @Nullable TypeDescriptor getterReturnType) { + return rowValueGetter(IDENTITY, type, getterReturnType); } static class GetRow extends Converter { final Schema schema; final Factory>> factory; + final @Nullable TypeDescriptor valueType; GetRow( FieldValueGetter getter, + @Nullable TypeDescriptor getterReturnType, Schema schema, Factory>> factory) { super(getter); this.schema = schema; this.factory = factory; + this.valueType = getterReturnType; } @Override Object convert(V value) { - return Row.withSchema(schema).withFieldValueGetters(factory, value); + return Row.withSchema(schema) + .withFieldValueGetters( + factory, + value, + Optional.ofNullable(valueType) + .orElse((TypeDescriptor) TypeDescriptor.of(value.getClass()))); } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java index 78808fdc10c8..a9353bcaaacb 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java @@ -162,7 +162,7 @@ private static String getAutoValueGeneratedName(String baseClass) { Optional> constructor = Arrays.stream(generatedTypeDescriptor.getRawType().getDeclaredConstructors()) .filter(c -> !Modifier.isPrivate(c.getModifiers())) - .filter(c -> matchConstructor(c, schemaTypes)) + .filter(c -> matchConstructor(generatedTypeDescriptor, c, schemaTypes)) .findAny(); return constructor .map( @@ -177,7 +177,9 @@ private static String getAutoValueGeneratedName(String baseClass) { } private static boolean matchConstructor( - Constructor constructor, List getterTypes) { + TypeDescriptor typeDescriptor, + Constructor constructor, + List getterTypes) { if (constructor.getParameters().length != getterTypes.size()) { return false; } @@ -197,7 +199,8 @@ private static boolean matchConstructor( // Verify that constructor parameters match (name and type) the inferred schema. for (Parameter parameter : constructor.getParameters()) { FieldValueTypeInformation type = typeMap.get(parameter.getName()); - if (type == null || type.getRawType() != parameter.getType()) { + if (type == null + || !type.getType().equals(typeDescriptor.resolveType(parameter.getParameterizedType()))) { valid = false; break; } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/Row.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/Row.java index 11d02be46d24..6eb063f84b67 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/Row.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/Row.java @@ -838,9 +838,11 @@ public int nextFieldId() { @Internal public Row withFieldValueGetters( - Factory>> fieldValueGetterFactory, T getterTarget) { + Factory>> fieldValueGetterFactory, + T getterTarget, + TypeDescriptor getterTargetType) { checkState(getterTarget != null, "getters require withGetterTarget."); - return new RowWithGetters<>(schema, fieldValueGetterFactory, getterTarget); + return new RowWithGetters<>(schema, fieldValueGetterFactory, getterTarget, getterTargetType); } public Row build() { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/RowWithGetters.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/RowWithGetters.java index 35e0ac20d3f7..0cbc7bfb992a 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/RowWithGetters.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/RowWithGetters.java @@ -44,14 +44,19 @@ @SuppressWarnings("rawtypes") public class RowWithGetters extends Row { private final T getterTarget; + private final TypeDescriptor getterTargetType; private final List> getters; private @Nullable Map cache = null; RowWithGetters( - Schema schema, Factory>> getterFactory, T getterTarget) { + Schema schema, + Factory>> getterFactory, + T getterTarget, + TypeDescriptor getterTargetType) { super(schema); this.getterTarget = getterTarget; - this.getters = getterFactory.create(TypeDescriptor.of(getterTarget.getClass()), schema); + this.getterTargetType = getterTargetType; + this.getters = getterFactory.create(getterTargetType, schema); } @Override @@ -90,6 +95,10 @@ public W getValue(int fieldIdx) { return (W) fieldValue; } + public TypeDescriptor getGetterTargetType() { + return getterTargetType; + } + private boolean cacheFieldType(Field field) { TypeName typeName = field.getType().getTypeName(); return typeName.equals(TypeName.MAP) diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/AutoValueSchemaTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/AutoValueSchemaTest.java index f47002125119..99b858a4e307 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/AutoValueSchemaTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/AutoValueSchemaTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas; +import static org.apache.beam.sdk.schemas.utils.SchemaTestUtils.assertSchemaEquivalent; import static org.apache.beam.sdk.schemas.utils.SchemaTestUtils.equivalentTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertArrayEquals; @@ -28,6 +29,8 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.annotations.DefaultSchema; @@ -37,9 +40,13 @@ import org.apache.beam.sdk.schemas.annotations.SchemaFieldName; import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; import org.apache.beam.sdk.schemas.utils.SchemaTestUtils; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.CaseFormat; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.joda.time.DateTime; import org.joda.time.Instant; import org.junit.Test; @@ -70,7 +77,7 @@ public class AutoValueSchemaTest { .build(); static final Schema OUTER_SCHEMA = Schema.builder().addRowField("inner", SIMPLE_SCHEMA).build(); - private Row createSimpleRow(String name) { + private static Row createSimpleRow(String name) { return Row.withSchema(SIMPLE_SCHEMA) .addValues( name, @@ -348,6 +355,50 @@ abstract static class Builder { } } + @DefaultSchema(AutoValueSchema.class) + @AutoValue + abstract static class GenericAutoValue { + public abstract T getT(); + + GenericAutoValue() {} + + public static GenericAutoValue create(T t) { + return new AutoValue_AutoValueSchemaTest_GenericAutoValue<>(t); + } + } + + @DefaultSchema(AutoValueSchema.class) + @AutoValue + abstract static class GenericAutoValueWithBuilder { + public abstract T getT(); + + GenericAutoValueWithBuilder() {} + + public static Builder builder() { + return new AutoValue_AutoValueSchemaTest_GenericAutoValueWithBuilder.Builder<>(); + } + + @AutoValue.Builder + abstract static class Builder { + public abstract Builder setT(T t); + + public abstract GenericAutoValueWithBuilder build(); + } + } + + @DefaultSchema(AutoValueSchema.class) + @AutoValue + abstract static class GenericAutoValueWithCreator { + public abstract T getT(); + + GenericAutoValueWithCreator() {} + + @SchemaCreate + public static GenericAutoValueWithCreator create(T t) { + return new AutoValue_AutoValueSchemaTest_GenericAutoValueWithCreator<>(t); + } + } + private void verifyRow(Row row) { assertEquals("string", row.getString("str")); assertEquals((byte) 1, (Object) row.getByte("aByte")); @@ -385,6 +436,375 @@ public void testSchema() throws NoSuchSchemaException { SchemaTestUtils.assertSchemaEquivalent(SIMPLE_SCHEMA, schema); } + @Test + public void testGenericAutoValueSchema() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema actual = registry.getSchema(new TypeDescriptor>() {}); + Schema expected = Schema.builder().addRowField("t", SIMPLE_SCHEMA).build(); + assertSchemaEquivalent(expected, actual); + } + + @Test + public void testNestedGenericAutoValueSchema() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema actual = + registry.getSchema( + new TypeDescriptor>>() {}); + Schema expected = + Schema.builder() + .addRowField("t", Schema.builder().addRowField("t", SIMPLE_SCHEMA).build()) + .build(); + + assertSchemaEquivalent(expected, actual); + } + + @Test + public void testGenericAutoValueToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction, Row> toRow = + registry.getToRowFunction(new TypeDescriptor>() {}); + Row row = + toRow.apply( + GenericAutoValue.create( + new AutoValue_AutoValueSchemaTest_SimpleAutoValue( + "string", + (byte) 1, + (short) 2, + 3, + 4L, + true, + DATE, + BYTE_ARRAY, + ByteBuffer.wrap(BYTE_ARRAY), + DATE.toInstant(), + BigDecimal.ONE, + STRING_BUILDER))); + + verifyRow(row.getRow("t")); + } + + @Test + public void testGenericAutoValueFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction> fromRow = + registry.getFromRowFunction(new TypeDescriptor>() {}); + + Row row = + Row.withSchema(Schema.builder().addRowField("t", SIMPLE_SCHEMA).build()) + .withFieldValue("t", createSimpleRow("string")) + .build(); + GenericAutoValue actual = fromRow.apply(row); + verifyAutoValue(actual.getT()); + } + + @Test + public void testGenericAutoValueWithCreatorFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction> fromRow = + registry.getFromRowFunction( + new TypeDescriptor>() {}); + + Row row = + Row.withSchema(Schema.builder().addRowField("t", SIMPLE_SCHEMA).build()) + .withFieldValue("t", createSimpleRow("string")) + .build(); + GenericAutoValueWithCreator actual = fromRow.apply(row); + verifyAutoValue(actual.getT()); + } + + @Test + public void testGenericAutoValueWithBuilderFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction> fromRow = + registry.getFromRowFunction( + new TypeDescriptor>() {}); + + Row row = + Row.withSchema(Schema.builder().addRowField("t", SIMPLE_SCHEMA).build()) + .withFieldValue("t", createSimpleRow("string")) + .build(); + GenericAutoValueWithBuilder actual = fromRow.apply(row); + verifyAutoValue(actual.getT()); + } + + @Test + public void testGenericAutoValueBuilderOfMapOfCreatorsFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction< + Row, GenericAutoValueWithBuilder>>> + fromRow = + registry.getFromRowFunction( + new TypeDescriptor< + GenericAutoValueWithBuilder< + Map>>>() {}); + + Schema mapValueSchema = Schema.builder().addField("t", FieldType.STRING).build(); + + Row row = + Row.withSchema( + Schema.builder() + .addMapField("t", FieldType.STRING, FieldType.row(mapValueSchema)) + .build()) + .withFieldValue( + "t", + ImmutableMap.builder() + .put("k1", Row.withSchema(mapValueSchema).withFieldValue("t", "v1").build()) + .put("k2", Row.withSchema(mapValueSchema).withFieldValue("t", "v2").build()) + .build()) + .build(); + + GenericAutoValueWithBuilder>> actual = + fromRow.apply(row); + GenericAutoValueWithCreator genericAutoValue1 = + GenericAutoValueWithCreator.create("v1"); + GenericAutoValueWithCreator genericAutoValue2 = + GenericAutoValueWithCreator.create("v2"); + + assertEquals(genericAutoValue1, actual.getT().get("k1")); + assertEquals(genericAutoValue2, actual.getT().get("k2")); + } + + @Test + public void testGenericAutoValueCreatorOfMapOfBuildersFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction< + Row, GenericAutoValueWithCreator>>> + fromRow = + registry.getFromRowFunction( + new TypeDescriptor< + GenericAutoValueWithCreator< + Map>>>() {}); + + Schema mapValueSchema = Schema.builder().addField("t", FieldType.STRING).build(); + + Row row = + Row.withSchema( + Schema.builder() + .addMapField("t", FieldType.STRING, FieldType.row(mapValueSchema)) + .build()) + .withFieldValue( + "t", + ImmutableMap.builder() + .put("k1", Row.withSchema(mapValueSchema).withFieldValue("t", "v1").build()) + .put("k2", Row.withSchema(mapValueSchema).withFieldValue("t", "v2").build()) + .build()) + .build(); + + GenericAutoValueWithCreator>> actual = + fromRow.apply(row); + GenericAutoValueWithBuilder genericAutoValue1 = + GenericAutoValueWithBuilder.builder().setT("v1").build(); + GenericAutoValueWithBuilder genericAutoValue2 = + GenericAutoValueWithBuilder.builder().setT("v2").build(); + + assertEquals(genericAutoValue1, actual.getT().get("k1")); + assertEquals(genericAutoValue2, actual.getT().get("k2")); + } + + @Test + public void testGenericAutoValueBuilderOfListOfCreatorsFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction< + Row, GenericAutoValueWithBuilder>>> + fromRow = + registry.getFromRowFunction( + new TypeDescriptor< + GenericAutoValueWithBuilder>>>() {}); + + Schema listElementSchema = Schema.builder().addField("t", FieldType.STRING).build(); + + Row row = + Row.withSchema( + Schema.builder().addArrayField("t", FieldType.row(listElementSchema)).build()) + .withFieldValue( + "t", + ImmutableList.builder() + .add(Row.withSchema(listElementSchema).withFieldValue("t", "v1").build()) + .add(Row.withSchema(listElementSchema).withFieldValue("t", "v2").build()) + .build()) + .build(); + + GenericAutoValueWithBuilder>> actual = + fromRow.apply(row); + GenericAutoValueWithCreator genericAutoValue1 = + GenericAutoValueWithCreator.create("v1"); + GenericAutoValueWithCreator genericAutoValue2 = + GenericAutoValueWithCreator.create("v2"); + + assertEquals(genericAutoValue1, actual.getT().get(0)); + assertEquals(genericAutoValue2, actual.getT().get(1)); + } + + @Test + public void testGenericAutoValueCreatorOfListOfBuildersFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction< + Row, GenericAutoValueWithCreator>>> + fromRow = + registry.getFromRowFunction( + new TypeDescriptor< + GenericAutoValueWithCreator>>>() {}); + + Schema listElementSchema = Schema.builder().addField("t", FieldType.STRING).build(); + + Row row = + Row.withSchema( + Schema.builder().addArrayField("t", FieldType.row(listElementSchema)).build()) + .withFieldValue( + "t", + ImmutableList.builder() + .add(Row.withSchema(listElementSchema).withFieldValue("t", "v1").build()) + .add(Row.withSchema(listElementSchema).withFieldValue("t", "v2").build()) + .build()) + .build(); + + GenericAutoValueWithCreator>> actual = + fromRow.apply(row); + GenericAutoValueWithBuilder genericAutoValue1 = + GenericAutoValueWithBuilder.builder().setT("v1").build(); + GenericAutoValueWithBuilder genericAutoValue2 = + GenericAutoValueWithBuilder.builder().setT("v2").build(); + + assertEquals(genericAutoValue1, actual.getT().get(0)); + assertEquals(genericAutoValue2, actual.getT().get(1)); + } + + @Test + public void testGenericAutoValueBuilderOfArrayOfCreatorsFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction[]>> + fromRow = + registry.getFromRowFunction( + new TypeDescriptor< + GenericAutoValueWithBuilder[]>>() {}); + + Schema arrayElementSchema = Schema.builder().addField("t", FieldType.STRING).build(); + + Row row = + Row.withSchema( + Schema.builder().addArrayField("t", FieldType.row(arrayElementSchema)).build()) + .withFieldValue( + "t", + ImmutableList.builder() + .add(Row.withSchema(arrayElementSchema).withFieldValue("t", "v1").build()) + .add(Row.withSchema(arrayElementSchema).withFieldValue("t", "v2").build()) + .build()) + .build(); + + GenericAutoValueWithBuilder[]> actual = fromRow.apply(row); + GenericAutoValueWithCreator genericAutoValue1 = + GenericAutoValueWithCreator.create("v1"); + GenericAutoValueWithCreator genericAutoValue2 = + GenericAutoValueWithCreator.create("v2"); + + assertEquals(genericAutoValue1, actual.getT()[0]); + assertEquals(genericAutoValue2, actual.getT()[1]); + } + + @Test + public void testGenericAutoValueCreatorOfArrayOfBuildersFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction[]>> + fromRow = + registry.getFromRowFunction( + new TypeDescriptor< + GenericAutoValueWithCreator[]>>() {}); + + Schema arrayElementSchema = Schema.builder().addField("t", FieldType.STRING).build(); + + Row row = + Row.withSchema( + Schema.builder().addArrayField("t", FieldType.row(arrayElementSchema)).build()) + .withFieldValue( + "t", + ImmutableList.builder() + .add(Row.withSchema(arrayElementSchema).withFieldValue("t", "v1").build()) + .add(Row.withSchema(arrayElementSchema).withFieldValue("t", "v2").build()) + .build()) + .build(); + + GenericAutoValueWithCreator[]> actual = fromRow.apply(row); + GenericAutoValueWithBuilder genericAutoValue1 = + GenericAutoValueWithBuilder.builder().setT("v1").build(); + GenericAutoValueWithBuilder genericAutoValue2 = + GenericAutoValueWithBuilder.builder().setT("v2").build(); + + assertEquals(genericAutoValue1, actual.getT()[0]); + assertEquals(genericAutoValue2, actual.getT()[1]); + } + + @Test + public void testGenericAutoValueWithMapToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction>>, Row> toRow = + registry.getToRowFunction( + new TypeDescriptor>>>() {}); + + GenericAutoValue genericAutoValue1 = GenericAutoValue.create("v1"); + GenericAutoValue genericAutoValue2 = GenericAutoValue.create("v2"); + + Row row = + toRow.apply( + GenericAutoValue.create( + ImmutableMap.of("k1", genericAutoValue1, "k2", genericAutoValue2))); + + assertEquals("v1", row.getMap("t").get("k1").getString("t")); + assertEquals("v2", row.getMap("t").get("k2").getString("t")); + } + + @Test + public void testGenericAutoValueWithListToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction>>, Row> toRow = + registry.getToRowFunction( + new TypeDescriptor>>>() {}); + + GenericAutoValue genericAutoValue1 = GenericAutoValue.create("v1"); + GenericAutoValue genericAutoValue2 = GenericAutoValue.create("v2"); + + Row row = + toRow.apply( + GenericAutoValue.create(ImmutableList.of(genericAutoValue1, genericAutoValue2))); + Row[] genericAutoValueRows = row.getArray("t").toArray(new Row[0]); + + assertEquals("v1", genericAutoValueRows[0].getString("t")); + assertEquals("v2", genericAutoValueRows[1].getString("t")); + } + + @Test + public void testGenericAutoValueWithArrayToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction[]>, Row> toRow = + registry.getToRowFunction( + new TypeDescriptor[]>>() {}); + + GenericAutoValue genericAutoValue1 = GenericAutoValue.create("v1"); + GenericAutoValue genericAutoValue2 = GenericAutoValue.create("v2"); + + @SuppressWarnings("unchecked") + Row row = + toRow.apply( + GenericAutoValue.create(new GenericAutoValue[] {genericAutoValue1, genericAutoValue2})); + Row[] genericAutoValueRows = row.getArray("t").toArray(new Row[0]); + + assertEquals("v1", genericAutoValueRows[0].getString("t")); + assertEquals("v2", genericAutoValueRows[1].getString("t")); + } + + @Test + public void testNestedGenericAutoValueToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SerializableFunction>>, Row> toRow = + registry.getToRowFunction( + new TypeDescriptor>>>() {}); + + Row row = + toRow.apply( + GenericAutoValue.create(GenericAutoValue.create(GenericAutoValue.create("v1")))); + + assertEquals("v1", row.getRow("t").getRow("t").getString("t")); + } + @Test public void testToRowConstructor() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); @@ -402,6 +822,7 @@ public void testToRowConstructor() throws NoSuchSchemaException { DATE.toInstant(), BigDecimal.ONE, STRING_BUILDER); + Row row = registry.getToRowFunction(SimpleAutoValue.class).apply(value); verifyRow(row); } @@ -444,6 +865,7 @@ public void testToRowConstructorMemoized() throws NoSuchSchemaException { DATE.toInstant(), BigDecimal.ONE, STRING_BUILDER); + Row row = registry.getToRowFunction(MemoizedAutoValue.class).apply(value); verifyRow(row); } @@ -571,6 +993,7 @@ public void testToRowNestedConstructor() throws NoSuchSchemaException { DATE.toInstant(), BigDecimal.ONE, STRING_BUILDER); + AutoValueOuter outer = new AutoValue_AutoValueSchemaTest_AutoValueOuter(inner); Row row = registry.getToRowFunction(AutoValueOuter.class).apply(outer); verifyRow(row.getRow("inner")); @@ -675,6 +1098,7 @@ static SimpleAutoValueWithStaticFactory create( Instant instant, BigDecimal bigDecimal, StringBuilder stringBuilder) { + return new AutoValue_AutoValueSchemaTest_SimpleAutoValueWithStaticFactory( str, aByte, diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java index de0953a0a088..21c6dee13692 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaBeanSchemaTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas; +import static org.apache.beam.sdk.schemas.utils.SchemaTestUtils.assertSchemaEquivalent; import static org.apache.beam.sdk.schemas.utils.SchemaTestUtils.equivalentTo; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.ALL_NULLABLE_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.ANNOTATED_SIMPLE_BEAN_SCHEMA; @@ -33,6 +34,7 @@ import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.PRIMITIVE_ARRAY_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.RENAMED_FIELDS_AND_SETTERS_BEAM_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.SIMPLE_BEAN_SCHEMA; +import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.genericBeanSchema; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -63,6 +65,7 @@ import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithCaseFormat; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithNoCreateOption; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithRenamedFieldsAndSetters; +import org.apache.beam.sdk.schemas.utils.TestJavaBeans.GenericBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.IterableBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.JavaTimeBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.MismatchingNullableBean; @@ -76,9 +79,11 @@ import org.apache.beam.sdk.schemas.utils.TestJavaBeans.SimpleBeanWithAnnotations; import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Streams; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.primitives.Ints; import org.joda.time.DateTime; import org.junit.Ignore; @@ -139,6 +144,16 @@ private Row createSimpleRow(String name) { .build(); } + private GenericBean createGeneric(T t) { + GenericBean genericBean = new GenericBean<>(); + genericBean.setT(t); + return genericBean; + } + + private Row createGenericRow(Schema.FieldType tFieldType, Object tFieldValue) { + return Row.withSchema(genericBeanSchema(tFieldType)).withFieldValue("t", tFieldValue).build(); + } + @Test public void testSchema() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); @@ -146,14 +161,9 @@ public void testSchema() throws NoSuchSchemaException { SchemaTestUtils.assertSchemaEquivalent(SIMPLE_BEAN_SCHEMA, schema); } - @Test - public void testToRow() throws NoSuchSchemaException { - SchemaRegistry registry = SchemaRegistry.createDefault(); - SimpleBean bean = createSimple("string"); - Row row = registry.getToRowFunction(SimpleBean.class).apply(bean); - + private static void verifyRow(String expectedStrField, Row row) { assertEquals(12, row.getFieldCount()); - assertEquals("string", row.getString("str")); + assertEquals(expectedStrField, row.getString("str")); assertEquals((byte) 1, (Object) row.getByte("aByte")); assertEquals((short) 2, (Object) row.getInt16("aShort")); assertEquals((int) 3, (Object) row.getInt32("anInt")); @@ -167,13 +177,8 @@ public void testToRow() throws NoSuchSchemaException { assertEquals("stringbuilder", row.getString("stringBuilder")); } - @Test - public void testFromRow() throws NoSuchSchemaException { - SchemaRegistry registry = SchemaRegistry.createDefault(); - Row row = createSimpleRow("string"); - - SimpleBean bean = registry.getFromRowFunction(SimpleBean.class).apply(row); - assertEquals("string", bean.getStr()); + private static void verifySimpleBean(String expectedStrField, SimpleBean bean) { + assertEquals(expectedStrField, bean.getStr()); assertEquals((byte) 1, bean.getaByte()); assertEquals((short) 2, bean.getaShort()); assertEquals((int) 3, bean.getAnInt()); @@ -262,6 +267,23 @@ public void testJavaTimeRoundTrip() throws NoSuchSchemaException { assertEquals(original, roundTripped); } + @Test + public void testToRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SimpleBean bean = createSimple("string"); + Row row = registry.getToRowFunction(SimpleBean.class).apply(bean); + verifyRow("string", row); + } + + @Test + public void testFromRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = createSimpleRow("string"); + + SimpleBean bean = registry.getFromRowFunction(SimpleBean.class).apply(row); + verifySimpleBean("string", bean); + } + @Test public void testNullableToRow() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); @@ -708,4 +730,121 @@ public void testSetterConstructionWithRenamedFields() throws NoSuchSchemaExcepti assertEquals( registry.getFromRowFunction(BeanWithCaseFormat.class).apply(row), beanWithCaseFormat); } + + @Test + public void testGenericBeamSchema() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema actual = registry.getSchema(new TypeDescriptor>() {}); + Schema expected = genericBeanSchema(Schema.FieldType.row(SIMPLE_BEAN_SCHEMA)); + + assertSchemaEquivalent(expected, actual); + } + + @Test + public void testGenericBeamSchemaToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + GenericBean> genericBean = + createGeneric(createGeneric(createSimple("string"))); + + Row row = + registry + .getToRowFunction(new TypeDescriptor>>() {}) + .apply(genericBean); + + verifyRow("string", row.getRow("t").getRow("t")); + } + + @Test + public void testGenericBeamSchemaFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema nestedSchema = genericBeanSchema(Schema.FieldType.row(SIMPLE_BEAN_SCHEMA)); + Row row = + createGenericRow( + Schema.FieldType.row(nestedSchema), + createGenericRow(Schema.FieldType.row(SIMPLE_BEAN_SCHEMA), createSimpleRow("string"))); + GenericBean> actual = + registry + .getFromRowFunction(new TypeDescriptor>>() {}) + .apply(row); + + verifySimpleBean("string", actual.getT().getT()); + } + + @Test + public void testGenericBeamSchemaMapToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = + registry + .getToRowFunction( + new TypeDescriptor>>>() {}) + .apply( + createGeneric( + ImmutableMap.>builder() + .put("k1", createGeneric("v1")) + .put("k2", createGeneric("v2")) + .build())); + + assertEquals("v1", row.getMap("t").get("k1").getString("t")); + assertEquals("v2", row.getMap("t").get("k2").getString("t")); + } + + @Test + public void testGenericBeamSchemaMapFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema.FieldType mapValueFieldType = + Schema.FieldType.row(genericBeanSchema(Schema.FieldType.STRING)); + GenericBean>> actual = + registry + .getFromRowFunction( + new TypeDescriptor>>>() {}) + .apply( + createGenericRow( + Schema.FieldType.map(Schema.FieldType.STRING, mapValueFieldType), + ImmutableMap.builder() + .put("k1", createGenericRow(Schema.FieldType.STRING, "v1")) + .put("k2", createGenericRow(Schema.FieldType.STRING, "v2")) + .build())); + + assertEquals("v1", actual.getT().get("k1").getT()); + assertEquals("v2", actual.getT().get("k2").getT()); + } + + @Test + public void testGenericBeamSchemaIterableToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = + registry + .getToRowFunction(new TypeDescriptor>>>() {}) + .apply( + createGeneric( + ImmutableList.>builder() + .add(createGeneric("v1")) + .add(createGeneric("v2")) + .build())); + + Row[] rows = Streams.stream(row.getIterable("t")).toArray(Row[]::new); + + assertEquals("v1", rows[0].getString("t")); + assertEquals("v2", rows[1].getString("t")); + } + + @Test + public void testGenericBeamSchemaIterableFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema.FieldType elementFieldType = + Schema.FieldType.row(genericBeanSchema(Schema.FieldType.STRING)); + GenericBean>> actual = + registry + .getFromRowFunction(new TypeDescriptor>>>() {}) + .apply( + createGenericRow( + Schema.FieldType.array(elementFieldType), + ImmutableList.builder() + .add(createGenericRow(Schema.FieldType.STRING, "v1")) + .add(createGenericRow(Schema.FieldType.STRING, "v2")) + .build())); + GenericBean[] beans = Streams.stream(actual.getT()).toArray(GenericBean[]::new); + assertEquals("v1", beans[0].getT()); + assertEquals("v2", beans[1].getT()); + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java index c80b758adc31..d4f94f021c33 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/JavaFieldSchemaTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas; +import static org.apache.beam.sdk.schemas.utils.SchemaTestUtils.assertSchemaEquivalent; import static org.apache.beam.sdk.schemas.utils.SchemaTestUtils.equivalentTo; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.ANNOTATED_SIMPLE_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.CASE_FORMAT_POJO_SCHEMA; @@ -36,6 +37,7 @@ import static org.apache.beam.sdk.schemas.utils.TestPOJOs.PRIMITIVE_ARRAY_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.SIMPLE_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.SIMPLE_POJO_WITH_DESCRIPTION_SCHEMA; +import static org.apache.beam.sdk.schemas.utils.TestPOJOs.genericPOJOSchema; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertArrayEquals; @@ -64,6 +66,7 @@ import org.apache.beam.sdk.schemas.utils.TestPOJOs; import org.apache.beam.sdk.schemas.utils.TestPOJOs.AnnotatedSimplePojo; import org.apache.beam.sdk.schemas.utils.TestPOJOs.FirstCircularNestedPOJO; +import org.apache.beam.sdk.schemas.utils.TestPOJOs.GenericPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.JavaTimePOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedArrayPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedArraysPOJO; @@ -86,9 +89,11 @@ import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Streams; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.primitives.Ints; import org.joda.time.DateTime; import org.joda.time.Instant; @@ -190,6 +195,10 @@ private Row createAnnotatedRow(String name) { .build(); } + private static Row createGenericRow(FieldType tFieldType, Object tFieldValue) { + return Row.withSchema(genericPOJOSchema(tFieldType)).withFieldValue("t", tFieldValue).build(); + } + @Test public void testSchema() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); @@ -197,14 +206,9 @@ public void testSchema() throws NoSuchSchemaException { SchemaTestUtils.assertSchemaEquivalent(SIMPLE_POJO_SCHEMA, schema); } - @Test - public void testToRow() throws NoSuchSchemaException { - SchemaRegistry registry = SchemaRegistry.createDefault(); - SimplePOJO pojo = createSimple("string"); - Row row = registry.getToRowFunction(SimplePOJO.class).apply(pojo); - + private static void verifySimpleRow(String expectedStrField, Row row) { assertEquals(12, row.getFieldCount()); - assertEquals("string", row.getString("str")); + assertEquals(expectedStrField, row.getString("str")); assertEquals((byte) 1, (Object) row.getByte("aByte")); assertEquals((short) 2, (Object) row.getInt16("aShort")); assertEquals((int) 3, (Object) row.getInt32("anInt")); @@ -218,13 +222,8 @@ public void testToRow() throws NoSuchSchemaException { assertEquals("stringbuilder", row.getString("stringBuilder")); } - @Test - public void testFromRow() throws NoSuchSchemaException { - SchemaRegistry registry = SchemaRegistry.createDefault(); - Row row = createSimpleRow("string"); - - SimplePOJO pojo = registry.getFromRowFunction(SimplePOJO.class).apply(row); - assertEquals("string", pojo.str); + private static void verifySimplePOJO(String expectedStrField, SimplePOJO pojo) { + assertEquals(expectedStrField, pojo.str); assertEquals((byte) 1, pojo.aByte); assertEquals((short) 2, pojo.aShort); assertEquals((int) 3, pojo.anInt); @@ -350,6 +349,23 @@ public void testNullableJavaTimeFromRow() throws NoSuchSchemaException { assertNull(pojo.uuid); } + @Test + public void testToRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + SimplePOJO pojo = createSimple("string"); + Row row = registry.getToRowFunction(SimplePOJO.class).apply(pojo); + verifySimpleRow("string", row); + } + + @Test + public void testFromRow() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = createSimpleRow("string"); + + SimplePOJO pojo = registry.getFromRowFunction(SimplePOJO.class).apply(row); + verifySimplePOJO("string", pojo); + } + @Test public void testNullableSchema() throws NoSuchSchemaException { SchemaRegistry registry = SchemaRegistry.createDefault(); @@ -903,4 +919,156 @@ public void testCircularNestedPOJOThrows() throws NoSuchSchemaException { thrown.getMessage(), containsString("TestPOJOs$FirstCircularNestedPOJO")); } + + @Test + public void testGenericPOJOSchema() throws Exception { + Schema actual = + SchemaRegistry.createDefault() + .getSchema(new TypeDescriptor>>() {}); + Schema expected = + genericPOJOSchema(FieldType.row(genericPOJOSchema(FieldType.row(SIMPLE_POJO_SCHEMA)))); + assertSchemaEquivalent(expected, actual); + } + + @Test + public void testGenericPOJOToRow() throws Exception { + Row row = + SchemaRegistry.createDefault() + .getToRowFunction(new TypeDescriptor>>() {}) + .apply(GenericPOJO.create(GenericPOJO.create(createSimple("string")))); + + verifySimpleRow("string", row.getRow("t").getRow("t")); + } + + @Test + public void testGenericPOJOFromRow() throws Exception { + FieldType innerGenericPOJOFieldType = + FieldType.row(genericPOJOSchema(FieldType.row(SIMPLE_POJO_SCHEMA))); + GenericPOJO> actualPOJO = + SchemaRegistry.createDefault() + .getFromRowFunction(new TypeDescriptor>>() {}) + .apply( + createGenericRow( + innerGenericPOJOFieldType, + createGenericRow( + FieldType.row(SIMPLE_POJO_SCHEMA), createSimpleRow("string")))); + + verifySimplePOJO("string", actualPOJO.t.t); + } + + @Test + public void testGenericPOJOSchemaMapToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = + registry + .getToRowFunction( + new TypeDescriptor>>>() {}) + .apply( + GenericPOJO.create( + ImmutableMap.>builder() + .put("k1", GenericPOJO.create("v1")) + .put("k2", GenericPOJO.create("v2")) + .build())); + + assertEquals("v1", row.getMap("t").get("k1").getString("t")); + assertEquals("v2", row.getMap("t").get("k2").getString("t")); + } + + @Test + public void testGenericPOJOSchemaMapFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema.FieldType mapValueFieldType = + Schema.FieldType.row(genericPOJOSchema(Schema.FieldType.STRING)); + GenericPOJO>> actual = + registry + .getFromRowFunction( + new TypeDescriptor>>>() {}) + .apply( + createGenericRow( + Schema.FieldType.map(Schema.FieldType.STRING, mapValueFieldType), + ImmutableMap.builder() + .put("k1", createGenericRow(Schema.FieldType.STRING, "v1")) + .put("k2", createGenericRow(Schema.FieldType.STRING, "v2")) + .build())); + + assertEquals("v1", actual.t.get("k1").t); + assertEquals("v2", actual.t.get("k2").t); + } + + @Test + public void testGenericBeamSchemaIterableToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = + registry + .getToRowFunction(new TypeDescriptor>>>() {}) + .apply( + GenericPOJO.create( + ImmutableList.>builder() + .add(GenericPOJO.create("v1")) + .add(GenericPOJO.create("v2")) + .build())); + + Row[] rows = Streams.stream(row.getIterable("t")).toArray(Row[]::new); + + assertEquals("v1", rows[0].getString("t")); + assertEquals("v2", rows[1].getString("t")); + } + + @Test + public void testGenericBeamSchemaIterableFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema.FieldType elementFieldType = + Schema.FieldType.row(genericPOJOSchema(Schema.FieldType.STRING)); + GenericPOJO>> actual = + registry + .getFromRowFunction(new TypeDescriptor>>>() {}) + .apply( + createGenericRow( + Schema.FieldType.array(elementFieldType), + ImmutableList.builder() + .add(createGenericRow(Schema.FieldType.STRING, "v1")) + .add(createGenericRow(Schema.FieldType.STRING, "v2")) + .build())); + GenericPOJO[] pojos = Streams.stream(actual.t).toArray(GenericPOJO[]::new); + assertEquals("v1", pojos[0].t); + assertEquals("v2", pojos[1].t); + } + + @Test + public void testGenericBeamSchemaArrayToRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Row row = + registry + .getToRowFunction(new TypeDescriptor[]>>() {}) + .apply( + GenericPOJO.create( + new GenericPOJO[] { + GenericPOJO.create("v1"), GenericPOJO.create("v2"), + })); + + Row[] rows = Streams.stream(row.getIterable("t")).toArray(Row[]::new); + + assertEquals("v1", rows[0].getString("t")); + assertEquals("v2", rows[1].getString("t")); + } + + @Test + public void testGenericBeamSchemaArrayFromRow() throws Exception { + SchemaRegistry registry = SchemaRegistry.createDefault(); + Schema.FieldType elementFieldType = + Schema.FieldType.row(genericPOJOSchema(Schema.FieldType.STRING)); + GenericPOJO[]> actual = + registry + .getFromRowFunction(new TypeDescriptor[]>>() {}) + .apply( + createGenericRow( + Schema.FieldType.array(elementFieldType), + ImmutableList.builder() + .add(createGenericRow(Schema.FieldType.STRING, "v1")) + .add(createGenericRow(Schema.FieldType.STRING, "v2")) + .build())); + GenericPOJO[] pojos = actual.t; + assertEquals("v1", pojos[0].t); + assertEquals("v2", pojos[1].t); + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java index d8ed86f2b552..72db48ccf974 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java @@ -1487,4 +1487,39 @@ public int hashCode() { .addLogicalTypeField("instant", new NanosInstant()) .addLogicalTypeField("uuid", SqlTypes.UUID) .build(); + + @DefaultSchema(JavaBeanSchema.class) + public static class GenericBean { + @Nullable private T t; + + @Nullable + public T getT() { + return t; + } + + public void setT(@Nullable T t) { + this.t = t; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GenericBean)) { + return false; + } + GenericBean that = (GenericBean) o; + return Objects.equals(t, that.t); + } + + @Override + public int hashCode() { + return Objects.hashCode(t); + } + } + + public static Schema genericBeanSchema(FieldType genericFieldType) { + return Schema.builder().addNullableField("t", genericFieldType).build(); + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java index 38ec507480f7..97f1bb20f428 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestPOJOs.java @@ -1399,4 +1399,29 @@ public int hashCode() { .addNullableLogicalTypeField("instant", new NanosInstant()) .addNullableLogicalTypeField("uuid", SqlTypes.UUID) .build(); + + @DefaultSchema(JavaFieldSchema.class) + public static class GenericPOJOWithCreator { + public @Nullable T t; + + @SchemaCreate + public GenericPOJOWithCreator(@Nullable T t) { + this.t = t; + } + } + + @DefaultSchema(JavaFieldSchema.class) + public static class GenericPOJO { + public @Nullable T t; + + public static GenericPOJO create(T t) { + GenericPOJO genericPOJO = new GenericPOJO<>(); + genericPOJO.t = t; + return genericPOJO; + } + } + + public static Schema genericPOJOSchema(FieldType tFieldType) { + return Schema.builder().addNullableField("t", tFieldType).build(); + } } diff --git a/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java b/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java index 3e22bad6a415..bf7078abc58e 100644 --- a/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java +++ b/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java @@ -533,7 +533,9 @@ public Row next() { throw new IllegalStateException("There are no more Rows."); } Row result = - Row.withSchema(schema).withFieldValueGetters(this.fieldValueGetters, this.currRowIndex); + Row.withSchema(schema) + .withFieldValueGetters( + this.fieldValueGetters, this.currRowIndex, TypeDescriptor.of(Integer.class)); this.currRowIndex += 1; return result; } diff --git a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsSchemaProvider.java b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsSchemaProvider.java index 21b5c7bcf97d..6b3ff6cac861 100644 --- a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsSchemaProvider.java +++ b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsSchemaProvider.java @@ -31,6 +31,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import org.apache.beam.sdk.io.aws2.schemas.AwsSchemaUtils.SdkBuilderSetter; import org.apache.beam.sdk.io.aws2.schemas.AwsTypes.ConverterFactory; import org.apache.beam.sdk.schemas.CachingFactory; @@ -205,7 +206,11 @@ private void checkForUnknownFields(Schema schema, Map> field @Override public List fieldValueTypeInformations( TypeDescriptor targetTypeDescriptor, Schema schema) { - throw new UnsupportedOperationException("FieldValueTypeInformation not available"); + List> sdkFieldList = sdkFields((Class) targetTypeDescriptor.getRawType()); + + return sdkFieldList.stream() + .map(AwsTypes::fieldValueTypeInformationFor) + .collect(Collectors.toList()); } @Override diff --git a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsTypes.java b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsTypes.java index a0fc0c8e91cd..f5b06d3cd1c9 100644 --- a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsTypes.java +++ b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/schemas/AwsTypes.java @@ -27,12 +27,14 @@ import static software.amazon.awssdk.core.protocol.MarshallingType.SDK_POJO; import java.io.Serializable; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import org.apache.beam.sdk.schemas.Factory; +import org.apache.beam.sdk.schemas.FieldValueTypeInformation; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.Field; import org.apache.beam.sdk.schemas.Schema.FieldType; @@ -91,6 +93,20 @@ private static FieldType fieldType(SdkField field, Set> seen) { String.format("Type %s of field %s is unknown.", type, normalizedNameOf(field))); } + static FieldValueTypeInformation fieldValueTypeInformationFor(SdkField sdkField) { + TypeDescriptor type = TypeDescriptor.of(sdkField.marshallingType().getTargetClass()); + return FieldValueTypeInformation.builder() + .setName(normalizedNameOf(sdkField)) + .setType(type) + .setRawType(sdkField.marshallingType().getClass()) + .setElementType(FieldValueTypeInformation.getIterableComponentType(type)) + .setMapKeyType(FieldValueTypeInformation.getMapKeyType(type)) + .setMapValueType(FieldValueTypeInformation.getMapValueType(type)) + .setOneOfTypes(Collections.emptyMap()) + .setNullable(true) + .build(); + } + private static Schema schemaFor(List> fields, Set> seen) { Schema.Builder builder = Schema.builder(); for (SdkField sdkField : fields) { From a7674e46ad214527b470406d401b4c679871a87c Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Mon, 8 Jun 2026 03:46:18 -0700 Subject: [PATCH 317/490] [Dataflow Streaming] [Multi Key] Introduce KeyGroupWorkQueue and integrate with BoundedQueueExecutor (#38767) * [Dataflow Streaming] Introduce KeyGroupWorkQueue and integrate with BoundedQueueExecutor - Add Uint128Proto and key_group to WorkItem in windmill.proto. - Update Work model to support key groups. - Implement KeyGroupWorkQueue for grouping tasks by key group. KepGroupWorkQueue is a FIFO queue that allows polling elements based on global order and also by order within a key group. - Integrate BoundedQueueExecutor with KepGroupWorkQueue and support targeted polling. --- .../worker/StreamingDataflowWorker.java | 8 +- .../worker/streaming/ExecutableWork.java | 12 +- .../dataflow/worker/streaming/Work.java | 71 +++ .../worker/util/BoundedQueueExecutor.java | 33 +- .../worker/util/KeyGroupWorkQueue.java | 462 ++++++++++++++++ .../worker/StreamingDataflowWorkerTest.java | 12 +- .../worker/util/BoundedQueueExecutorTest.java | 150 +++++- .../worker/util/KeyGroupWorkQueueTest.java | 504 ++++++++++++++++++ .../StreamingCommitFinalizerTest.java | 3 +- .../failures/WorkFailureProcessorTest.java | 3 +- .../work/refresh/ActiveWorkRefresherTest.java | 3 +- .../windmill/src/main/proto/windmill.proto | 7 + 12 files changed, 1247 insertions(+), 21 deletions(-) create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java index 4c3e58978ec3..9e82343474c6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java @@ -179,6 +179,9 @@ public final class StreamingDataflowWorker { // Experiment make the monitor within BoundedQueueExecutor fair public static final String BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT = "windmill_bounded_queue_executor_use_fair_monitor"; + // Don't use. Experiment guarding multi key bundles. The feature is work in progress and + // incomplete. + private static final String UNSTABLE_ENABLE_MULTI_KEY_BUNDLE = "unstable_enable_multi_key_bundle"; private final WindmillStateCache stateCache; private AtomicReference statusPages = new AtomicReference<>(); @@ -1018,6 +1021,8 @@ private static JobHeader createJobHeader(DataflowWorkerHarnessOptions options, l private static BoundedQueueExecutor createWorkUnitExecutor(DataflowWorkerHarnessOptions options) { boolean useFairMonitor = DataflowRunner.hasExperiment(options, BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT); + boolean useKeyGroupWorkQueue = + DataflowRunner.hasExperiment(options, UNSTABLE_ENABLE_MULTI_KEY_BUNDLE); return new BoundedQueueExecutor( chooseMaxThreads(options), THREAD_EXPIRATION_TIME_SEC, @@ -1025,7 +1030,8 @@ private static BoundedQueueExecutor createWorkUnitExecutor(DataflowWorkerHarness chooseMaxBundlesOutstanding(options), chooseMaxBytesOutstanding(options), new ThreadFactoryBuilder().setNameFormat("DataflowWorkUnits-%d").setDaemon(true).build(), - useFairMonitor); + useFairMonitor, + useKeyGroupWorkQueue); } public static void main(String[] args) throws Exception { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java index ecaa673f5570..7748a554f0fc 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ExecutableWork.java @@ -62,11 +62,11 @@ public void run(BoundedQueueExecutorWorkHandle handle) { } } - public final WorkId id() { + public WorkId id() { return work().id(); } - public final Windmill.WorkItem getWorkItem() { + public Windmill.WorkItem getWorkItem() { return work().getWorkItem(); } @@ -74,4 +74,12 @@ public final Windmill.WorkItem getWorkItem() { public String toString() { return "ExecutableWork{" + id() + "}"; } + + public String getComputationId() { + return work().getComputationId(); + } + + public Work.KeyGroup getKeyGroup() { + return work().getKeyGroup(); + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java index cb01e1e508ce..53ed30fdedbb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java @@ -25,6 +25,7 @@ import java.util.IntSummaryStatistics; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -52,6 +53,7 @@ import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; @@ -74,6 +76,7 @@ public final class Work implements RefreshableWork { private final Instant startTime; private final Map totalDurationPerState; private final WorkId id; + private final KeyGroup keyGroup; private final String latencyTrackingId; private final long serializedWorkItemSize; private volatile TimedState currentState; @@ -101,6 +104,10 @@ private Work( // keyUniverse inside EnumMap every time. this.totalDurationPerState = new EnumMap<>(EMPTY_ENUM_MAP); this.id = WorkId.of(workItem); + this.keyGroup = + workItem.hasKeyGroup() + ? KeyGroup.create(workItem.getKeyGroup().getHigh(), workItem.getKeyGroup().getLow()) + : KeyGroup.DEFAULT; this.latencyTrackingId = Long.toHexString(workItem.getShardingKey()) + '-' @@ -383,6 +390,14 @@ private boolean isCommitPending() { abstract Instant startTime(); } + public String getComputationId() { + return processingContext.computationId(); + } + + public KeyGroup getKeyGroup() { + return keyGroup; + } + @AutoValue public abstract static class ProcessingContext { @@ -416,4 +431,60 @@ private Optional fetchKeyedState(KeyedGetDataRequest reque return Optional.ofNullable(getDataClient().getStateData(computationId(), request)); } } + + /** + * WorkItems with same key group and computation are eligible to be executed together in a + * multi-key bundle. + */ + public static final class KeyGroup { + + // Work items equaling to the default keyGroup will always be executed + // separately and not in a multi-key bundle + public static final KeyGroup DEFAULT = new KeyGroup(0, 0); + + private final long high; + private final long low; + + private KeyGroup(long high, long low) { + this.high = high; + this.low = low; + } + + public static KeyGroup create(long high, long low) { + if (high == 0 && low == 0) { + return DEFAULT; + } + return new KeyGroup(high, low); + } + + public long high() { + return high; + } + + public long low() { + return low; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof KeyGroup)) { + return false; + } + KeyGroup other = (KeyGroup) o; + return high == other.high && low == other.low; + } + + @Override + public int hashCode() { + return Objects.hash(high, low); + } + + @Override + public String toString() { + return String.format("%016x%016x", high, low); + } + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java index c6fd96e0a4cb..8964246c1160 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java @@ -29,15 +29,15 @@ import javax.annotation.concurrent.GuardedBy; import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle; import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; +import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.streaming.Work.KeyGroup; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard; +import org.checkerframework.checker.nullness.qual.Nullable; /** An executor for executing work on windmill items. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class BoundedQueueExecutor { private final ThreadPoolExecutor executor; @@ -78,6 +78,9 @@ private static class Budget { @GuardedBy("this") private long totalTimeMaxActiveThreadsUsed; + // If set the keyGroupWorkQueue is used by the underlying executor. + private final @Nullable KeyGroupWorkQueue keyGroupWorkQueue; + public BoundedQueueExecutor( int initialMaximumPoolSize, long keepAliveTime, @@ -85,7 +88,9 @@ public BoundedQueueExecutor( int maximumElementsOutstanding, long maximumBytesOutstanding, ThreadFactory threadFactory, - boolean useFairMonitor) { + boolean useFairMonitor, + boolean useKeyGroupWorkQueue) { + this.keyGroupWorkQueue = useKeyGroupWorkQueue ? new KeyGroupWorkQueue(useFairMonitor) : null; this.maximumPoolSize = initialMaximumPoolSize; monitor = new Monitor(useFairMonitor); executor = @@ -94,7 +99,7 @@ public BoundedQueueExecutor( initialMaximumPoolSize, keepAliveTime, unit, - new LinkedBlockingQueue<>(), + keyGroupWorkQueue != null ? keyGroupWorkQueue : new LinkedBlockingQueue<>(), threadFactory) { @Override protected void beforeExecute(Thread t, Runnable r) { @@ -313,7 +318,7 @@ public synchronized void close() { } } - private static final class QueuedWork implements Runnable { + static final class QueuedWork implements Runnable { private final ExecutableWork work; private final BoundedQueueExecutorWorkHandleImpl handle; @@ -378,6 +383,22 @@ BoundedQueueExecutorWorkHandleImpl createBudgetHandle(int elements, long bytes) return new BoundedQueueExecutorWorkHandleImpl(elements, bytes); } + public @Nullable ExecutableWork pollWork( + String computationId, Work.KeyGroup keyGroup, BoundedQueueExecutorWorkHandle handle) { + checkArgument(handle instanceof BoundedQueueExecutorWorkHandleImpl); + checkArgument(computationId != null && keyGroup != null && !keyGroup.equals(KeyGroup.DEFAULT)); + BoundedQueueExecutorWorkHandleImpl internalHandle = (BoundedQueueExecutorWorkHandleImpl) handle; + if (keyGroupWorkQueue == null) { + return null; + } + @Nullable QueuedWork queuedWork = keyGroupWorkQueue.pollWork(computationId, keyGroup); + if (queuedWork == null) { + return null; + } + internalHandle.merge(queuedWork.getHandle()); + return queuedWork.getWork(); + } + private void decrementCounters(int elements, long bytes) { // All threads queue decrements and one thread grabs the monitor and updates // counters. We do this to reduce contention on monitor which is locked by diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java new file mode 100644 index 000000000000..d151157ec68f --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueue.java @@ -0,0 +1,462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + +import java.util.AbstractQueue; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import javax.annotation.concurrent.GuardedBy; +import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.streaming.Work.KeyGroup; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.QueuedWork; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A custom, thread-safe doubly-linked BlockingQueue. In addition to global FIFO ordering, the queue + * supports polling work by computation + key group in FIFO order + */ +class KeyGroupWorkQueue extends AbstractQueue implements BlockingQueue { + + public static final Runnable SENTINEL_RUNNABLE = + () -> { + throw new IllegalStateException("sentinel runnable called"); + }; + + static class Node { + // If keyGroup is non-null, task is an instance of QueuedWork + final Runnable task; + final @Nullable String computationId; + final Work.@Nullable KeyGroup keyGroup; + // cached keyGroupList if the Node is part of one. + @Nullable KeyGroupWorkList keyGroupList; + + // prevNode, nextNode are used for the global order across all queued Runnables + @Nullable Node prevNode; + @Nullable Node nextNode; + + // prevKeyGroupNode and nextKeyGroupNode are used for the keyGroup level lists linking + // QueuedWork with same keyGroup + @Nullable Node prevKeyGroupNode; + @Nullable Node nextKeyGroupNode; + + Node(Runnable task) { + this.task = task; + if (task instanceof QueuedWork) { + this.computationId = ((QueuedWork) task).getWork().getComputationId(); + this.keyGroup = ((QueuedWork) task).getWork().getKeyGroup(); + } else { + this.computationId = null; + this.keyGroup = null; + } + } + } + + /** Double linked list implementing key group level queue */ + private static class KeyGroupWorkList { + final Node head = new Node(SENTINEL_RUNNABLE); + final Node tail = new Node(SENTINEL_RUNNABLE); + + KeyGroupWorkList() { + head.nextKeyGroupNode = tail; + tail.prevKeyGroupNode = head; + } + + boolean isEmpty() { + return head.nextKeyGroupNode == tail; + } + + void append(Node node) { + Node last = checkStateNotNull(tail.prevKeyGroupNode); + node.prevKeyGroupNode = last; + node.nextKeyGroupNode = tail; + last.nextKeyGroupNode = node; + tail.prevKeyGroupNode = node; + } + + void remove(Node node) { + @Nullable Node prev = node.prevKeyGroupNode; + @Nullable Node next = node.nextKeyGroupNode; + if (prev != null && next != null) { + prev.nextKeyGroupNode = next; + next.prevKeyGroupNode = prev; + node.prevKeyGroupNode = null; + node.nextKeyGroupNode = null; + } + } + } + + private final ReentrantLock lock; + private final Condition notEmpty; + + // Sentinels for the global list + @GuardedBy("lock") + private final Node globalHead = new Node(SENTINEL_RUNNABLE); + + @GuardedBy("lock") + private final Node globalTail = new Node(SENTINEL_RUNNABLE); + + @GuardedBy("lock") + private final Map keyGroupQueueMap = new HashMap<>(); + + @GuardedBy("lock") + private int size = 0; + + public KeyGroupWorkQueue(boolean fair) { + this.lock = new ReentrantLock(fair); + this.notEmpty = lock.newCondition(); + globalHead.nextNode = globalTail; + globalTail.prevNode = globalHead; + } + + @GuardedBy("lock") + private void unlinkNode(Node node) { + // An existing node should always have previous and next since we have sentinels + // 1. Unlink from global list + Node prevG = checkArgumentNotNull(node.prevNode); + Node nextG = checkArgumentNotNull(node.nextNode); + prevG.nextNode = nextG; + nextG.prevNode = prevG; + node.prevNode = null; + node.nextNode = null; + + // 2. Unlink from key group list + KeyGroupWorkList list = node.keyGroupList; + if (list != null) { + list.remove(node); + if (list.isEmpty()) { + String compId = checkStateNotNull(node.computationId); + Work.KeyGroup keyGroup = checkStateNotNull(node.keyGroup); + QueueKey key = new QueueKey(compId, keyGroup); + keyGroupQueueMap.remove(key); + } + node.keyGroupList = null; + } + --size; + } + + @GuardedBy("lock") + private @Nullable Node removeFirstGlobal() { + Node first = checkStateNotNull(globalHead.nextNode); + if (first == globalTail) { + return null; + } + unlinkNode(first); + return first; + } + + /** + * Remove and Return QueuedWork for the computationId, keyGroup in the FIFO order. Returns null, + * if there are no matches. + * + * @param keyGroup should not be equal to KeyGroup.DEFAULT + */ + public @Nullable QueuedWork pollWork(String computationId, Work.KeyGroup keyGroup) { + checkArgument(computationId != null && keyGroup != null && !keyGroup.equals(KeyGroup.DEFAULT)); + QueueKey key = new QueueKey(computationId, keyGroup); + lock.lock(); + try { + KeyGroupWorkList keyGroupWorkList = keyGroupQueueMap.get(key); + if (keyGroupWorkList == null || keyGroupWorkList.isEmpty()) { + return null; + } + + // Retrieve the first pending task for this computation and keyGroup in O(1) + Node firstNode = checkStateNotNull(keyGroupWorkList.head.nextKeyGroupNode); + if (firstNode == keyGroupWorkList.tail) { + return null; + } + unlinkNode(firstNode); + + return (QueuedWork) firstNode.task; + } finally { + lock.unlock(); + } + } + + @Override + public boolean offer(@NonNull Runnable runnable) { + Node node = new Node(checkStateNotNull(runnable)); + lock.lock(); + try { + // Append to global list tail + Node lastG = checkStateNotNull(globalTail.prevNode); + node.prevNode = lastG; + node.nextNode = globalTail; + lastG.nextNode = node; + globalTail.prevNode = node; + + // Append to key group list if applicable + String compId = node.computationId; + Work.KeyGroup keyGroup = node.keyGroup; + if (compId != null && keyGroup != null && !keyGroup.equals(KeyGroup.DEFAULT)) { + QueueKey key = new QueueKey(compId, keyGroup); + KeyGroupWorkList keyGroupWorkList = + keyGroupQueueMap.computeIfAbsent(key, k -> new KeyGroupWorkList()); + keyGroupWorkList.append(node); + node.keyGroupList = keyGroupWorkList; + } + + ++size; + notEmpty.signal(); + return true; + } finally { + lock.unlock(); + } + } + + @Override + public void put(Runnable e) throws InterruptedException { + offer(e); // Unbounded queue + } + + @Override + public boolean offer(Runnable e, long timeout, TimeUnit unit) throws InterruptedException { + return offer(e); // Unbounded queue + } + + @Override + public @Nullable Runnable poll() { + lock.lock(); + try { + @Nullable Node node = removeFirstGlobal(); + return (node != null) ? node.task : null; + } finally { + lock.unlock(); + } + } + + @Override + public Runnable take() throws InterruptedException { + lock.lockInterruptibly(); + try { + while (size == 0) { + notEmpty.await(); + } + @Nullable Node node = removeFirstGlobal(); + checkStateNotNull(node, "Queue is empty but size was " + size); + return node.task; + } finally { + lock.unlock(); + } + } + + @Override + public @Nullable Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { + long nanos = unit.toNanos(timeout); + lock.lockInterruptibly(); + try { + while (size == 0) { + if (nanos <= 0) { + return null; + } + nanos = notEmpty.awaitNanos(nanos); + } + @Nullable Node node = removeFirstGlobal(); + return (node != null) ? node.task : null; + } finally { + lock.unlock(); + } + } + + @Override + public @Nullable Runnable peek() { + lock.lock(); + try { + Node first = checkStateNotNull(globalHead.nextNode); + if (first == globalTail) { + return null; + } + return first.task; + } finally { + lock.unlock(); + } + } + + @Override + public int size() { + lock.lock(); + try { + return size; + } finally { + lock.unlock(); + } + } + + @Override + public boolean isEmpty() { + lock.lock(); + try { + return size == 0; + } finally { + lock.unlock(); + } + } + + @Override + public boolean remove(Object o) { + if (o == null) return false; + lock.lock(); + try { + // Walk the global queue in O(N) to find and unlink the node + Node curr = checkStateNotNull(globalHead.nextNode); + while (curr != globalTail) { + if (o.equals(curr.task)) { + unlinkNode(curr); + return true; + } + curr = checkStateNotNull(curr.nextNode); + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + public boolean contains(Object o) { + if (o == null) return false; + lock.lock(); + try { + Node curr = checkStateNotNull(globalHead.nextNode); + while (curr != globalTail) { + if (o.equals(curr.task)) { + return true; + } + curr = checkStateNotNull(curr.nextNode); + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + public int remainingCapacity() { + return Integer.MAX_VALUE; + } + + @Override + public int drainTo(Collection c) { + return drainTo(c, Integer.MAX_VALUE); + } + + @Override + public int drainTo(Collection c, int maxElements) { + if (c == null) throw new NullPointerException(); + if (c == this) throw new IllegalArgumentException(); + if (maxElements <= 0) return 0; + lock.lock(); + try { + int added = 0; + Node curr = checkStateNotNull(globalHead.nextNode); + while (curr != globalTail && added < maxElements) { + Node next = checkStateNotNull(curr.nextNode); + unlinkNode(curr); + Runnable task = curr.task; + c.add(task); + ++added; + curr = next; + } + return added; + } finally { + lock.unlock(); + } + } + + @Override + public void clear() { + lock.lock(); + try { + Node curr = checkStateNotNull(globalHead.nextNode); + while (curr != globalTail) { + Node next = checkStateNotNull(curr.nextNode); + unlinkNode(curr); + curr = next; + } + } finally { + lock.unlock(); + } + } + + @Override + public Iterator iterator() { + lock.lock(); + try { + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(size); + Node curr = checkStateNotNull(globalHead.nextNode); + while (curr != globalTail) { + builder.add(curr.task); + curr = checkStateNotNull(curr.nextNode); + } + return builder.build().iterator(); + } finally { + lock.unlock(); + } + } + + static final class QueueKey { + private final String computationId; + private final Work.KeyGroup keyGroup; + + QueueKey(String computationId, Work.KeyGroup keyGroup) { + this.computationId = computationId; + this.keyGroup = keyGroup; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof QueueKey)) { + return false; + } + QueueKey other = (QueueKey) o; + return computationId.equals(other.computationId) && keyGroup.equals(other.keyGroup); + } + + @Override + public int hashCode() { + return Objects.hash(computationId, keyGroup); + } + + @Override + public String toString() { + return "QueueKey{" + + "computationId='" + + computationId + + '\'' + + ", keyGroup=" + + keyGroup + + '}'; + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java index d58f20076994..5bcdffcc2564 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java @@ -3036,7 +3036,8 @@ public void testMaxThreadMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3097,7 +3098,8 @@ public void testActiveThreadMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3167,7 +3169,8 @@ public void testOutstandingBytesMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3241,7 +3244,8 @@ public void testOutstandingBundlesMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java index 55fe82c7163c..a98102751fb2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -33,6 +35,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.Watermarks; import org.apache.beam.runners.dataflow.worker.streaming.Work; import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.BoundedQueueExecutorWorkHandleImpl; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; @@ -66,13 +69,30 @@ public static Collection useFairMonitor() { @Rule public transient Timeout globalTimeout = Timeout.seconds(300); private BoundedQueueExecutor executor; + private static final Work.KeyGroup DEFAULT_KEY_GROUP = Work.KeyGroup.create(1, 2); + private static ExecutableWork createWork(Consumer executeWorkFn) { + return createWorkWithCompId("computationId", executeWorkFn); + } + + private static ExecutableWork createWorkWithCompId( + String computationId, Consumer executeWorkFn) { + return createWorkWithCompIdAndKeyGroup(computationId, DEFAULT_KEY_GROUP, executeWorkFn); + } + + private static ExecutableWork createWorkWithCompIdAndKeyGroup( + String computationId, Work.KeyGroup keyGroup, Consumer executeWorkFn) { WorkItem workItem = WorkItem.newBuilder() .setKey(ByteString.EMPTY) .setShardingKey(1) .setWorkToken(33) .setCacheToken(1) + .setKeyGroup( + Windmill.Uint128Proto.newBuilder() + .setHigh(keyGroup.high()) + .setLow(keyGroup.low()) + .build()) .build(); return ExecutableWork.create( Work.create( @@ -80,10 +100,7 @@ private static ExecutableWork createWork(Consumer executeWorkFn) { workItem.getSerializedSize(), Watermarks.builder().setInputDataWatermark(Instant.now()).build(), Work.createProcessingContext( - "computationId", - new FakeGetDataClient(), - ignored -> {}, - mock(HeartbeatSender.class)), + computationId, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), false, Instant::now), (work, handle) -> { @@ -116,7 +133,8 @@ public void setUp() { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - useFairMonitor); + useFairMonitor, + /*useKeyGroupWorkQueue=*/ false); } @Test @@ -413,4 +431,126 @@ public void testRenderSummaryHtml() { + "Work Queue Bytes: 0/10000000
    /n"; assertEquals(expectedSummaryHtml, executor.summaryHtml()); } + + @Test + public void testPollWork() throws Exception { + // Create separate BoundedQueueExecutor with 1 thread so we can block it easily + BoundedQueueExecutor testExecutor = + new BoundedQueueExecutor( + 1, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("testStealing-%d").setDaemon(true).build(), + useFairMonitor, + /*useKeyGroupWorkQueue=*/ true); + + // 1. Create blocker task to occupy the worker thread + CountDownLatch blockerStart = new CountDownLatch(1); + CountDownLatch blockerStop = new CountDownLatch(1); + ExecutableWork blockerWork = + createWorkWithCompIdAndKeyGroup( + "blockerComp", + DEFAULT_KEY_GROUP, + ignored -> { + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 0); + blockerStart.await(); + + // 2. Create two distinct key groups + Work.KeyGroup keyGroup1 = Work.KeyGroup.create(1, 1); + Work.KeyGroup keyGroup2 = Work.KeyGroup.create(1, 2); + + // Create executable tasks + CountDownLatch targetStart = new CountDownLatch(1); + ExecutableWork work1 = createWorkWithCompIdAndKeyGroup("compA", keyGroup1, ignored -> {}); + ExecutableWork work2 = + createWorkWithCompIdAndKeyGroup( + "compA", + keyGroup2, + ignored -> { + targetStart.countDown(); + }); + + // Enqueue tasks (they will wait in the queue because the thread is blocked) + testExecutor.execute(work1, 100); + testExecutor.execute(work2, 150); + + // Total outstanding elements must be 3 (blocker + work1 + work2) + assertEquals(3, testExecutor.elementsOutstanding()); + + // Steal work2 using pollWork with compA and keyGroup2 + try (BoundedQueueExecutorWorkHandleImpl stealHandle = testExecutor.createBudgetHandle(0, 0L)) { + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup2, stealHandle); + assertNotNull(stolen); + assertEquals(work2, stolen); + + // Run the stolen task + stolen.run(stealHandle); + targetStart.await(); + } + + // Steal work1 using pollWork with compA and keyGroup1 + try (BoundedQueueExecutorWorkHandleImpl stealHandle = testExecutor.createBudgetHandle(0, 0L)) { + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup1, stealHandle); + assertNotNull(stolen); + assertEquals(work1, stolen); + } + + // Unblock the blocker and shut down + blockerStop.countDown(); + testExecutor.shutdown(); + } + + @Test + public void testPollWorkWithLinkedBlockingQueue() throws Exception { + BoundedQueueExecutor testExecutor = + new BoundedQueueExecutor( + 1, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("testLinkedQueue-%d").setDaemon(true).build(), + useFairMonitor, + /* useKeyGroupWorkQueue= */ false); + + CountDownLatch blockerStart = new CountDownLatch(1); + CountDownLatch blockerStop = new CountDownLatch(1); + ExecutableWork blockerWork = + createWorkWithCompIdAndKeyGroup( + "blockerComp", + DEFAULT_KEY_GROUP, + ignored -> { + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 0); + blockerStart.await(); + + Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1); + ExecutableWork work = createWorkWithCompIdAndKeyGroup("compA", keyGroup, ignored -> {}); + testExecutor.execute(work, 100); + + try (BoundedQueueExecutorWorkHandleImpl stealHandle = testExecutor.createBudgetHandle(0, 0L)) { + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup, stealHandle); + assertNull(stolen); + } + + blockerStop.countDown(); + testExecutor.shutdown(); + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java new file mode 100644 index 000000000000..994aa2030f3f --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import java.lang.Thread.State; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; +import org.apache.beam.runners.dataflow.worker.streaming.Watermarks; +import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.QueuedWork; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; +import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; +import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; +import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class KeyGroupWorkQueueTest { + + @Parameters(name = "fairQueue={0}") + public static Iterable data() { + return Arrays.asList(new Object[][] {{true}, {false}}); + } + + @Parameterized.Parameter public boolean fairQueue; + + private BoundedQueueExecutor executor; + + @Before + public void setUp() { + executor = + new BoundedQueueExecutor( + 2, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("Test-%d").setDaemon(true).build(), + fairQueue, + /*useKeyGroupWorkQueue=*/ true); + } + + private static final Work.KeyGroup TEST_KEY_GROUP = Work.KeyGroup.create(1, 2); + + private QueuedWork createQueuedWork(String computationId, long workBytes) { + return createQueuedWork(computationId, TEST_KEY_GROUP, workBytes); + } + + private QueuedWork createQueuedWork( + String computationId, Work.@Nullable KeyGroup keyGroup, long workBytes) { + WorkItem.Builder workItemBuilder = + WorkItem.newBuilder() + .setKey(ByteString.EMPTY) + .setShardingKey(1) + .setWorkToken(33) + .setCacheToken(1); + if (keyGroup != null) { + workItemBuilder.setKeyGroup( + org.apache.beam.runners.dataflow.worker.windmill.Windmill.Uint128Proto.newBuilder() + .setHigh(keyGroup.high()) + .setLow(keyGroup.low()) + .build()); + } + WorkItem workItem = workItemBuilder.build(); + ExecutableWork work = + ExecutableWork.create( + Work.create( + workItem, + workItem.getSerializedSize(), + Watermarks.builder().setInputDataWatermark(Instant.now()).build(), + Work.createProcessingContext( + computationId, + new FakeGetDataClient(), + ignored -> {}, + mock(HeartbeatSender.class)), + false, + Instant::now), + (w, h) -> {}); + return new QueuedWork(work, executor.createBudgetHandle(1, workBytes)); + } + + private static class NoOpRunnable implements Runnable { + final String id; + + NoOpRunnable(String id) { + this.id = id; + } + + @Override + public void run() {} + + @Override + public String toString() { + return "NoOpRunnable(" + id + ")"; + } + } + + @Test + public void testBasicOfferAndPoll() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + assertTrue(queue.isEmpty()); + assertEquals(0, queue.size()); + + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + + assertTrue(queue.offer(task1)); + assertTrue(queue.offer(task2)); + assertEquals(2, queue.size()); + + assertEquals(task1, queue.poll()); + assertEquals(task2, queue.poll()); + assertNull(queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + public void testRemove() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + + queue.offer(task1); + queue.offer(task2); + + assertTrue(queue.remove(task1)); + assertEquals(1, queue.size()); + assertEquals(task2, queue.poll()); + assertFalse(queue.remove(task1)); // Already gone + } + + @Test + public void testDrainTo() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + queue.offer(task1); + queue.offer(task2); + + List drained = new ArrayList<>(); + assertEquals(2, queue.drainTo(drained)); + assertEquals(2, drained.size()); + assertEquals(task1, drained.get(0)); + assertEquals(task2, drained.get(1)); + assertTrue(queue.isEmpty()); + } + + @Test + public void testIteratorSafeTraversalAndImmutable() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + queue.offer(task1); + queue.offer(task2); + + Iterator it = queue.iterator(); + assertTrue(it.hasNext()); + assertEquals(task1, it.next()); + assertTrue(it.hasNext()); + assertEquals(task2, it.next()); + assertFalse(it.hasNext()); + + // Assert that mutating the iterator throws UnsupportedOperationException + it = queue.iterator(); + assertTrue(it.hasNext()); + it.next(); + try { + it.remove(); + fail("Iterator must be immutable"); + } catch (UnsupportedOperationException e) { + // Expected + } + } + + @Test + public void testPollWorkTargeted() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + + QueuedWork workA1 = createQueuedWork("compA", 100); + QueuedWork workB1 = createQueuedWork("compB", 200); + QueuedWork workA2 = createQueuedWork("compA", 150); + + queue.offer(workA1); + queue.offer(workB1); + queue.offer(workA2); + + assertEquals(3, queue.size()); + assertFalse(queue.isEmpty()); + + // Targeted poll A + QueuedWork polledA1 = queue.pollWork("compA", TEST_KEY_GROUP); + assertNotNull(polledA1); + assertEquals("compA", polledA1.getWork().getComputationId()); + assertEquals(100, polledA1.getHandle().bytes()); + assertEquals(2, queue.size()); + assertFalse(queue.isEmpty()); + + // Poll next should be B1 (since A1 was stolen, B1 is now first global) + assertEquals(workB1, queue.poll()); + assertEquals(1, queue.size()); + assertFalse(queue.isEmpty()); + + // Last should be A2 + assertEquals(workA2, queue.poll()); + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + + assertEquals(null, queue.poll()); + } + + @Test + public void testConcurrentStress() throws InterruptedException, ExecutionException { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + int producerThreads = 4; + int consumerThreads = 4; + int tasksPerProducer = 1000; + int totalTasks = producerThreads * tasksPerProducer; + Runnable poisonPill = + new Runnable() { + @Override + public void run() {} + + @Override + public String toString() { + return "POISON_PILL"; + } + }; + + ExecutorService executorService = + Executors.newFixedThreadPool(producerThreads + consumerThreads); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch producersDoneLatch = new CountDownLatch(producerThreads); + CountDownLatch consumersDoneLatch = new CountDownLatch(consumerThreads); + CountDownLatch consumedLatch = new CountDownLatch(totalTasks); + List> futures = new ArrayList<>(); + + // Start consumers + for (int i = 0; i < consumerThreads; i++) { + int consumerId = i; + futures.add( + executorService.submit( + () -> { + try { + startLatch.await(); + int iteration = consumerId % 4; + while (true) { + int strategy = iteration; + iteration = (iteration + 1) % 4; + Runnable task = null; + if (strategy == 0) { + String compId = "comp-" + (consumedLatch.getCount() % 5); + task = queue.pollWork(compId, TEST_KEY_GROUP); + } else if (strategy == 1) { + task = queue.poll(); + } else if (strategy == 2) { + task = queue.poll(10, TimeUnit.MICROSECONDS); + } else if (strategy == 3) { + task = queue.take(); + } + + if (task == poisonPill) { + break; + } + if (task != null) { + consumedLatch.countDown(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + consumersDoneLatch.countDown(); + } + })); + } + + // Start producers + for (int i = 0; i < producerThreads; i++) { + futures.add( + executorService.submit( + () -> { + try { + startLatch.await(); + for (int j = 0; j < tasksPerProducer; j++) { + String compId = "comp-" + (j % 5); + queue.offer(createQueuedWork(compId, 10)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + producersDoneLatch.countDown(); + } + })); + } + + // Release the start latch to start the test + startLatch.countDown(); + + // Wait for all tasks to be consumed + assertTrue(consumedLatch.await(30, TimeUnit.SECONDS)); + + // Send poison pills to stop all consumers + for (int i = 0; i < consumerThreads; i++) { + queue.offer(poisonPill); + } + + // Wait for consumers to finish + assertTrue(consumersDoneLatch.await(30, TimeUnit.SECONDS)); + // Wait for producers to finish + assertTrue(producersDoneLatch.await(30, TimeUnit.SECONDS)); + + // Check for exceptions in threads + for (Future future : futures) { + future.get(); + } + + executorService.shutdown(); + assertTrue(executorService.awaitTermination(30, TimeUnit.SECONDS)); + + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + } + + @Test + public void testTakeBlocksAndWakesUp() throws InterruptedException { + final KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + final NoOpRunnable task = new NoOpRunnable("take-task"); + final AtomicReference result = new AtomicReference<>(); + final CountDownLatch started = new CountDownLatch(1); + final CountDownLatch finished = new CountDownLatch(1); + + Thread t = + new Thread( + () -> { + started.countDown(); + try { + result.set(queue.take()); + } catch (InterruptedException e) { + // Ignore + } finally { + finished.countDown(); + } + }); + t.setDaemon(true); + t.start(); + + assertTrue(started.await(30, TimeUnit.SECONDS)); + waitForThreadState(t, State.WAITING); + + queue.offer(task); + + assertTrue(finished.await(30, TimeUnit.SECONDS)); + assertEquals(task, result.get()); + } + + @Test + public void testPollWithTimeout() throws InterruptedException { + final KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + final NoOpRunnable task = new NoOpRunnable("poll-task"); + final AtomicReference result = new AtomicReference<>(); + final CountDownLatch started = new CountDownLatch(1); + final CountDownLatch finished = new CountDownLatch(1); + + // 1. Verify timeout returns null + Thread t1 = + new Thread( + () -> { + started.countDown(); + try { + result.set(queue.poll(500, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + // Ignore + } finally { + finished.countDown(); + } + }); + t1.setDaemon(true); + t1.start(); + + assertTrue(started.await(30, TimeUnit.SECONDS)); + waitForThreadState(t1, State.TIMED_WAITING); + + assertTrue(finished.await(30, TimeUnit.SECONDS)); + assertNull(result.get()); + + // 2. Verify timed poll receives task offered concurrently + final CountDownLatch started2 = new CountDownLatch(1); + final CountDownLatch finished2 = new CountDownLatch(1); + final AtomicReference result2 = new AtomicReference<>(); + + Thread t2 = + new Thread( + () -> { + started2.countDown(); + try { + result2.set(queue.poll(2, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + // Ignore + } finally { + finished2.countDown(); + } + }); + t2.setDaemon(true); + t2.start(); + + assertTrue(started2.await(30, TimeUnit.SECONDS)); + waitForThreadState(t2, State.TIMED_WAITING); + + queue.offer(task); + + assertTrue(finished2.await(30, TimeUnit.SECONDS)); + assertEquals(task, result2.get()); + } + + @Test + public void testPollWorkWithKeyGroup() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + + Work.KeyGroup keyGroup1 = Work.KeyGroup.create(1, 1); + Work.KeyGroup keyGroup2 = Work.KeyGroup.create(1, 2); + Work.KeyGroup keyGroupNotExist = Work.KeyGroup.create(3, 4); + + QueuedWork workA1 = createQueuedWork("compA", keyGroup1, 100); + QueuedWork workA2 = createQueuedWork("compA", keyGroup2, 150); + + queue.offer(workA1); + queue.offer(workA2); + + assertEquals(2, queue.size()); + + QueuedWork polledNotExist = queue.pollWork("compA", keyGroupNotExist); + assertNull(polledNotExist); + assertEquals(2, queue.size()); + + // Poll with keyGroup2 first - should return workA2 + QueuedWork polledA2 = queue.pollWork("compA", keyGroup2); + assertNotNull(polledA2); + assertEquals(workA2, polledA2); + assertEquals(1, queue.size()); + + // Poll with keyGroup2 again - should return null + assertNull(queue.pollWork("compA", keyGroup2)); + + // Poll with keyGroup1 - should return workA1 + QueuedWork polledA1 = queue.pollWork("compA", keyGroup1); + assertNotNull(polledA1); + assertEquals(workA1, polledA1); + assertTrue(queue.isEmpty()); + + polledNotExist = queue.pollWork("compA", keyGroupNotExist); + assertNull(polledNotExist); + assertTrue(queue.isEmpty()); + } + + private void waitForThreadState(Thread t, State state) throws InterruptedException { + long timeoutMs = 30000; + long start = System.currentTimeMillis(); + while (t.getState() != state) { + if (System.currentTimeMillis() - start > timeoutMs) { + fail("Thread did not reach " + state + " state within " + timeoutMs + "ms"); + } + Thread.sleep(1); + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java index 07b4b14fd115..ef0d8e434858 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingCommitFinalizerTest.java @@ -62,7 +62,8 @@ public void setUp() { .setNameFormat("FinalizationCallback-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); cleanupExecutor = Executors.newScheduledThreadPool( diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java index 51bd4816b031..0610ed44c27f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java @@ -64,7 +64,8 @@ private static WorkFailureProcessor createWorkFailureProcessor( .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); return WorkFailureProcessor.forTesting(workExecutor, failureTracker, Optional::empty, clock, 0); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java index 88a82c6f76b6..f32282056e4f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java @@ -80,7 +80,8 @@ private static BoundedQueueExecutor workExecutor() { 1, 10000000, new ThreadFactoryBuilder().setNameFormat("DataflowWorkUnits-%d").setDaemon(true).build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); } private static ComputationState createComputationState(int computationIdSuffix) { diff --git a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto index 1da7ef9be8bb..aaa09c105fc3 100644 --- a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto +++ b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto @@ -421,6 +421,11 @@ message WatermarkHold { optional string state_family = 4; } +message Uint128Proto { + required fixed64 high = 1; + required fixed64 low = 2; +} + // Proto describing a hot key detected on a given WorkItem. message HotKeyInfo { // The age of the hot key measured from when it was first detected. @@ -448,6 +453,8 @@ message WorkItem { // present, this field includes metadata associated with any hot key. optional HotKeyInfo hot_key_info = 11; + optional Uint128Proto key_group = 18; + reserved 12, 13, 14, 15, 16; } From e1320d3a27cf81d8ea422d0000b92dba356c4622 Mon Sep 17 00:00:00 2001 From: Drew Stevens <43360753+googledrew@users.noreply.github.com> Date: Mon, 8 Jun 2026 04:14:32 -0700 Subject: [PATCH 318/490] Update SpannerIO.java - comment documentation (#38492) Spanner supports 80,000 mutations per transacton https://docs.cloud.google.com/spanner/quotas#limits_for_creating_reading_updating_and_deleting_data:~:text=Mutations%20per%20commit Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java index cccfced08218..d271e763aac5 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java @@ -311,10 +311,10 @@ * *

    Note that the maximum - * size of a single transaction is 20,000 mutated cells - including cells in indexes. If you - * have a large number of indexes and are getting exceptions with message: INVALID_ARGUMENT: The - * transaction contains too many mutations you will need to specify a smaller number of {@code - * MaxNumMutations}. + * number of mutations in a single transaction is 80,000 mutations - including mutations in + * indexes. If you have a large number of indexes and are getting exceptions with message: + * INVALID_ARGUMENT: The transaction contains too many mutations you will need to specify a + * smaller number of {@code MaxNumMutations}. * *

    The batches written are obtained from by grouping enough {@link Mutation Mutations} from the * Bundle provided by Beam to form several batches. This group of {@link Mutation Mutations} is then From d3dece45c90aec690d0b954a76fbecb0cb41db94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:36:07 -0400 Subject: [PATCH 319/490] Bump github.com/aws/smithy-go from 1.27.1 to 1.27.2 in /sdks (#38841) Bumps [github.com/aws/smithy-go](https://github.com/aws/smithy-go) from 1.27.1 to 1.27.2. - [Release notes](https://github.com/aws/smithy-go/releases) - [Changelog](https://github.com/aws/smithy-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/smithy-go/compare/v1.27.1...v1.27.2) --- updated-dependencies: - dependency-name: github.com/aws/smithy-go dependency-version: 1.27.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 7cbe318bb8f9..2151cae2f1e0 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -37,7 +37,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.22 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 - github.com/aws/smithy-go v1.27.1 + github.com/aws/smithy-go v1.27.2 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-sql-driver/mysql v1.10.0 diff --git a/sdks/go.sum b/sdks/go.sum index 0b5cc9db4bac..d945d9e669d8 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -274,8 +274,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 h1:RTO7mmGyedgnNmcPh3yQizNfc6GK github.com/aws/aws-sdk-go-v2/service/sts v1.43.2/go.mod h1:fBhUZXDin9YYqhcpOMjIcpdik25rVwWyxLdPH1RZd9s= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= -github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= +github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= From 58a030e81aa34dc561405b4f562e86f8b656a935 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:41:57 -0400 Subject: [PATCH 320/490] [Experimental] Fix Yaml Xlang Test Timeout (#38798) * [Experiment] Try single pip call + uv usage * update workflow for 310 ml deps * sickbay the test suite * formatting --- .../beam_PreCommit_Yaml_Xlang_Direct.json | 2 +- .../workflows/beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- .../org/apache/beam/gradle/BeamModulePlugin.groovy | 12 +++++------- sdks/python/apache_beam/yaml/integration_tests.py | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/trigger_files/beam_PreCommit_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PreCommit_Yaml_Xlang_Direct.json index 616d37428c01..8c604b0a135c 100644 --- a/.github/trigger_files/beam_PreCommit_Yaml_Xlang_Direct.json +++ b/.github/trigger_files/beam_PreCommit_Yaml_Xlang_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 1 + "revision": 2 } diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 34a1af741f74..21281b8cd3e5 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -91,7 +91,7 @@ jobs: - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=p310_ml_test,yaml - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 9f34fb54e47b..bff5aee4f6a1 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -3139,15 +3139,13 @@ class BeamModulePlugin implements Plugin { def distTarBall = "${pythonRootDir}/build/apache-beam.tar.gz" def packages = "gcp,test,aws,azure,dataframe" def extra = project.findProperty('beamPythonExtra') + def installTargets = "${distTarBall}[${packages}]" + if (extra) { + installTargets = "${distTarBall}[${packages},${extra}]" + } project.exec { executable 'sh' - args '-c', ". ${project.ext.envdir}/bin/activate && pip install --pre --retries 10 ${distTarBall}[${packages}]" - } - if (extra) { - project.exec { - executable 'sh' - args '-c', ". ${project.ext.envdir}/bin/activate && pip install --pre --retries 10 ${distTarBall}[${extra}]" - } + args '-c', ". ${project.ext.envdir}/bin/activate && pip install uv && uv pip install --pre ${installTargets}" } } } diff --git a/sdks/python/apache_beam/yaml/integration_tests.py b/sdks/python/apache_beam/yaml/integration_tests.py index 2d0b2787fc9b..150c0ca86254 100644 --- a/sdks/python/apache_beam/yaml/integration_tests.py +++ b/sdks/python/apache_beam/yaml/integration_tests.py @@ -790,6 +790,11 @@ def test(self, providers=providers): # default arg to capture loop value yield f'test_{suffix}', test +_SICKBAY_TESTS = { + 'ml_transform.yaml': 'Requires broken TFT dependency types (e.g. ScaleTo01)', +} + + def parse_test_files(filepattern): """Parses YAML test files and dynamically creates test cases. @@ -810,13 +815,18 @@ def parse_test_files(filepattern): """ for path in glob.glob(filepattern): with open(path) as fin: - suite_name = os.path.splitext(os.path.basename(path))[0].title().replace( + filename = os.path.basename(path) + suite_name = os.path.splitext(filename)[0].title().replace( '-', '') + 'Test' print(path, suite_name) methods = dict( create_test_methods( yaml.load(fin, Loader=yaml_transform.SafeLineLoader))) - globals()[suite_name] = type(suite_name, (unittest.TestCase, ), methods) + suite_class = type(suite_name, (unittest.TestCase, ), methods) + if filename in _SICKBAY_TESTS: + suite_class = unittest.skip(f"Sickbayed: {_SICKBAY_TESTS[filename]}")( + suite_class) + globals()[suite_name] = suite_class # Logging setups From 450d72e5fa5d1de016c52dae734c2416efccd950 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:33:00 -0400 Subject: [PATCH 321/490] Fix parsing of dataflow api endpoint URLs to remove trailing slash (#38847) * Fix parsing of dataflow api endpoint URLs to remove trailing slash * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * gemini existance suggestion --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- ...ommit_Python_ValidatesRunner_Dataflow.json | 2 +- .../runners/dataflow/internal/apiclient.py | 14 ++++---- .../dataflow/internal/apiclient_test.py | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json index c537844dc84a..e0266d62f2e0 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 3 + "modification": 4 } diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index fdf2e0ea03e3..4755aea0bc2c 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -519,12 +519,14 @@ def __init__(self, options, root_staging_location=None): client_options = None transport = None if self.google_cloud_options.dataflow_endpoint: - endpoint = self.google_cloud_options.dataflow_endpoint - if 'localhost' in endpoint or 'sandbox' in endpoint: - transport = 'rest' - else: - endpoint = re.sub('^https?://', '', endpoint) - client_options = client_options_lib.ClientOptions(api_endpoint=endpoint) + endpoint = self.google_cloud_options.dataflow_endpoint.strip() + if endpoint: + if 'localhost' in endpoint or 'sandbox' in endpoint: + transport = 'rest' + else: + endpoint = re.sub('^https?://', '', endpoint) + endpoint = endpoint.rstrip('/') + client_options = client_options_lib.ClientOptions(api_endpoint=endpoint) self._jobs_client = dataflow.JobsV1Beta3Client( credentials=gapic_credentials, diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py index b34f06b64df4..12c51d305145 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py @@ -1298,6 +1298,42 @@ def test_template_file_generation_with_upload_graph(self): self.assertFalse(template_obj.get('steps')) self.assertTrue(template_obj['stepsLocation']) + def test_dataflow_endpoint_clean(self): + endpoints_and_expectations = [ + # (input_endpoint, expected_endpoint, expected_transport) + ('https://dataflow.googleapis.com/', 'dataflow.googleapis.com', None), + ('https://dataflow.googleapis.com ', 'dataflow.googleapis.com', None), + ('dataflow.googleapis.com/', 'dataflow.googleapis.com', None), + ('http://localhost:8080/', 'http://localhost:8080', 'rest'), + ('localhost:8080/', 'localhost:8080', 'rest'), + ] + + for input_ep, expected_ep, expected_transport in endpoints_and_expectations: + pipeline_options = PipelineOptions([ + '--project', + 'test-project', + '--temp_location', + 'gs://test-location/temp', + '--dataflow_endpoint', + input_ep, + '--no_auth', + ]) + with mock.patch('apache_beam.runners.dataflow.internal.apiclient.dataflow' + '.JobsV1Beta3Client') as mock_jobs: + with mock.patch( + 'apache_beam.runners.dataflow.internal.apiclient.dataflow' + '.MessagesV1Beta3Client'): + with mock.patch( + 'apache_beam.runners.dataflow.internal.apiclient.dataflow' + '.MetricsV1Beta3Client'): + apiclient.DataflowApplicationClient(pipeline_options) + mock_jobs.assert_called_once() + called_kwargs = mock_jobs.call_args.kwargs + client_opts = called_kwargs.get('client_options') + self.assertIsNotNone(client_opts) + self.assertEqual(client_opts.api_endpoint, expected_ep) + self.assertEqual(called_kwargs.get('transport'), expected_transport) + def test_stage_resources(self): pipeline_options = PipelineOptions([ '--temp_location', From 089ba44334cb24e6c84b8a142f05a5f4942bf9be Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Mon, 8 Jun 2026 21:08:23 +0200 Subject: [PATCH 322/490] Update Gemini text classification to gemini-2.5-flash (#38839) --- .github/trigger_files/beam_PostCommit_Python.json | 2 +- .../apache_beam/examples/inference/gemini_image_generation.py | 4 ++-- .../examples/inference/gemini_text_classification.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 4f79e635cc02..b12889937d52 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "38069", - "modification": 43 + "modification": 44 } diff --git a/sdks/python/apache_beam/examples/inference/gemini_image_generation.py b/sdks/python/apache_beam/examples/inference/gemini_image_generation.py index 29b2d562e634..069b3fca3a94 100644 --- a/sdks/python/apache_beam/examples/inference/gemini_image_generation.py +++ b/sdks/python/apache_beam/examples/inference/gemini_image_generation.py @@ -17,8 +17,8 @@ """ A sample pipeline using the RunInference API to classify text using an LLM. This pipeline creates a set of prompts and sends it to a Gemini service then -returns the predictions from the classifier model. This example uses the -gemini-2.0-flash-001 model. +returns generated images from the model. This example uses the +gemini-2.5-flash-image model. """ import argparse diff --git a/sdks/python/apache_beam/examples/inference/gemini_text_classification.py b/sdks/python/apache_beam/examples/inference/gemini_text_classification.py index 0072dfc50b2f..55b603bc2ab5 100644 --- a/sdks/python/apache_beam/examples/inference/gemini_text_classification.py +++ b/sdks/python/apache_beam/examples/inference/gemini_text_classification.py @@ -18,7 +18,7 @@ """ A sample pipeline using the RunInference API to classify text using an LLM. This pipeline creates a set of prompts and sends it to a Gemini service then returns the predictions from the classifier model. This example uses the -gemini-2.0-flash-001 model. +gemini-2.5-flash model. """ import argparse @@ -88,7 +88,7 @@ def run( pipeline_options = PipelineOptions(pipeline_args) pipeline_options.view_as(SetupOptions).save_main_session = save_main_session model_handler = GeminiModelHandler( - model_name='gemini-2.0-flash-001', + model_name='gemini-2.5-flash', request_fn=generate_from_string, api_key=known_args.api_key, project=known_args.project, From 4581922b1d45f0312dbc26ccbb009b4794809715 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Mon, 8 Jun 2026 21:09:01 +0200 Subject: [PATCH 323/490] fix flaky Arm postcommit test collection on Python 3.14 (#38844) * fix flaky Arm postcommit test collection on Python 3.14 * fix formatter --------- Co-authored-by: Yi Hu --- .../trigger_files/beam_PostCommit_Python.json | 2 +- .../apache_beam/dataframe/frames_test.py | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index b12889937d52..213813294758 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "38069", - "modification": 44 + "modification": 48 } diff --git a/sdks/python/apache_beam/dataframe/frames_test.py b/sdks/python/apache_beam/dataframe/frames_test.py index 2e560c013417..bb46f0e29be1 100644 --- a/sdks/python/apache_beam/dataframe/frames_test.py +++ b/sdks/python/apache_beam/dataframe/frames_test.py @@ -2955,19 +2955,26 @@ def test_allow_non_parallel_nesting(self): self._use_non_parallel_operation() +_CONSTRUCTION_TIME_TEST_COLUMNS = [ + 'str_col', 'int_col', 'flt_col', 'cat_col', 'datetime_col' +] + + class ConstructionTimeTest(unittest.TestCase): """Tests for operations that can be executed eagerly.""" - DF = pd.DataFrame({ - 'str_col': ['foo', 'bar'] * 3, - 'int_col': [1, 2] * 3, - 'flt_col': [1.1, 2.2] * 3, - 'cat_col': pd.Series(list('aabbca'), dtype="category"), - 'datetime_col': pd.Series( - pd.date_range( - '1/1/2000', periods=6, freq='m', tz='America/Los_Angeles')) - }) - DEFERRED_DF = frame_base.DeferredFrame.wrap( - expressions.PlaceholderExpression(DF.iloc[:0])) + @classmethod + def setUpClass(cls): + cls.DF = pd.DataFrame({ + 'str_col': ['foo', 'bar'] * 3, + 'int_col': [1, 2] * 3, + 'flt_col': [1.1, 2.2] * 3, + 'cat_col': pd.Series(list('aabbca'), dtype="category"), + 'datetime_col': pd.Series( + pd.date_range( + '1/1/2000', periods=6, freq='m', tz='America/Los_Angeles')) + }) + cls.DEFERRED_DF = frame_base.DeferredFrame.wrap( + expressions.PlaceholderExpression(cls.DF.iloc[:0])) def _run_test(self, fn): expected = fn(self.DF) @@ -2982,11 +2989,11 @@ def _run_test(self, fn): else: self.assertEqual(expected, actual) - @parameterized.expand(DF.columns) + @parameterized.expand(_CONSTRUCTION_TIME_TEST_COLUMNS) def test_series_name(self, col_name): self._run_test(lambda df: df[col_name].name) - @parameterized.expand(DF.columns) + @parameterized.expand(_CONSTRUCTION_TIME_TEST_COLUMNS) def test_series_dtype(self, col_name): self._run_test(lambda df: df[col_name].dtype) self._run_test(lambda df: df[col_name].dtypes) From 86ce68db8296c7d3dde367c31153b867bc58bd14 Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Mon, 8 Jun 2026 13:01:24 -0700 Subject: [PATCH 324/490] [Dataflow Streaming] Fix nullness supression in StreamingModeExecutionContext (#38842) --- .../worker/StreamingModeExecutionContext.java | 271 +++++++++--------- .../StreamingModeExecutionContextTest.java | 74 +++-- 2 files changed, 178 insertions(+), 167 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java index 25ce299adf7a..00fdf67b8d02 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java @@ -18,7 +18,6 @@ package org.apache.beam.runners.dataflow.worker; import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import com.google.api.services.dataflow.model.CounterUpdate; @@ -62,6 +61,7 @@ import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataId; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataRequest; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.Timer; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItemCommitRequest; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache.ForComputation; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateInternals; @@ -94,6 +94,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.PeekingIterator; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Table; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; @@ -105,11 +106,7 @@ * state pertaining to a processing its owning computation. Can be reused across processing * different WorkItems for the same computation. */ -@SuppressWarnings({ - "deprecation", - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -// TODO(m-trieu) fix nullability issues in StreamingModeExecutionContext.java +@SuppressWarnings({"deprecation"}) @NotThreadSafe @Internal public class StreamingModeExecutionContext extends DataflowExecutionContext { @@ -130,7 +127,7 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext, Map>> sideInputCache; - private WindmillTagEncoding windmillTagEncoding; + private final WindmillTagEncoding windmillTagEncoding; /** * The current user-facing key for this execution context. * @@ -143,13 +140,13 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext SideInput fetchSideInput( return fetchSideInputFromWindmill( view, sideInputWindow, - checkNotNull(stateFamily), + checkStateNotNull(stateFamily), state, - checkNotNull(scopedReadStateSupplier), + checkStateNotNull(scopedReadStateSupplier), tagCache); } @@ -383,8 +382,8 @@ private SideInput fetchSideInputFromWindmill( Supplier scopedReadStateSupplier, Map> tagCache) { SideInput fetched = - sideInputStateFetcher.fetchSideInput( - view, sideInputWindow, stateFamily, state, scopedReadStateSupplier); + checkStateNotNull(sideInputStateFetcher) + .fetchSideInput(view, sideInputWindow, stateFamily, state, scopedReadStateSupplier); if (fetched.isReady()) { tagCache.put(sideInputWindow, fetched); @@ -406,7 +405,7 @@ private List getFiredTimers() { } public WindmillComputationKey getComputationKey() { - return computationKey; + return checkStateNotNull(computationKey); } public long getWorkToken() { @@ -414,7 +413,7 @@ public long getWorkToken() { } public Windmill.WorkItem getWorkItem() { - return checkNotNull( + return checkStateNotNull( work, "work is null. A call to StreamingModeExecutionContext.start(...) is required to set" + " work for execution.") @@ -422,7 +421,7 @@ public Windmill.WorkItem getWorkItem() { } public Windmill.WorkItemCommitRequest.Builder getOutputBuilder() { - return outputBuilder; + return checkStateNotNull(outputBuilder); } /** @@ -490,15 +489,16 @@ public Map> flushState() { throw new RuntimeException("Exception while running bundle finalizer", e); } })); - outputBuilder.addFinalizeIds(id); + getOutputBuilder().addFinalizeIds(id); } } - if (activeReader != null) { - Windmill.SourceState.Builder sourceStateBuilder = - outputBuilder.getSourceStateUpdatesBuilder(); - final UnboundedSource.CheckpointMark checkpointMark = activeReader.getCheckpointMark(); - final Instant watermark = activeReader.getWatermark(); + UnboundedReader reader = activeReader; + if (reader != null) { + Windmill.WorkItemCommitRequest.Builder builder = getOutputBuilder(); + Windmill.SourceState.Builder sourceStateBuilder = builder.getSourceStateUpdatesBuilder(); + final UnboundedSource.CheckpointMark checkpointMark = reader.getCheckpointMark(); + final Instant watermark = reader.getWatermark(); long id = ThreadLocalRandom.current().nextLong(); sourceStateBuilder.addFinalizeIds(id); callbacks.put( @@ -515,7 +515,7 @@ public Map> flushState() { @SuppressWarnings("unchecked") Coder checkpointCoder = - ((UnboundedSource) activeReader.getCurrentSource()) + ((UnboundedSource) reader.getCurrentSource()) .getCheckpointMarkCoder(); if (checkpointCoder != null) { ByteStringOutputStream stream = new ByteStringOutputStream(); @@ -525,7 +525,7 @@ public Map> flushState() { throw new RuntimeException("Exception while encoding checkpoint", e); } sourceStateBuilder.setState(stream.toByteString()); - if (activeReader.getCurrentSource().offsetBasedDeduplicationSupported()) { + if (reader.getCurrentSource().offsetBasedDeduplicationSupported()) { byte[] offsetLimit = checkpointMark.getOffsetLimit(); if (offsetLimit.length == 0) { throw new RuntimeException("Checkpoint offset limit must be non-empty."); @@ -533,31 +533,30 @@ public Map> flushState() { sourceStateBuilder.setOffsetLimit(ByteString.copyFrom(offsetLimit)); } } - outputBuilder.setSourceWatermark(WindmillTimeUtils.harnessToWindmillTimestamp(watermark)); + builder.setSourceWatermark(WindmillTimeUtils.harnessToWindmillTimestamp(watermark)); - backlogBytes = activeReader.getSplitBacklogBytes(); + backlogBytes = reader.getSplitBacklogBytes(); + ByteString serializedKey = checkStateNotNull(getSerializedKey()); if (backlogBytes == UnboundedReader.BACKLOG_UNKNOWN - && WorkerCustomSources.isFirstUnboundedSourceSplit(getSerializedKey())) { + && WorkerCustomSources.isFirstUnboundedSourceSplit(serializedKey)) { // Only call getTotalBacklogBytes() on the first split. - backlogBytes = activeReader.getTotalBacklogBytes(); + backlogBytes = reader.getTotalBacklogBytes(); } - outputBuilder.setSourceBacklogBytes(backlogBytes); + builder.setSourceBacklogBytes(backlogBytes); readerCache.cacheReader( - getComputationKey(), - getWorkItem().getCacheToken(), - getWorkItem().getWorkToken(), - activeReader); + getComputationKey(), getWorkItem().getCacheToken(), getWorkItem().getWorkToken(), reader); activeReader = null; } else if (backlogBytes != UnboundedReader.BACKLOG_UNKNOWN && backlogBytes != 1L) { // If activeReader is null, we might still have backlogBytes from an SDF. We ignore a reported // backlogBytes of 1 since older versions of the Java SDK use this value as a default when // RestrictionTracker.getProgress() or GetSize() are not defined. - outputBuilder.setSourceBacklogBytes(backlogBytes); + getOutputBuilder().setSourceBacklogBytes(backlogBytes); } return callbacks; } + @Nullable String getStateFamily(NameContext nameContext) { return nameContext.userName() == null ? null : stateNameMap.get(nameContext.userName()); } @@ -599,7 +598,7 @@ public static class StreamingModeExecutionState extends DataflowExecutionState { public StreamingModeExecutionState( NameContext nameContext, String stateName, - MetricsContainer metricsContainer, + @Nullable MetricsContainer metricsContainer, ProfileScope profileScope) { // TODO: Take in the requesting step name and side input index for streaming. super(nameContext, stateName, null, null, metricsContainer, profileScope); @@ -642,14 +641,16 @@ public static class StreamingModeExecutionStateRegistry extends DataflowExecutio protected DataflowExecutionState createState( NameContext nameContext, String stateName, - String requestingStepName, - Integer inputIndex, - MetricsContainer container, + @Nullable String requestingStepName, + @Nullable Integer inputIndex, + @Nullable MetricsContainer container, ProfileScope profileScope) { return new StreamingModeExecutionState(nameContext, stateName, container, profileScope); } } + private static final Closeable NO_OP_CLOSEABLE = () -> {}; + private static class ScopedReadStateSupplier implements Supplier { private final ExecutionState readState; @@ -662,9 +663,9 @@ private ScopedReadStateSupplier( } @Override - public @Nullable Closeable get() { + public Closeable get() { if (stateTracker == null) { - return null; + return NO_OP_CLOSEABLE; } return stateTracker.enterState(readState); } @@ -725,7 +726,7 @@ public TimerInternals timerInternals() { } @Override - public TimerData getNextFiredTimer(Coder windowCoder) { + public @Nullable TimerData getNextFiredTimer(Coder windowCoder) { return wrapped.getNextFiredUserTimer(windowCoder); } @@ -777,7 +778,7 @@ public static StreamingModeSideInputReader of( } @Override - public T get(PCollectionView view, BoundedWindow window) { + public @Nullable T get(PCollectionView view, BoundedWindow window) { if (!contains(view)) { throw new RuntimeException("get() called with unknown view"); } @@ -810,31 +811,32 @@ public boolean isEmpty() { class StepContext extends DataflowExecutionContext.DataflowStepContext implements StreamingModeStepContext { - private final String stateFamily; + private final @Nullable String stateFamily; private final Supplier scopedReadStateSupplier; - private WindmillStateInternals stateInternals; - private WindmillTimerInternals systemTimerInternals; - private WindmillTimerInternals userTimerInternals; + private @MonotonicNonNull WindmillStateInternals stateInternals; + private @MonotonicNonNull WindmillTimerInternals systemTimerInternals; + private @MonotonicNonNull WindmillTimerInternals userTimerInternals; // Lazily initialized - private Iterator cachedFiredSystemTimers = null; + private @Nullable Iterator cachedFiredSystemTimers = null; // Lazily initialized - private PeekingIterator cachedFiredUserTimers = null; + private @Nullable PeekingIterator cachedFiredUserTimers = null; // An ordered list of any timers that were set or modified by user processing earlier in this // bundle. // We use a NavigableSet instead of a priority queue to prevent duplicate elements from ending // up in the queue. - private NavigableSet modifiedUserEventTimersOrdered = null; - private NavigableSet modifiedUserProcessingTimersOrdered = null; - private NavigableSet modifiedUserSynchronizedProcessingTimersOrdered = null; + private final NavigableSet modifiedUserEventTimersOrdered = Sets.newTreeSet(); + private final NavigableSet modifiedUserProcessingTimersOrdered = Sets.newTreeSet(); + private final NavigableSet modifiedUserSynchronizedProcessingTimersOrdered = + Sets.newTreeSet(); // A list of timer keys that were modified by user processing earlier in this bundle. This // serves a tombstone, so that we know not to fire any bundle timers that were modified. - private Table modifiedUserTimerKeys = null; + private final Table modifiedUserTimerKeys = + HashBasedTable.create(); private final WindmillBundleFinalizer bundleFinalizer = new WindmillBundleFinalizer(); public StepContext(DataflowOperationContext operationContext) { super(operationContext.nameContext()); this.stateFamily = getStateFamily(operationContext.nameContext()); - this.scopedReadStateSupplier = new ScopedReadStateSupplier(operationContext, getExecutionStateTracker()); } @@ -845,46 +847,50 @@ public void start( Instant processingTime, WindmillStateCache.ForKey cacheForKey, Watermarks watermarks) { - this.stateInternals = - new WindmillStateInternals<>( - key, - stateFamily, - stateReader, - getWorkItem().getIsNewKey(), - cacheForKey.forFamily(stateFamily), - windmillTagEncoding, - scopedReadStateSupplier); - - this.systemTimerInternals = - new WindmillTimerInternals( - stateFamily, - WindmillTimerType.SYSTEM_TIMER, - processingTime, - watermarks, - windmillTagEncoding, - td -> {}); - - this.userTimerInternals = - new WindmillTimerInternals( - stateFamily, - WindmillTimerType.USER_TIMER, - processingTime, - watermarks, - windmillTagEncoding, - this::onUserTimerModified); - + if (stateFamily != null) { + this.stateInternals = + new WindmillStateInternals<>( + key, + stateFamily, + stateReader, + getWorkItem().getIsNewKey(), + cacheForKey.forFamily(stateFamily), + windmillTagEncoding, + scopedReadStateSupplier); + + this.systemTimerInternals = + new WindmillTimerInternals( + stateFamily, + WindmillTimerType.SYSTEM_TIMER, + processingTime, + watermarks, + windmillTagEncoding, + td -> {}); + + this.userTimerInternals = + new WindmillTimerInternals( + stateFamily, + WindmillTimerType.USER_TIMER, + processingTime, + watermarks, + windmillTagEncoding, + this::onUserTimerModified); + } this.cachedFiredSystemTimers = null; this.cachedFiredUserTimers = null; - modifiedUserEventTimersOrdered = Sets.newTreeSet(); - modifiedUserProcessingTimersOrdered = Sets.newTreeSet(); - modifiedUserSynchronizedProcessingTimersOrdered = Sets.newTreeSet(); - modifiedUserTimerKeys = HashBasedTable.create(); + this.modifiedUserEventTimersOrdered.clear(); + this.modifiedUserProcessingTimersOrdered.clear(); + this.modifiedUserSynchronizedProcessingTimersOrdered.clear(); + this.modifiedUserTimerKeys.clear(); } public void flushState() { - stateInternals.persist(outputBuilder); - systemTimerInternals.persistTo(outputBuilder); - userTimerInternals.persistTo(outputBuilder); + if (stateFamily != null) { + WorkItemCommitRequest.Builder builder = getOutputBuilder(); + checkStateNotNull(stateInternals).persist(builder); + checkStateNotNull(systemTimerInternals).persistTo(builder); + checkStateNotNull(userTimerInternals).persistTo(builder); + } } @Override @@ -893,9 +899,14 @@ public void setBacklogBytes(double backlogBytes) { } @Override - public TimerData getNextFiredTimer(Coder windowCoder) { - if (cachedFiredSystemTimers == null) { - cachedFiredSystemTimers = + public @Nullable TimerData getNextFiredTimer(Coder windowCoder) { + if (stateFamily == null) { + // no timers on stateless stages + return null; + } + Iterator firedSystemTimers = cachedFiredSystemTimers; + if (firedSystemTimers == null) { + firedSystemTimers = FluentIterable.from(StreamingModeExecutionContext.this.getFiredTimers()) .filter(timer -> timer.getStateFamily().equals(stateFamily)) .transform( @@ -907,16 +918,17 @@ timer, windowCoder, getDrainMode())) windmillTimerData.getWindmillTimerType() == WindmillTimerType.SYSTEM_TIMER) .transform(WindmillTimerData::getTimerData) .iterator(); + cachedFiredSystemTimers = firedSystemTimers; } - if (!cachedFiredSystemTimers.hasNext()) { + if (!firedSystemTimers.hasNext()) { return null; } - TimerData nextTimer = cachedFiredSystemTimers.next(); + TimerData nextTimer = firedSystemTimers.next(); // system timers ( GC timer) must be explicitly deleted if only there is a hold. // if timestamp is not equals to outputTimestamp then there should be a hold if (!nextTimer.getTimestamp().equals(nextTimer.getOutputTimestamp())) { - systemTimerInternals.deleteTimer(nextTimer); + checkStateNotNull(systemTimerInternals).deleteTimer(nextTimer); } return nextTimer; } @@ -950,12 +962,19 @@ private boolean isTimerUnmodified(TimerData timerData) { return updatedTimer == null || updatedTimer.equals(timerData); } - public TimerData getNextFiredUserTimer(Coder windowCoder) { - if (cachedFiredUserTimers == null) { + public @Nullable TimerData getNextFiredUserTimer( + Coder windowCoder) { + if (stateFamily == null) { + // no timers on stateless stages + return null; + } + + PeekingIterator firedUserTimers = cachedFiredUserTimers; + if (firedUserTimers == null) { // This is the first call to getNextFiredUserTimer in this bundle. Extract any user timers // from the bundle // and cache the list for the rest of this bundle processing. - cachedFiredUserTimers = + firedUserTimers = Iterators.peekingIterator( FluentIterable.from(StreamingModeExecutionContext.this.getFiredTimers()) .filter(timer -> timer.getStateFamily().equals(stateFamily)) @@ -969,17 +988,20 @@ timer, windowCoder, getDrainMode())) == WindmillTimerType.USER_TIMER) .transform(WindmillTimerData::getTimerData) .iterator()); + cachedFiredUserTimers = firedUserTimers; } - while (cachedFiredUserTimers.hasNext()) { - TimerData nextInBundle = cachedFiredUserTimers.peek(); + WindmillTimerInternals nonNullUserTimerInternals = checkStateNotNull(this.userTimerInternals); + + while (firedUserTimers.hasNext()) { + TimerData nextInBundle = firedUserTimers.peek(); NavigableSet modifiedUserTimersOrdered = getModifiedUserTimersOrdered(nextInBundle.getDomain()); // If there is a modified timer that is earlier than the next timer in the bundle, try and // fire that first. while (!modifiedUserTimersOrdered.isEmpty() && modifiedUserTimersOrdered.first().compareTo(nextInBundle) <= 0) { - TimerData earlierTimer = modifiedUserTimersOrdered.pollFirst(); + TimerData earlierTimer = checkStateNotNull(modifiedUserTimersOrdered.pollFirst()); if (isTimerUnmodified(earlierTimer)) { // We must delete the timer. This prevents it from being committed to the backing store. // It also handles the @@ -987,15 +1009,15 @@ timer, windowCoder, getDrainMode())) // without deleting the // timer, the runner will still have that future timer stored, and would fire it // spuriously. - userTimerInternals.deleteTimer(earlierTimer); + nonNullUserTimerInternals.deleteTimer(earlierTimer); return earlierTimer; } } // There is no earlier timer to fire, so return the next timer in the bundle. - nextInBundle = cachedFiredUserTimers.next(); + nextInBundle = firedUserTimers.next(); if (isTimerUnmodified(nextInBundle)) { // User timers must be explicitly deleted when delivered, to release the implied hold. - userTimerInternals.deleteTimer(nextInBundle); + nonNullUserTimerInternals.deleteTimer(nextInBundle); return nextInBundle; } } @@ -1029,12 +1051,6 @@ public Iterable getSideInputNotifications() { return StreamingModeExecutionContext.this.getSideInputNotifications(); } - private void ensureStateful(String errorPrefix) { - if (stateFamily == null) { - throw new IllegalStateException(errorPrefix + " for stateless step: " + getNameContext()); - } - } - @Override public void writePCollectionViewData( TupleTag tag, @@ -1043,7 +1059,8 @@ public void writePCollectionViewData( W window, Coder windowCoder) throws IOException { - if (getSerializedKey().size() != 0) { + ByteString serializedKey = checkStateNotNull(getSerializedKey()); + if (serializedKey.size() != 0) { throw new IllegalStateException("writePCollectionViewData must follow a Combine.globally"); } @@ -1053,7 +1070,7 @@ public void writePCollectionViewData( ByteStringOutputStream windowStream = new ByteStringOutputStream(); windowCoder.encode(window, windowStream, Coder.Context.OUTER); - ensureStateful("Tried to write view data"); + String stateFamily = checkStateNotNull(this.stateFamily, "Tried to write view data"); Windmill.GlobalData.Builder builder = Windmill.GlobalData.newBuilder() @@ -1065,7 +1082,7 @@ public void writePCollectionViewData( .setData(dataStream.toByteString()) .setStateFamily(stateFamily); - outputBuilder.addGlobalDataUpdates(builder.build()); + getOutputBuilder().addGlobalDataUpdates(builder.build()); } /** Fetch the given side input asynchronously and return true if it is present. */ @@ -1080,11 +1097,12 @@ public boolean issueSideInputFetch( /** Note that there is data on the current key that is blocked on the given side input. */ @Override public void addBlockingSideInput(Windmill.GlobalDataRequest sideInput) { - ensureStateful("Tried to set global data request"); + String stateFamily = checkStateNotNull(this.stateFamily, "Tried to set global data request"); sideInput = Windmill.GlobalDataRequest.newBuilder(sideInput).setStateFamily(stateFamily).build(); - outputBuilder.addGlobalDataRequests(sideInput); - outputBuilder.addGlobalDataIdRequests(sideInput.getDataId()); + WorkItemCommitRequest.Builder builder = getOutputBuilder(); + builder.addGlobalDataRequests(sideInput); + builder.addGlobalDataIdRequests(sideInput.getDataId()); } /** Note that there is data on the current key that is blocked on the given side inputs. */ @@ -1097,14 +1115,12 @@ public void addBlockingSideInputs(Iterable sideInput @Override public StateInternals stateInternals() { - ensureStateful("Tried to access state"); - return checkNotNull(stateInternals); + return checkStateNotNull(stateInternals, "Tried to access state"); } @Override public TimerInternals timerInternals() { - ensureStateful("Tried to access timers"); - return checkNotNull(systemTimerInternals); + return checkStateNotNull(systemTimerInternals, "Tried to access timers"); } @Override @@ -1113,8 +1129,7 @@ public BundleFinalizer bundleFinalizer() { } public TimerInternals userTimerInternals() { - ensureStateful("Tried to access user timers"); - return checkNotNull(userTimerInternals); + return checkStateNotNull(userTimerInternals, "Tried to access user timers"); } public ImmutableList> flushBundleFinalizerCallbacks() { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java index 216ca5386675..13601410bfd9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java @@ -61,6 +61,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.Work; import org.apache.beam.runners.dataflow.worker.streaming.config.FakeGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfig; +import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; @@ -110,36 +111,40 @@ public class StreamingModeExecutionContextTest { DataflowWorkerHarnessOptions options; private FakeGlobalConfigHandle globalConfigHandle; + private StreamingModeExecutionContext createExecutionContext( + StreamingGlobalConfigHandle configHandle) { + CounterSet counterSet = new CounterSet(); + ConcurrentHashMap stateNameMap = new ConcurrentHashMap<>(); + stateNameMap.put(NameContextsForTests.nameContextForTest().userName(), "testStateFamily"); + return new StreamingModeExecutionContext( + counterSet, + COMPUTATION_ID, + new ReaderCache(Duration.standardMinutes(1), Executors.newCachedThreadPool()), + stateNameMap, + WindmillStateCache.builder() + .setSizeMb(options.getWorkerCacheMb()) + .build() + .forComputation("comp"), + StreamingStepMetricsContainer.createRegistry(), + new DataflowExecutionStateTracker( + ExecutionStateSampler.newForTest(), + executionStateRegistry.getState( + NameContext.forStage("stage"), "other", null, NoopProfileScope.NOOP), + counterSet, + PipelineOptionsFactory.create(), + "test-work-item-id"), + executionStateRegistry, + configHandle, + Long.MAX_VALUE, + /*throwExceptionOnLargeOutput=*/ false); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); options = PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); - CounterSet counterSet = new CounterSet(); - ConcurrentHashMap stateNameMap = new ConcurrentHashMap<>(); globalConfigHandle = new FakeGlobalConfigHandle(StreamingGlobalConfig.builder().build()); - stateNameMap.put(NameContextsForTests.nameContextForTest().userName(), "testStateFamily"); - executionContext = - new StreamingModeExecutionContext( - counterSet, - COMPUTATION_ID, - new ReaderCache(Duration.standardMinutes(1), Executors.newCachedThreadPool()), - stateNameMap, - WindmillStateCache.builder() - .setSizeMb(options.getWorkerCacheMb()) - .build() - .forComputation("comp"), - StreamingStepMetricsContainer.createRegistry(), - new DataflowExecutionStateTracker( - ExecutionStateSampler.newForTest(), - executionStateRegistry.getState( - NameContext.forStage("stage"), "other", null, NoopProfileScope.NOOP), - counterSet, - PipelineOptionsFactory.create(), - "test-work-item-id"), - executionStateRegistry, - globalConfigHandle, - Long.MAX_VALUE, - /*throwExceptionOnLargeOutput=*/ false); + executionContext = createExecutionContext(globalConfigHandle); } private static Work createMockWork(Windmill.WorkItem workItem, Watermarks watermarks) { @@ -421,20 +426,11 @@ public void testStateTagEncodingBasedOnConfig() { for (Boolean isV2Encoding : Lists.newArrayList(Boolean.TRUE, Boolean.FALSE)) { Class expectedEncoding = isV2Encoding ? WindmillTagEncodingV2.class : WindmillTagEncodingV1.class; - Windmill.WorkItemCommitRequest.Builder outputBuilder = - Windmill.WorkItemCommitRequest.newBuilder(); - globalConfigHandle.setConfig( - StreamingGlobalConfig.builder().setEnableStateTagEncodingV2(isV2Encoding).build()); - executionContext.start( - "key", - createMockWork( - Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), - Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), - stateReader, - sideInputStateFetcher, - outputBuilder, - workExecutor); - assertEquals(expectedEncoding, executionContext.getWindmillTagEncoding().getClass()); + FakeGlobalConfigHandle configHandle = + new FakeGlobalConfigHandle( + StreamingGlobalConfig.builder().setEnableStateTagEncodingV2(isV2Encoding).build()); + StreamingModeExecutionContext context = createExecutionContext(configHandle); + assertEquals(expectedEncoding, context.getWindmillTagEncoding().getClass()); } } From 1cee5b451d49cab794377683b63c380d6ae2f470 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Mon, 8 Jun 2026 22:31:20 +0200 Subject: [PATCH 325/490] Fix ensurepip bundled pip upgrade for Python 3.12+ (#38765) * Fix ensurepip bundled pip upgrade for Python 3.12+ * Fix ensurepip upgrade for Python 3.14 container images * Fix ensurepip upgrade for Python 3.12+ container images * Fix ensurepip upgrade * Add comment on upgrade_ensurepip Python 3.12+ incompatibility --- sdks/python/container/Dockerfile | 14 +++- .../license_scripts/upgrade_bundled_pip.py | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 sdks/python/container/license_scripts/upgrade_bundled_pip.py diff --git a/sdks/python/container/Dockerfile b/sdks/python/container/Dockerfile index 3bdef2dc1ddc..b0e0b94e7086 100644 --- a/sdks/python/container/Dockerfile +++ b/sdks/python/container/Dockerfile @@ -23,6 +23,7 @@ ARG TARGETOS ARG TARGETARCH COPY target/base_image_requirements.txt /tmp/base_image_requirements.txt +COPY target/license_scripts/upgrade_bundled_pip.py /tmp/upgrade_bundled_pip.py COPY target/apache-beam.tar.gz /opt/apache/beam/tars/ COPY target/launcher/${TARGETOS}_${TARGETARCH}/boot target/LICENSE target/NOTICE target/LICENSE.python /opt/apache/beam/ @@ -85,14 +86,21 @@ RUN \ rm -rf /root/.cache/pip && \ # Update ensurepip to use most recent versions of setuptools and pip. This avoids some vulnerabilities which won't be fixed on older versions of python. - pip install upgrade_ensurepip; \ - python3 -m upgrade_ensurepip; \ + # upgrade_ensurepip is not maintained for Python 3.12+ (ensurepip no longer bundles setuptools). + if [ "${py_version}" = "3.10" ] || [ "${py_version}" = "3.11" ]; then \ + pip install upgrade_ensurepip; \ + python3 -m upgrade_ensurepip; \ + else \ + python3 /tmp/upgrade_bundled_pip.py; \ + fi; \ # setuptools is not bundled with ensurepip in Python 3.12+ if [ "${py_version}" = "3.10" ] || [ "${py_version}" = "3.11" ]; then \ find /usr/local/lib/python${py_version}/ensurepip/_bundled/setuptools-* -type f ! -name $(basename $(ls -v /usr/local/lib/python${py_version}/ensurepip/_bundled/setuptools-*-py3-none-any.whl | tail -n 1)) -delete; \ fi; \ find /usr/local/lib/python${py_version}/ensurepip/_bundled/pip-* -type f ! -name $(basename $(ls -v /usr/local/lib/python${py_version}/ensurepip/_bundled/pip-*-py3-none-any.whl | tail -n 1)) -delete; \ - pip uninstall upgrade_ensurepip -y; \ + if [ "${py_version}" = "3.10" ] || [ "${py_version}" = "3.11" ]; then \ + pip uninstall upgrade_ensurepip -y; \ + fi; \ python3 -m ensurepip; ENTRYPOINT ["/opt/apache/beam/boot"] diff --git a/sdks/python/container/license_scripts/upgrade_bundled_pip.py b/sdks/python/container/license_scripts/upgrade_bundled_pip.py new file mode 100644 index 000000000000..7a2a667d398e --- /dev/null +++ b/sdks/python/container/license_scripts/upgrade_bundled_pip.py @@ -0,0 +1,65 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +Upgrade the pip wheel bundled in ensurepip for Python 3.12+. + +The script is executed within Docker after the image pip has been upgraded. +upgrade_ensurepip expects setuptools to be bundled as well, but Python 3.12+ +only ships pip in ensurepip/_bundled. +""" + +import subprocess +import sys +from pathlib import Path + +import ensurepip + + +def main(): + ep_path = Path(ensurepip.__file__) + wheel_dir = ep_path.parent / '_bundled' + pip_version = subprocess.check_output( + [sys.executable, '-m', 'pip', '--version'], + text=True).split()[1] + subprocess.check_call([ + sys.executable, + '-m', + 'pip', + 'download', + 'pip=={}'.format(pip_version), + '-d', + str(wheel_dir), + '--no-deps', + ]) + lines = ep_path.read_text().splitlines() + pip_line = None + for idx, line in enumerate(lines): + if line.startswith('_PIP_VERSION = '): + pip_line = idx + break + if pip_line is None: + sys.exit('ensurepip _PIP_VERSION not found') + org = ep_path.with_suffix('.py.org') + if not org.exists(): + ep_path.rename(org) + lines[pip_line] = '_PIP_VERSION = "{}"'.format(pip_version) + ep_path.write_text('\n'.join(lines) + '\n') + + +if __name__ == '__main__': + main() From 12f022d9644608b4f8bedd456b94ea9a14433594 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Mon, 8 Jun 2026 22:32:23 +0200 Subject: [PATCH 326/490] Fix cloudML TFT install avoiding pip ResolutionTooDeep (#38781) * Fix cloudML TFT install avoiding pip ResolutionTooDeep * Trigger workflow * Added a comment CloudML TFT install --- .../trigger_files/beam_CloudML_Benchmarks_Dataflow.json | 2 +- sdks/python/test-suites/dataflow/common.gradle | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json b/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json index b73af5e61a43..37dd25bf9029 100644 --- a/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json +++ b/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 3 } diff --git a/sdks/python/test-suites/dataflow/common.gradle b/sdks/python/test-suites/dataflow/common.gradle index 0e5d0b01ee14..27cf2869600c 100644 --- a/sdks/python/test-suites/dataflow/common.gradle +++ b/sdks/python/test-suites/dataflow/common.gradle @@ -571,7 +571,13 @@ task installTFTRequirements { exec { workingDir "$rootProject.projectDir/sdks/python/apache_beam/testing/benchmarks/cloudml/" executable 'sh' - args '-c', ". ${envdir}/bin/activate && pip install -r requirements.txt" + // installGcpTest already installed apache-beam[gcp]. tensorflow-transform also + // lists that dependency, so a plain pip install -r can re-resolve the GCP extra + // and hit ResolutionTooDeep. Install TFT with --no-deps instead. + args '-c', ". ${envdir}/bin/activate && " + + "grep -v '^tensorflow-transform' requirements.txt > /tmp/cloudml_tft_base_requirements.txt && " + + "pip install -r /tmp/cloudml_tft_base_requirements.txt && " + + "pip install --no-deps tensorflow-transform==1.16.0" } } } From d133f0260054d1b39e30c0b283b58e2df6f2ec45 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:41:17 -0400 Subject: [PATCH 327/490] use local variable (#38850) --- sdks/python/apache_beam/io/fileio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/io/fileio.py b/sdks/python/apache_beam/io/fileio.py index 7c8350743267..a333b7c89775 100644 --- a/sdks/python/apache_beam/io/fileio.py +++ b/sdks/python/apache_beam/io/fileio.py @@ -313,9 +313,10 @@ def expand(self, pbegin) -> beam.PCollection[filesystem.FileMetadata]: fire_interval=self.interval) # match file pattern periodically + file_pattern = self.file_pattern match_files = ( impulse - | 'GetFilePattern' >> beam.Map(lambda x: self.file_pattern) + | 'GetFilePattern' >> beam.Map(lambda x: file_pattern) | MatchAll(self.empty_match_treatment)) # apply deduplication strategy if required From 35de131186db003591d2ef7fb6a429a3d708aaf8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Tue, 9 Jun 2026 00:37:31 +0200 Subject: [PATCH 328/490] Fix Dataflow cost benchmark after Dataflow client migration (#38788) * Fix Dataflow cost benchmark after Dataflow client migration * Fix formatter --- .../load_tests/dataflow_cost_benchmark.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py b/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py index 3300831b8ea1..4d412eb228ec 100644 --- a/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py +++ b/sdks/python/apache_beam/testing/load_tests/dataflow_cost_benchmark.py @@ -141,7 +141,7 @@ def _process_metrics_list(self, return system_metrics def _get_worker_time_interval( - self, job_id: str) -> tuple[Optional[str], Optional[str]]: + self, job_id: str) -> tuple[Optional[datetime], Optional[datetime]]: """Extracts worker start and stop times from job messages.""" start_time, end_time = None, None page_token = None @@ -155,7 +155,7 @@ def _get_worker_time_interval( page_token=page_token, minimum_importance='JOB_MESSAGE_DEBUG') for message in messages: - text = message.messageText + text = message.message_text if getattr(message, 'time', None): last_message_time = message.time if text: @@ -186,8 +186,8 @@ def _get_throughput_metrics( self, project: str, job_id: str, - start_time: str, - end_time: str, + start_time: datetime, + end_time: datetime, pcollection_name: Optional[str] = None, ) -> dict[str, float]: """Query Cloud Monitoring for per-PCollection throughput.""" @@ -256,7 +256,8 @@ def _point_numeric_value(point) -> float: return metrics def _get_streaming_throughput_metrics( - self, project: str, start_time: str, end_time: str) -> dict[str, float]: + self, project: str, start_time: datetime, + end_time: datetime) -> dict[str, float]: if not self.subscription: return {'AvgThroughputBytes': 0.0, 'AvgThroughputElements': 0.0} @@ -297,17 +298,14 @@ def _get_streaming_throughput_metrics( metrics[f"AvgThroughput{key}"] = avg_rate return metrics - def _get_job_runtime(self, start_time: str, end_time: str) -> float: + def _get_job_runtime(self, start_time: datetime, end_time: datetime) -> float: """Calculates the job runtime duration in seconds.""" - start_dt = datetime.fromisoformat(start_time[:-1]) - end_dt = datetime.fromisoformat(end_time[:-1]) - return (end_dt - start_dt).total_seconds() + return (end_time - start_time).total_seconds() def _get_additional_metrics(self, result: DataflowPipelineResult) -> dict[str, Any]: job_id = result.job_id() - job = self.dataflow_client.get_job(job_id) - project = job.projectId + project = self.project_id start_time, end_time = self._get_worker_time_interval(job_id) if not start_time or not end_time: logging.warning('Could not find valid worker start/end times.') From 169498d78fc8cd5f0b725aa71434a996ff2c12a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:24:36 -0400 Subject: [PATCH 329/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#38856) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.103.2 to 1.103.3. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.103.2...service/s3/v1.103.3) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.103.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 16 ++++++++-------- sdks/go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 2151cae2f1e0..d30a584c4c2c 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -32,11 +32,11 @@ require ( cloud.google.com/go/pubsub v1.50.2 cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.3 - github.com/aws/aws-sdk-go-v2 v1.41.12 + github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.23 github.com/aws/aws-sdk-go-v2/credentials v1.19.22 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 - github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 github.com/aws/smithy-go v1.27.2 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 @@ -150,13 +150,13 @@ require ( github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index d945d9e669d8..d7059807e15b 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -199,8 +199,8 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.12 h1:DIKX2c31ekm9RA2D9FBj1EWXx++9AdAqRw+e78Tq2Ck= -github.com/aws/aws-sdk-go-v2 v1.41.12/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= @@ -223,38 +223,38 @@ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 h1:xJ3WVH3J0xESIqkavgbN github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25/go.mod h1:0EOIx2v10rBBcaoTpOsRRNKDxhtRecRbN42YMrJaKZE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 h1:Xf2j7NdVcUKomlZ4iihOP4AZ3Fzlr8h4yKpXeP+OFPg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28/go.mod h1:O8cDo1dW63jU7ki//kRe1z+tLGcpnD1jrouitsQddDw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 h1:KqIfN9kpkKkcBqBbNpNGTIrXO6ExTUvFKvXkC+YAzVo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28/go.mod h1:uxtQiKvLtNS4iXVsH2McVD/ls8FKN/uUhe1hGxPjrw0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 h1:VkE9FuzTQVjBBrnj4+oCdxCLFIz7aqLYKUCjtvxVcOs= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29/go.mod h1:H32Z2Qth9b+9LqjyBsCnozMQ8H2N7YBUDVXwbs0iggg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21 h1:FsZxbPiVgEHYofziwfylouMki8b1Z7mI4CMU/7bhwBA= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21/go.mod h1:Mmm30OV+JLXYQUcbSd84THnv3P5JtjhVDujLwMqRG0U= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28 h1:axj4mEDletwKmTm/9jR+DkIMmCfcn5vE4jBMAAN+3Vg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28/go.mod h1:3Aaz69M0jqfSHLKqxgolgUBFT4hpwSNc7DzC95orEi8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.28 h1:li8rTZAAb22g4UsxbjwMdaNVWbgVcDzPqI7nDTI+mF4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.28/go.mod h1:/brXioSGIMEdcBFoubpSdmighSVp6poP+mma/wB7iHA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 h1:b4ikkRk22T4xYkEgaWc3Voe+3xbt5YbbFhNehOWyUiY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2/go.mod h1:Gp7eHZ0NZ8ZK5RXpoIUp/C8OeAmJqpCgdwEK1D/QOek= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 h1:YcpVyIPLCbiypN6KSphijN5fC7DDjX114SqA7prnnxg= github.com/aws/aws-sdk-go-v2/service/signin v1.1.4/go.mod h1:5ZICS++oFTRPfa1GsBqFDWX/8WamZ/QQOcCzIuU/zLw= From 06221588e555ea63ba313033fee3d30dca8c2829 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:25:05 -0400 Subject: [PATCH 330/490] Bump golang.org/x/sys from 0.45.0 to 0.46.0 in /sdks (#38857) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.45.0 to 0.46.0. - [Commits](https://github.com/golang/sys/compare/v0.45.0...v0.46.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index d30a584c4c2c..fd411586de95 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -58,7 +58,7 @@ require ( golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.45.0 + golang.org/x/sys v0.46.0 golang.org/x/text v0.37.0 google.golang.org/api v0.283.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 diff --git a/sdks/go.sum b/sdks/go.sum index d7059807e15b..fca2c0b226ae 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1158,8 +1158,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= From 874515c515a7fdfc609d2876785b1632b4ab6f84 Mon Sep 17 00:00:00 2001 From: kellen Date: Tue, 9 Jun 2026 14:17:00 +0200 Subject: [PATCH 331/490] Unpin google-cloud-bigtable version (fixes apache#37637) (#38861) --- .../groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index bff5aee4f6a1..0db2d1b27ab3 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -639,8 +639,6 @@ class BeamModulePlugin implements Plugin { def postgres_version = "42.6.2" // [bomupgrader] determined by: com.google.protobuf:protobuf-java, consistent with: google_cloud_platform_libraries_bom def protobuf_version = "4.33.2" - // TODO(https://github.com/apache/beam/issues/37637): Remove this once the Bom has been updated to at least reach this version - def bigtable_version = "2.73.1" def qpid_jms_client_version = "0.61.0" def quickcheck_version = "1.0" def sbe_tool_version = "1.25.1" @@ -754,7 +752,7 @@ class BeamModulePlugin implements Plugin { google_auth_library_oauth2_http : "com.google.auth:google-auth-library-oauth2-http", // google_cloud_platform_libraries_bom sets version google_cloud_bigquery : "com.google.cloud:google-cloud-bigquery", // google_cloud_platform_libraries_bom sets version google_cloud_bigquery_storage : "com.google.cloud:google-cloud-bigquerystorage", // google_cloud_platform_libraries_bom sets version - google_cloud_bigtable : "com.google.cloud:google-cloud-bigtable:$bigtable_version", // google_cloud_platform_libraries_bom sets version + google_cloud_bigtable : "com.google.cloud:google-cloud-bigtable", // google_cloud_platform_libraries_bom sets version google_cloud_bigtable_client_core_config : "com.google.cloud.bigtable:bigtable-client-core-config:1.28.0", google_cloud_bigtable_emulator : "com.google.cloud:google-cloud-bigtable-emulator", // google_cloud_platform_libraries_bom sets version google_cloud_core : "com.google.cloud:google-cloud-core", // google_cloud_platform_libraries_bom sets version From cfd35d922242c8dc1f62134422d7eb0b713e860f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:31:52 -0400 Subject: [PATCH 332/490] Bump distlib from 0.3.9 to 0.4.2 in /sdks/python (#38855) Bumps [distlib](https://github.com/pypa/distlib) from 0.3.9 to 0.4.2. - [Release notes](https://github.com/pypa/distlib/releases) - [Changelog](https://github.com/pypa/distlib/blob/master/CHANGES.rst) - [Commits](https://github.com/pypa/distlib/compare/0.3.9...0.4.2) --- updated-dependencies: - dependency-name: distlib dependency-version: 0.4.2 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 7da1d29acef3..1e12990171df 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -28,7 +28,7 @@ requires = [ "grpcio-tools==1.71.0; python_version >= '3.13'", "mypy-protobuf==3.5.0", # Avoid https://github.com/pypa/virtualenv/issues/2006 - "distlib==0.3.9", + "distlib==0.4.2", # Numpy headers "numpy>=1.14.3,<2.5.0", # Update setup.py as well. # having cython here will create wheels that are platform dependent. From 9fa747c66ed9552833d673a078fb24077d4cb0cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:32:42 -0400 Subject: [PATCH 333/490] Bump github.com/aws/aws-sdk-go-v2/credentials in /sdks (#38858) Bumps [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) from 1.19.22 to 1.19.23. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/credentials/v1.19.22...credentials/v1.19.23) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/credentials dependency-version: 1.19.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 12 ++++++------ sdks/go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index fd411586de95..ba58651b29be 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -34,7 +34,7 @@ require ( cloud.google.com/go/storage v1.62.3 github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.23 - github.com/aws/aws-sdk-go-v2/credentials v1.19.22 + github.com/aws/aws-sdk-go-v2/credentials v1.19.23 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 github.com/aws/smithy-go v1.27.2 @@ -91,7 +91,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -149,7 +149,7 @@ require ( github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect @@ -157,9 +157,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index fca2c0b226ae..1bfc39b07dd1 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -211,12 +211,12 @@ github.com/aws/aws-sdk-go-v2/config v1.32.23 h1:PYDobtcsJXK6bQe9I8RQk6s19Bz3xa3x github.com/aws/aws-sdk-go-v2/config v1.32.23/go.mod h1:QID4dqUQVgEOYPKsPWd1sNWCCR2c5g7o3jeEtIXPOZU= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.22 h1:SHfH6wyPsEgG7fVsi5rQxWEt7tuIcN2PGhb1mTFv6tE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.22/go.mod h1:54nO8lKD4aQPOntM/VTWjnR+DYzTwx0YkSMZMhAgewQ= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 h1:b+kcDejJrXc30zU/w8Tc9klISwaO5wh+6T0sMBdDoHM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28/go.mod h1:LnI62O9GnSv6GcuLXxOYqlq0C8EmxMcgnF6m7LdYuOY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 h1:xJ3WVH3J0xESIqkavgbNfvQdMzB98bSkGwFXCyM2Tdw= @@ -256,22 +256,22 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU66 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 h1:YcpVyIPLCbiypN6KSphijN5fC7DDjX114SqA7prnnxg= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.4/go.mod h1:5ZICS++oFTRPfa1GsBqFDWX/8WamZ/QQOcCzIuU/zLw= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 h1:ySNWu7TPmj5fKFIa1GYvX+Ddxd5ccruqC20aMNuyWDM= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.2/go.mod h1:A+U9luAOwFeB1kseyWCITVg7/NntoPebCFR9pQ4ch9A= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 h1:KSzGGqfk39O+WU3OEyYbx6F7sLDQCqxlOJ+2IksfK6U= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5/go.mod h1:ATs88lXDeQB6CZOgQ5BIl9JbYS+EsCWUSDyff6L/oVo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 h1:RTO7mmGyedgnNmcPh3yQizNfc6GKoV5iqfdJavuf9vw= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.2/go.mod h1:fBhUZXDin9YYqhcpOMjIcpdik25rVwWyxLdPH1RZd9s= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= From 77bda4d5955db7601406263bbdb3accb2810830f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:33:19 -0400 Subject: [PATCH 334/490] Bump golang.org/x/text from 0.37.0 to 0.38.0 in /sdks (#38860) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.37.0 to 0.38.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index ba58651b29be..e110296eb540 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -57,9 +57,9 @@ require ( go.mongodb.org/mongo-driver v1.17.9 golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.20.0 + golang.org/x/sync v0.21.0 golang.org/x/sys v0.46.0 - golang.org/x/text v0.37.0 + golang.org/x/text v0.38.0 google.golang.org/api v0.283.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 diff --git a/sdks/go.sum b/sdks/go.sum index 1bfc39b07dd1..b8a399fadc1f 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1071,8 +1071,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1186,8 +1186,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 529c559c1b0aea78e238458e188a97f19842474a Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 9 Jun 2026 10:35:21 -0400 Subject: [PATCH 335/490] Disable public ip for Dataflow Validates Runner test (#38424) --- .../beam_PostCommit_Java_ValidatesRunner_Dataflow.json | 2 +- .../beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json | 2 +- runners/google-cloud-dataflow-java/build.gradle | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow.json index e9d869cc508c..8144784f5f02 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 1, + "modification": 2, } diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json index 8bbe16c113f1..0726e6343b31 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 3 + "modification": 4 } diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index be2b7b2b01e4..c569c03b956e 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -167,6 +167,7 @@ def legacyPipelineOptions = [ "--region=${gcpRegion}", "--tempRoot=${dataflowValidatesTempRoot}", "--dataflowWorkerJar=${dataflowLegacyWorkerJar}", + "--usePublicIps=false", "--experiments=enable_lineage" ] @@ -184,6 +185,7 @@ def runnerV2CommonPipelineOptions = [ "--tempRoot=${dataflowValidatesTempRoot}", "--experiments=use_unified_worker,use_runner_v2", "--firestoreDb=${firestoreDb}", + "--usePublicIps=false", "--experiments=enable_lineage" ] From 21050a53aa1317acc6c989206adc2a2d2c39d6a6 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 9 Jun 2026 10:58:54 -0400 Subject: [PATCH 336/490] increase timeout (#38864) --- .github/workflows/beam_PreCommit_PythonDocker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PreCommit_PythonDocker.yml b/.github/workflows/beam_PreCommit_PythonDocker.yml index e933df2b5ef1..34b8a2c4f02c 100644 --- a/.github/workflows/beam_PreCommit_PythonDocker.yml +++ b/.github/workflows/beam_PreCommit_PythonDocker.yml @@ -56,7 +56,7 @@ jobs: beam_PreCommit_PythonDocker: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 45 + timeout-minutes: 60 strategy: fail-fast: false matrix: From 67bf365f05569241a42019d01b0b41714d18c2de Mon Sep 17 00:00:00 2001 From: Chamikara Jayalath Date: Tue, 9 Jun 2026 10:43:47 -0700 Subject: [PATCH 337/490] Adds a Github action for running Delta Lake I/O unit tests (#38869) --- .github/autolabeler.yml | 1 + .../beam_PreCommit_Java_Delta_IO_Direct.json | 4 + .github/workflows/README.md | 1 + .../beam_PreCommit_Java_Delta_IO_Direct.yml | 131 ++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 .github/trigger_files/beam_PreCommit_Java_Delta_IO_Direct.json create mode 100644 .github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index 0eb9a04b6d2c..0b2cdf7dc282 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -52,6 +52,7 @@ io: ["sdks/go/pkg/beam/io/**/*", "sdks/java/io/**/*", "sdks/python/apache_beam/ "aws": ["sdks/java/io/amazon-web-services/**/*", "sdks/java/io/amazon-web-services2/**/*", "sdks/python/apache_beam/io/aws/**/*"] "cassandra": ["sdks/java/io/cassandra/**/*"] "clickhouse": ["sdks/java/io/clickhouse/**/*"] +"delta": ["sdks/java/io/delta/**/*"] "elasticsearch": ["sdks/java/io/elasticsearch/**/*", "sdks/java/io/elasticsearch-tests/**/*"] "gcp": ["sdks/go/pkg/beam/io/bigqueryio/**/*", "sdks/go/pkg/beam/io/datastoreio/**/*", "sdks/go/pkg/beam/io/pubsubio/**/*", "sdks/java/io/bigquery-io-perf-tests/**/*", "sdks/java/io/google-cloud-platform/**/*", "sdks/python/apache_beam/io/gcp/**/*", "sdks/java/extensions/google-cloud-platform-core/**/*"] "hadoop": ["sdks/java/io/hadoop-common/**/*", "sdks/java/io/hadoop-file-system/**/*", "sdks/java/io/hadoop-format/**/*"] diff --git a/.github/trigger_files/beam_PreCommit_Java_Delta_IO_Direct.json b/.github/trigger_files/beam_PreCommit_Java_Delta_IO_Direct.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PreCommit_Java_Delta_IO_Direct.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "modification": 1 +} diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c6a95b29b4c0..0785b12a1d12 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -237,6 +237,7 @@ PreCommit Jobs run in a schedule and also get triggered in a PR if relevant sour | [ PreCommit Java Clickhouse IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml) | N/A |`Run Java_Clickhouse_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Csv IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml) | N/A |`Run Java_Csv_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Datadog IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml) | N/A |`Run Java_Datadog_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml?query=event%3Aschedule) | +| [ PreCommit Java Delta IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml) | N/A |`Run Java_Delta_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Debezium IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml) | N/A |`Run Java_Debezium_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java ElasticSearch IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml) | N/A |`Run Java_ElasticSearch_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Examples Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow.yml) | N/A |`Run Java_Examples_Dataflow PreCommit`| [![.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml new file mode 100644 index 000000000000..617260e55b88 --- /dev/null +++ b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml @@ -0,0 +1,131 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: PreCommit Java Delta IO Direct + +on: + push: + tags: ['v*'] + branches: ['master', 'release-*'] + paths: + - "sdks/java/io/delta/**" + - "sdks/java/managed/**" + - "sdks/java/expansion-service/**" + - "sdks/java/io/expansion-service/**" + - ".github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml" + pull_request_target: + branches: ['master', 'release-*'] + paths: + - "sdks/java/io/delta/**" + - "sdks/java/managed/**" + - "sdks/java/expansion-service/**" + - "sdks/java/io/expansion-service/**" + - 'release/trigger_all_tests.json' + - '.github/trigger_files/beam_PreCommit_Java_Delta_IO_Direct.json' + issue_comment: + types: [created] + schedule: + - cron: '15 2/6 * * *' + workflow_dispatch: + +#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +permissions: + actions: write + pull-requests: write + checks: write + contents: read + deployments: read + id-token: none + issues: write + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.event.pull_request.head.label || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + +jobs: + beam_PreCommit_Java_Delta_IO_Direct: + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + strategy: + matrix: + job_name: ["beam_PreCommit_Java_Delta_IO_Direct"] + job_phrase: ["Run Java_Delta_IO_Direct PreCommit"] + timeout-minutes: 60 + if: | + github.event_name == 'push' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event_name == 'workflow_dispatch' || + github.event.comment.body == 'Run Java_Delta_IO_Direct PreCommit' + runs-on: [self-hosted, ubuntu-24.04, main] + steps: + - uses: actions/checkout@v6 + - name: Setup repository + uses: ./.github/actions/setup-action + with: + comment_phrase: ${{ matrix.job_phrase }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + java-version: 17 + - name: run Delta IO build script + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :sdks:java:io:delta:build + arguments: | + -PdisableSpotlessCheck=true \ + -PdisableCheckStyle=true \ + -PtestJavaVersion=17 \ + -Pjava17Home=$JAVA_HOME_17_X64 \ + --info + - name: Archive JUnit Test Results + uses: actions/upload-artifact@v7 + if: ${{ !success() }} + with: + name: JUnit Test Results + path: "**/build/reports/tests/" + - name: Publish JUnit Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + commit: '${{ env.prsha || env.GITHUB_SHA }}' + comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} + files: '**/build/test-results/**/*.xml' + large_files: true + - name: Archive SpotBugs Results + uses: actions/upload-artifact@v7 + if: always() + with: + name: SpotBugs Results + path: '**/build/reports/spotbugs/*.html' + - name: Publish SpotBugs Results + uses: jwgmeligmeyling/spotbugs-github-action@v1.2 + if: always() + with: + name: Publish SpotBugs + path: '**/build/reports/spotbugs/*.html' From ca7a6f2065891adb957a4ffde9adab2fc1c97d18 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 9 Jun 2026 13:44:29 -0400 Subject: [PATCH 338/490] Enable code scanning alerts (#38817) * [wip] try codesql * Update codeql.yml to remove all other branches but master * Update codeql.yml --- .github/workflows/codeql.yml | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000000..30539e8ec08a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Enables code scanning alerts in repo. + +name: "CodeQL Advanced" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + workflow_dispatch: + schedule: + - cron: '25 10 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + # - language: c-cpp + # build-mode: autobuild + - language: go + build-mode: autobuild + - language: java-kotlin + build-mode: autobuild + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + - language: rust + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From efa266c382c436d2dc9a770ddcb3445e2f1c1772 Mon Sep 17 00:00:00 2001 From: reuvenlax Date: Tue, 9 Jun 2026 11:10:17 -0700 Subject: [PATCH 339/490] Side Input improvements (#38363) * side-input improvements * remove files * add Override * foo * foo * fix splittable dofn * fix * foo * fix windows * fix compilation * foo --- .../beam/runners/core/SimpleDoFnRunner.java | 26 +- .../worker/SimpleDoFnRunnerFactory.java | 12 - .../dataflow/worker/SimpleParDoFn.java | 533 ++++-------------- .../dataflow/worker/SimpleParDoFnHelpers.java | 518 +++++++++++++++++ .../worker/SplittableProcessFnFactory.java | 20 +- ...treamingKeyedWorkItemSideInputParDoFn.java | 246 ++++++++ .../worker/StreamingSideInputDoFnRunner.java | 41 +- .../worker/StreamingSideInputFetcher.java | 34 +- .../worker/StreamingSideInputProcessor.java | 132 +++++ .../dataflow/worker/UserParDoFnFactory.java | 56 +- .../worker/SimpleParDoFnHelpersTest.java | 133 +++++ .../dataflow/worker/SimpleParDoFnTest.java | 6 +- ...gKeyedWorkItemSideInputDoFnRunnerTest.java | 3 +- ...mingKeyedWorkItemSideInputParDoFnTest.java | 488 ++++++++++++++++ .../StreamingSideInputProcessorTest.java | 215 +++++++ .../worker/UserParDoFnFactoryTest.java | 27 +- runners/prism/java/build.gradle | 3 + .../sdk/testing/UsesSideInputsInTimer.java | 27 + .../org/apache/beam/sdk/transforms/DoFn.java | 16 + .../transforms/reflect/DoFnSignatures.java | 9 +- .../apache/beam/sdk/transforms/ParDoTest.java | 149 +++++ .../beam/fn/harness/FnApiDoFnRunner.java | 28 + 22 files changed, 2181 insertions(+), 541 deletions(-) create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFn.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessor.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpersTest.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFnTest.java create mode 100644 runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessorTest.java create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/testing/UsesSideInputsInTimer.java diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java index 9859d672b3e8..470e22a66991 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java @@ -870,8 +870,17 @@ public InputT element(DoFn doFn) { } @Override - public Object sideInput(String tagId) { - throw new UnsupportedOperationException("SideInput parameters are not supported."); + public @Nullable Object sideInput(String tagId) { + PCollectionView view = + checkStateNotNull(sideInputMapping.get(tagId), "Side input tag %s not found", tagId); + return sideInput(view); + } + + @Override + public T sideInput(PCollectionView view) { + checkNotNull(view, "View passed to sideInput cannot be null"); + return SimpleDoFnRunner.this.sideInput( + view, view.getWindowMappingFn().getSideInputWindow(window())); } @Override @@ -1196,8 +1205,17 @@ public InputT element(DoFn doFn) { } @Override - public Object sideInput(String tagId) { - throw new UnsupportedOperationException("SideInput parameters are not supported."); + public @Nullable Object sideInput(String tagId) { + PCollectionView view = + checkStateNotNull(sideInputMapping.get(tagId), "Side input tag %s not found", tagId); + return sideInput(view); + } + + @Override + public T sideInput(PCollectionView view) { + checkNotNull(view, "View passed to sideInput cannot be null"); + return SimpleDoFnRunner.this.sideInput( + view, view.getWindowMappingFn().getSideInputWindow(window())); } @Override diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleDoFnRunnerFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleDoFnRunnerFactory.java index 52fcec439aaf..5286fc1aae90 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleDoFnRunnerFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleDoFnRunnerFactory.java @@ -24,7 +24,6 @@ import org.apache.beam.runners.core.SideInputReader; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.DoFnSchemaInformation; import org.apache.beam.sdk.util.WindowedValueMultiReceiver; @@ -68,17 +67,6 @@ public DoFnRunner createRunner( windowingStrategy, doFnSchemaInformation, sideInputMapping); - boolean hasStreamingSideInput = - options.as(StreamingOptions.class).isStreaming() && !sideInputReader.isEmpty(); - if (hasStreamingSideInput) { - return new StreamingSideInputDoFnRunner<>( - fnRunner, - new StreamingSideInputFetcher<>( - sideInputViews, - inputCoder, - windowingStrategy, - (StreamingModeExecutionContext.StreamingModeStepContext) userStepContext)); - } return fnRunner; } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFn.java index 434d46c20a5b..34dff6b88358 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFn.java @@ -17,53 +17,26 @@ */ package org.apache.beam.runners.dataflow.worker; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - import java.io.Closeable; import java.util.Collection; -import java.util.HashMap; -import java.util.List; +import java.util.Collections; +import java.util.Iterator; import java.util.Map; -import org.apache.beam.runners.core.DoFnRunner; import org.apache.beam.runners.core.SideInputReader; -import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateNamespaces.WindowNamespace; -import org.apache.beam.runners.core.StateTag; -import org.apache.beam.runners.core.StateTags; -import org.apache.beam.runners.core.TimerInternals.TimerData; -import org.apache.beam.runners.dataflow.options.DataflowPipelineDebugOptions; -import org.apache.beam.runners.dataflow.worker.counters.Counter; -import org.apache.beam.runners.dataflow.worker.counters.CounterFactory; -import org.apache.beam.runners.dataflow.worker.counters.CounterName; -import org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter; -import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputReceiver; import org.apache.beam.runners.dataflow.worker.util.common.worker.ParDoFn; import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.StreamingOptions; -import org.apache.beam.sdk.state.StateSpec; -import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.transforms.DoFnSchemaInformation; -import org.apache.beam.sdk.transforms.reflect.DoFnSignature; -import org.apache.beam.sdk.transforms.reflect.DoFnSignature.StateDeclaration; -import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.util.DoFnInfo; -import org.apache.beam.sdk.util.WindowedValueMultiReceiver; import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowingStrategy; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A base class providing simple set up, processing, and tear down for a wrapped {@link @@ -76,41 +49,9 @@ "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) "nullness" // TODO(https://github.com/apache/beam/issues/20497) }) -public class SimpleParDoFn implements ParDoFn { - - // TODO: Remove once Distributions has shipped. - @VisibleForTesting - static final String OUTPUTS_PER_ELEMENT_EXPERIMENT = "outputs_per_element_counter"; - - private static final String COUNTER_NAME = "per-element-output-count"; - - private static final Logger LOG = LoggerFactory.getLogger(SimpleParDoFn.class); - - protected final PipelineOptions options; - private final DoFnInstanceManager doFnInstanceManager; - - private final SideInputReader sideInputReader; - private final DataflowOperationContext operationContext; - private final TupleTag mainOutputTag; - private final Map, Integer> outputTupleTagsToReceiverIndices; - private final List> sideOutputTags; - private final DataflowExecutionContext.DataflowStepContext stepContext; - private final DataflowExecutionContext.DataflowStepContext userStepContext; - private final CounterFactory counterFactory; - private final DoFnRunnerFactory runnerFactory; - private final boolean hasStreamingSideInput; - private final OutputsPerElementTracker outputsPerElementTracker; - private final DoFnSchemaInformation doFnSchemaInformation; - private final Map> sideInputMapping; - - // Various DoFn helpers, null between bundles - private @Nullable DoFnRunner fnRunner; - @Nullable DoFnInfo fnInfo; - private Receiver @Nullable [] receivers; - - // This may additionally be null if it is not a real DoFn but an OldDoFn or - // GroupAlsoByWindowViaWindowSetDoFn - private @Nullable DoFnSignature fnSignature; +public class SimpleParDoFn implements ParDoFn { + private final SimpleParDoFnHelpers helpers; + private @Nullable StreamingSideInputProcessor sideInputProcessor; /** Creates a {@link SimpleParDoFn} using basic information about the step being executed. */ SimpleParDoFn( @@ -124,225 +65,106 @@ public class SimpleParDoFn implements ParDoFn { DoFnSchemaInformation doFnSchemaInformation, Map> sideInputMapping, DoFnRunnerFactory runnerFactory) { - this.options = options; - this.doFnInstanceManager = doFnInstanceManager; - - // We vend a freshly deserialized version for each run - this.sideInputReader = sideInputReader; - this.operationContext = operationContext; - checkArgument(!outputTupleTagsToReceiverIndices.isEmpty(), "expected at least one output"); - this.mainOutputTag = mainOutputTag; - this.outputTupleTagsToReceiverIndices = outputTupleTagsToReceiverIndices; - ImmutableList.Builder> sideOutputTagsBuilder = ImmutableList.builder(); - for (TupleTag tag : outputTupleTagsToReceiverIndices.keySet()) { - if (!mainOutputTag.equals(tag)) { - sideOutputTagsBuilder.add(tag); - } - } - this.sideOutputTags = sideOutputTagsBuilder.build(); - this.stepContext = stepContext; - - // StepContext provides a TimerInternals and StateInternals for use by the system - this class. - // For the user, we request a user-scoped StepContext to provide a user-scoped - // StateInternals and TimerInternals. - this.userStepContext = stepContext.namespacedToUser(); - - this.counterFactory = operationContext.counterFactory(); - this.runnerFactory = runnerFactory; - this.hasStreamingSideInput = - options.as(StreamingOptions.class).isStreaming() && !sideInputReader.isEmpty(); - this.outputsPerElementTracker = createOutputsPerElementTracker(); - this.doFnSchemaInformation = doFnSchemaInformation; - this.sideInputMapping = sideInputMapping; - } - - private OutputsPerElementTracker createOutputsPerElementTracker() { - // TODO: Remove once Distributions has shipped. - if (!hasExperiment(OUTPUTS_PER_ELEMENT_EXPERIMENT)) { - return NoopOutputsPerElementTracker.INSTANCE; - } - - // TODO: Remove log statement when functionality is enabled by default. - LOG.info("{} counter enabled.", COUNTER_NAME); - - return new OutputsPerElementTrackerImpl(); - } - - private boolean hasExperiment(String experiment) { - List experiments = options.as(DataflowPipelineDebugOptions.class).getExperiments(); - return experiments != null && experiments.contains(experiment); - } - - /** Simple state tracker to calculate PerElementOutputCount counter. */ - private interface OutputsPerElementTracker { - - void onOutput(); - - void onProcessElement(); - - void onProcessElementSuccess(); - } - - private class OutputsPerElementTrackerImpl implements OutputsPerElementTracker { - - private long outputsPerElement; - private final Counter counter; - - public OutputsPerElementTrackerImpl() { - this.counter = - counterFactory.distribution( - CounterName.named(COUNTER_NAME).withOriginalName(stepContext.getNameContext())); - } - - @Override - public void onProcessElement() { - reset(); - } - - @Override - public void onOutput() { - outputsPerElement++; - } - - @Override - public void onProcessElementSuccess() { - counter.addValue(outputsPerElement); - reset(); - } - - private void reset() { - outputsPerElement = 0L; - } - } - - /** No-op {@link OutputsPerElementTracker} implementation used when the counter is disabled. */ - private static class NoopOutputsPerElementTracker implements OutputsPerElementTracker { - - private NoopOutputsPerElementTracker() {} - - public static final OutputsPerElementTracker INSTANCE = new NoopOutputsPerElementTracker(); - - @Override - public void onOutput() {} - - @Override - public void onProcessElement() {} - - @Override - public void onProcessElementSuccess() {} + this.helpers = + new SimpleParDoFnHelpers<>( + options, + doFnInstanceManager, + sideInputReader, + mainOutputTag, + outputTupleTagsToReceiverIndices, + stepContext, + operationContext, + doFnSchemaInformation, + sideInputMapping, + runnerFactory); } @Override public void startBundle(Receiver... receivers) throws Exception { - checkArgument( - receivers.length == outputTupleTagsToReceiverIndices.size(), - "unexpected number of receivers for DoFn"); - - this.receivers = receivers; - if (hasStreamingSideInput) { + helpers.startBundle(receivers); + if (helpers.hasStreamingSideInput) { // There is non-trivial setup that needs to be performed for watermark propagation // even on empty bundles. - reallyStartBundle(); - } - } - - private void reallyStartBundle() throws Exception { - checkState(fnRunner == null, "bundle already started (or not properly finished)"); - - WindowedValueMultiReceiver outputManager = - new WindowedValueMultiReceiver() { - final Map, OutputReceiver> undeclaredOutputs = new HashMap<>(); - - private @Nullable Receiver getReceiverOrNull(TupleTag tag) { - Integer receiverIndex = outputTupleTagsToReceiverIndices.get(tag); - if (receiverIndex != null) { - return receivers[receiverIndex]; - } else { - return undeclaredOutputs.get(tag); - } - } - - @Override - public void output(TupleTag tag, WindowedValue output) { - outputsPerElementTracker.onOutput(); - Receiver receiver = getReceiverOrNull(tag); - if (receiver == null) { - // A new undeclared output. - // TODO: plumb through the operationName, so that we can - // name implicit outputs after it. - String outputName = "implicit-" + tag.getId(); - // TODO: plumb through the counter prefix, so we can - // make it available to the OutputReceiver class in case - // it wants to use it in naming output counterFactory. (It - // doesn't today.) - OutputReceiver undeclaredReceiver = new OutputReceiver(); - - ElementCounter outputCounter = - new DataflowOutputCounter( - outputName, counterFactory, stepContext.getNameContext()); - undeclaredReceiver.addOutputCounter(outputCounter); - undeclaredOutputs.put(tag, undeclaredReceiver); - receiver = undeclaredReceiver; + helpers.reallyStartBundle(); + onStartKey(); + } + } + + protected void onStartKey() { + // TODO(relax): This assumes single-key bundles, which will change! Refactor this to not make + // this assumption. + if (helpers.hasStreamingSideInput) { + sideInputProcessor = + new StreamingSideInputProcessor<>( + new StreamingSideInputFetcher( + helpers.fnInfo.getSideInputViews(), + helpers.fnInfo.getInputCoder(), + (WindowingStrategy) helpers.fnInfo.getWindowingStrategy(), + (StreamingModeExecutionContext.StreamingModeStepContext) + helpers.userStepContext)); + + boolean hasState = helpers.hasState(); + sideInputProcessor.tryUnblockElements( + unblockedElements -> { + for (WindowedValue unblockedElement : unblockedElements) { + helpers.fnRunner.processElement(unblockedElement); + if (hasState) { + // These elements are now processed. Register cleanup timers for all the unblocked + // windows. + helpers.registerStateCleanup( + (WindowingStrategy) getDoFnInfo().getWindowingStrategy(), + (Collection) unblockedElement.getWindows()); + } } - - try { - receiver.process(output); - } catch (RuntimeException | Error e) { - // Rethrow unchecked exceptions as-is to avoid excessive nesting - // via a chain of DoFn's. - throw e; - } catch (Exception e) { - // This should never happen in practice with DoFn's, but can happen - // with other Receivers. - throw new RuntimeException(e); - } - } - }; - fnInfo = (DoFnInfo) doFnInstanceManager.get(); - fnSignature = DoFnSignatures.getSignature(fnInfo.getDoFn().getClass()); - - fnRunner = - runnerFactory.createRunner( - fnInfo.getDoFn(), - options, - mainOutputTag, - sideOutputTags, - fnInfo.getSideInputViews(), - sideInputReader, - fnInfo.getInputCoder(), - fnInfo.getOutputCoders(), - fnInfo.getWindowingStrategy(), - stepContext, - userStepContext, - outputManager, - doFnSchemaInformation, - sideInputMapping); - - fnRunner.startBundle(); + }); + } } @Override @SuppressWarnings("unchecked") public void processElement(Object untypedElem) throws Exception { - if (fnRunner == null) { + if (helpers.fnRunner == null) { // If we need to run reallyStartBundle in here, we need to make sure to switch the state // sampler into the start state. - try (Closeable start = operationContext.enterStart()) { - reallyStartBundle(); + try (Closeable start = helpers.operationContext.enterStart()) { + helpers.reallyStartBundle(); + onStartKey(); } } + helpers.outputsPerElementTracker.onProcessElement(); WindowedValue elem = (WindowedValue) untypedElem; - - if (fnSignature != null && fnSignature.stateDeclarations().size() > 0) { - registerStateCleanup( - (WindowingStrategy) getDoFnInfo().getWindowingStrategy(), - (Collection) elem.getWindows()); + onProcessWindowedValue(elem); + + helpers.outputsPerElementTracker.onProcessElementSuccess(); + } + + protected void onProcessWindowedValue(WindowedValue elem) { + boolean hasState = helpers.hasState(); + + Collection windowsProcessed; + if (sideInputProcessor != null) { + windowsProcessed = hasState ? Lists.newArrayList() : Collections.emptyList(); + for (Iterator> it = + sideInputProcessor.handleProcessElement(elem); + it.hasNext(); ) { + WindowedValue toProcess = it.next(); + helpers.fnRunner.processElement(toProcess); + if (hasState) { + windowsProcessed.addAll((Collection) toProcess.getWindows()); + // If the element was blocked, don't register a cleanup timer. The timer will be + // registered + // when the window is unblocked ensuring that it is not processed until the element is. + } + } + } else { + helpers.fnRunner.processElement(elem); + windowsProcessed = (Collection) elem.getWindows(); + } + if (hasState) { + helpers.registerStateCleanup( + (WindowingStrategy) getDoFnInfo().getWindowingStrategy(), windowsProcessed); } - - outputsPerElementTracker.onProcessElement(); - fnRunner.processElement(elem); - outputsPerElementTracker.onProcessElementSuccess(); } @Override @@ -355,180 +177,33 @@ public void processTimers() throws Exception { // exist without actually decoding them. Coder windowCoder = (Coder) - (fnInfo != null ? fnInfo : doFnInstanceManager.peek()) + (helpers.fnInfo != null ? helpers.fnInfo : helpers.doFnInstanceManager.peek()) .getWindowingStrategy() .getWindowFn() .windowCoder(); - processTimers(TimerType.USER, userStepContext, windowCoder); - processTimers(TimerType.SYSTEM, stepContext, windowCoder); - } - - private void processUserTimer(TimerData timer) throws Exception { - if (fnSignature.timerDeclarations().containsKey(timer.getTimerId()) - || fnSignature.timerFamilyDeclarations().containsKey(timer.getTimerFamilyId())) { - BoundedWindow window = ((WindowNamespace) timer.getNamespace()).getWindow(); - fnRunner.onTimer( - timer.getTimerId(), - timer.getTimerFamilyId(), - this.stepContext.stateInternals().getKey(), - window, - timer.getTimestamp(), - timer.getOutputTimestamp(), - timer.getDomain(), - timer.causedByDrain()); - } - } - - private void processSystemTimer(TimerData timer) throws Exception { - - // Timer owned by this class, for cleaning up state in expired windows - if (timer.getTimerId().equals(CLEANUP_TIMER_ID)) { - checkState( - timer.getDomain().equals(TimeDomain.EVENT_TIME), - "%s received cleanup timer with domain not EVENT_TIME: %s", - this, - timer); - - checkState( - timer.getNamespace() instanceof WindowNamespace, - "%s received cleanup timer not for a %s: %s", - this, - WindowNamespace.class.getSimpleName(), - timer); - - BoundedWindow window = ((WindowNamespace) timer.getNamespace()).getWindow(); - Instant targetTime = earliestAllowableCleanupTime(window, fnInfo.getWindowingStrategy()); - - checkState( - !targetTime.isAfter(timer.getTimestamp()), - "%s received state cleanup timer for window %s " - + " that is before the appropriate cleanup time %s", - this, - window, - targetTime); - - fnRunner.onWindowExpiration( - window, timer.getOutputTimestamp(), this.stepContext.stateInternals().getKey()); - - // This is for a timer for a window that is expired, so clean it up. - for (StateDeclaration stateDecl : fnSignature.stateDeclarations().values()) { - StateTag tag; - try { - tag = - StateTags.tagForSpec( - stateDecl.id(), (StateSpec) stateDecl.field().get(fnInfo.getDoFn())); - } catch (IllegalAccessException e) { - throw new RuntimeException( - String.format( - "Error accessing %s for %s", - StateSpec.class.getName(), fnInfo.getDoFn().getClass().getName()), - e); - } - - StateInternals stateInternals = userStepContext.stateInternals(); - org.apache.beam.sdk.state.State state = stateInternals.state(timer.getNamespace(), tag); - state.clear(); - } - } + helpers.processTimers( + SimpleParDoFnHelpers.TimerType.USER, + helpers.userStepContext, + windowCoder, + this::onStartKey, + () -> sideInputProcessor); + helpers.processTimers( + SimpleParDoFnHelpers.TimerType.SYSTEM, + helpers.stepContext, + windowCoder, + this::onStartKey, + () -> sideInputProcessor); } @Override public void finishBundle() throws Exception { - if (fnRunner != null) { - fnRunner.finishBundle(); - doFnInstanceManager.complete(fnInfo); - fnRunner = null; - fnInfo = null; - fnSignature = null; - } + helpers.finishBundle(sideInputProcessor); + this.sideInputProcessor = null; } @Override public void abort() throws Exception { - doFnInstanceManager.abort(fnInfo); - fnRunner = null; - fnInfo = null; - } - - @VisibleForTesting static final String CLEANUP_TIMER_ID = "cleanup-timer"; - - private enum TimerType { - USER { - @Override - public void processTimer(SimpleParDoFn doFn, TimerData timer) throws Exception { - doFn.processUserTimer(timer); - } - }, - SYSTEM { - @Override - public void processTimer(SimpleParDoFn doFn, TimerData timer) throws Exception { - doFn.processSystemTimer(timer); - } - }; - - public abstract void processTimer(SimpleParDoFn doFn, TimerData timer) throws Exception; - }; - - private void processTimers( - TimerType mode, - DataflowExecutionContext.DataflowStepContext context, - Coder windowCoder) - throws Exception { - TimerData timer = context.getNextFiredTimer(windowCoder); - - if (timer != null && fnRunner == null) { - // If we need to run reallyStartBundle in here, we need to make sure to switch the state - // sampler into the start state. - try (Closeable start = operationContext.enterStart()) { - reallyStartBundle(); - } - } - - while (timer != null) { - mode.processTimer(this, timer); - timer = context.getNextFiredTimer(windowCoder); - } - } - - private void registerStateCleanup( - WindowingStrategy windowingStrategy, Collection windowsToCleanup) { - Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); - - for (W window : windowsToCleanup) { - // The stepContext is the thing that know if it is batch or streaming, hence - // whether state needs to be cleaned up or will simply be discarded so the - // timer can be ignored. - Instant cleanupTime = earliestAllowableCleanupTime(window, windowingStrategy); - // Set a cleanup timer for state at the end of the window to trigger onWindowExpiration and - // garbage collect state. We avoid doing this for the global window if there is no window - // expiration set as the state will be up when the pipeline terminates. Setting the timer - // leads to a unbounded growth of timers for pipelines with many unique keys in the global - // window. - if (cleanupTime.isBefore(GlobalWindow.INSTANCE.maxTimestamp()) - || fnSignature.onWindowExpiration() != null) { - // If the DoFn has OnWindowExpiration, then set the watermark hold so that the watermark - // does - // not advance until OnWindowExpiration completes. - Instant cleanupOutputTimestamp = - fnSignature.onWindowExpiration() == null - ? cleanupTime - : cleanupTime.minus(Duration.millis(1L)); - stepContext.setStateCleanupTimer( - CLEANUP_TIMER_ID, window, windowCoder, cleanupTime, cleanupOutputTimestamp); - } - } - } - - private Instant earliestAllowableCleanupTime( - BoundedWindow window, WindowingStrategy windowingStrategy) { - Instant cleanupTime = - window - .maxTimestamp() - .plus(windowingStrategy.getAllowedLateness()) - .plus(Duration.millis(1L)); - return cleanupTime.isAfter(BoundedWindow.TIMESTAMP_MAX_VALUE) - ? BoundedWindow.TIMESTAMP_MAX_VALUE - : cleanupTime; + helpers.abort(); } /** @@ -540,6 +215,6 @@ private Instant earliestAllowableCleanupTime( @VisibleForTesting @Nullable DoFnInfo getDoFnInfo() { - return fnInfo; + return helpers.fnInfo; } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java new file mode 100644 index 000000000000..964cf2323d51 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java @@ -0,0 +1,518 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + +import java.io.Closeable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.beam.runners.core.DoFnRunner; +import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.core.StateInternals; +import org.apache.beam.runners.core.StateNamespaces; +import org.apache.beam.runners.core.StateTag; +import org.apache.beam.runners.core.StateTags; +import org.apache.beam.runners.core.TimerInternals; +import org.apache.beam.runners.dataflow.options.DataflowPipelineDebugOptions; +import org.apache.beam.runners.dataflow.worker.counters.Counter; +import org.apache.beam.runners.dataflow.worker.counters.CounterFactory; +import org.apache.beam.runners.dataflow.worker.counters.CounterName; +import org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter; +import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputReceiver; +import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.StreamingOptions; +import org.apache.beam.sdk.state.State; +import org.apache.beam.sdk.state.StateSpec; +import org.apache.beam.sdk.state.TimeDomain; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature; +import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.util.DoFnInfo; +import org.apache.beam.sdk.util.WindowedValueMultiReceiver; +import org.apache.beam.sdk.values.PCollectionView; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({ + "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) + "nullness" // TODO(https://github.com/apache/beam/issues/20497) +}) +class SimpleParDoFnHelpers { + private static final Logger LOG = LoggerFactory.getLogger(SimpleParDoFnHelpers.class); + + // TODO: Remove once Distributions has shipped. + @VisibleForTesting + static final String OUTPUTS_PER_ELEMENT_EXPERIMENT = "outputs_per_element_counter"; + + private static final String COUNTER_NAME = "per-element-output-count"; + + final PipelineOptions options; + final DoFnInstanceManager doFnInstanceManager; + + private final SideInputReader sideInputReader; + final DataflowOperationContext operationContext; + private final TupleTag mainOutputTag; + private final Map, Integer> outputTupleTagsToReceiverIndices; + private final List> sideOutputTags; + final DataflowExecutionContext.DataflowStepContext stepContext; + final DataflowExecutionContext.DataflowStepContext userStepContext; + private final CounterFactory counterFactory; + private final DoFnRunnerFactory runnerFactory; + final boolean hasStreamingSideInput; + final OutputsPerElementTracker outputsPerElementTracker; + private final DoFnSchemaInformation doFnSchemaInformation; + private final Map> sideInputMapping; + + // Various DoFn helpers, null between bundles + @Nullable DoFnRunner fnRunner; + @Nullable DoFnInfo fnInfo; + private Receiver @Nullable [] receivers; + + // This may additionally be null if it is not a real DoFn but an OldDoFn or + // GroupAlsoByWindowViaWindowSetDoFn + protected @Nullable DoFnSignature fnSignature; + + SimpleParDoFnHelpers( + PipelineOptions options, + DoFnInstanceManager doFnInstanceManager, + SideInputReader sideInputReader, + TupleTag mainOutputTag, + Map, Integer> outputTupleTagsToReceiverIndices, + DataflowExecutionContext.DataflowStepContext stepContext, + DataflowOperationContext operationContext, + DoFnSchemaInformation doFnSchemaInformation, + Map> sideInputMapping, + DoFnRunnerFactory runnerFactory) { + this.options = options; + this.doFnInstanceManager = doFnInstanceManager; + + // We vend a freshly deserialized version for each run + this.sideInputReader = sideInputReader; + this.operationContext = operationContext; + checkArgument(!outputTupleTagsToReceiverIndices.isEmpty(), "expected at least one output"); + this.mainOutputTag = mainOutputTag; + this.outputTupleTagsToReceiverIndices = outputTupleTagsToReceiverIndices; + ImmutableList.Builder> sideOutputTagsBuilder = ImmutableList.builder(); + for (TupleTag tag : outputTupleTagsToReceiverIndices.keySet()) { + if (!mainOutputTag.equals(tag)) { + sideOutputTagsBuilder.add(tag); + } + } + this.sideOutputTags = sideOutputTagsBuilder.build(); + this.stepContext = stepContext; + + // StepContext provides a TimerInternals and StateInternals for use by the system - this class. + // For the user, we request a user-scoped StepContext to provide a user-scoped + // StateInternals and TimerInternals. + this.userStepContext = stepContext.namespacedToUser(); + + this.counterFactory = operationContext.counterFactory(); + this.runnerFactory = runnerFactory; + this.hasStreamingSideInput = + options.as(StreamingOptions.class).isStreaming() && !sideInputReader.isEmpty(); + this.outputsPerElementTracker = createOutputsPerElementTracker(); + this.doFnSchemaInformation = doFnSchemaInformation; + this.sideInputMapping = sideInputMapping; + } + + boolean hasState() { + return fnSignature != null && !fnSignature.stateDeclarations().isEmpty(); + } + + void startBundle(Receiver... receivers) throws Exception { + checkArgument( + receivers.length == outputTupleTagsToReceiverIndices.size(), + "unexpected number of receivers for DoFn"); + + this.receivers = receivers; + } + + void reallyStartBundle() throws Exception { + checkState(fnRunner == null, "bundle already started (or not properly finished)"); + + WindowedValueMultiReceiver outputManager = + new WindowedValueMultiReceiver() { + final Map, OutputReceiver> undeclaredOutputs = new HashMap<>(); + + private @Nullable Receiver getReceiverOrNull(TupleTag tag) { + Integer receiverIndex = outputTupleTagsToReceiverIndices.get(tag); + if (receiverIndex != null) { + return receivers[receiverIndex]; + } else { + return undeclaredOutputs.get(tag); + } + } + + @Override + public void output(TupleTag tag, WindowedValue output) { + outputsPerElementTracker.onOutput(); + Receiver receiver = getReceiverOrNull(tag); + if (receiver == null) { + // A new undeclared output. + // TODO: plumb through the operationName, so that we can + // name implicit outputs after it. + String outputName = "implicit-" + tag.getId(); + // TODO: plumb through the counter prefix, so we can + // make it available to the OutputReceiver class in case + // it wants to use it in naming output counterFactory. (It + // doesn't today.) + OutputReceiver undeclaredReceiver = new OutputReceiver(); + + ElementCounter outputCounter = + new DataflowOutputCounter( + outputName, counterFactory, stepContext.getNameContext()); + undeclaredReceiver.addOutputCounter(outputCounter); + undeclaredOutputs.put(tag, undeclaredReceiver); + receiver = undeclaredReceiver; + } + + try { + receiver.process(output); + } catch (RuntimeException | Error e) { + // Rethrow unchecked exceptions as-is to avoid excessive nesting + // via a chain of DoFn's. + throw e; + } catch (Exception e) { + // This should never happen in practice with DoFn's, but can happen + // with other Receivers. + throw new RuntimeException(e); + } + } + }; + fnInfo = (DoFnInfo) doFnInstanceManager.get(); + fnSignature = DoFnSignatures.getSignature(fnInfo.getDoFn().getClass()); + + fnRunner = + runnerFactory.createRunner( + fnInfo.getDoFn(), + options, + mainOutputTag, + sideOutputTags, + fnInfo.getSideInputViews(), + sideInputReader, + fnInfo.getInputCoder(), + fnInfo.getOutputCoders(), + fnInfo.getWindowingStrategy(), + stepContext, + userStepContext, + outputManager, + doFnSchemaInformation, + sideInputMapping); + fnRunner.startBundle(); + } + + void finishBundle(StreamingSideInputProcessor sideInputProcessor) throws Exception { + if (fnRunner != null) { + fnRunner.finishBundle(); + if (sideInputProcessor != null) { + sideInputProcessor.handleFinishBundle(); + } + doFnInstanceManager.complete(fnInfo); + fnRunner = null; + fnInfo = null; + fnSignature = null; + sideInputProcessor = null; + } + } + + void abort() throws Exception { + doFnInstanceManager.abort(fnInfo); + fnRunner = null; + fnInfo = null; + } + + @VisibleForTesting static final String CLEANUP_TIMER_ID = "cleanup-timer"; + + enum TimerType { + USER { + @Override + public void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception { + doFn.processUserTimer(timer, sideInputProcessor.get()); + } + }, + FAIL_USER { + @Override + public void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception { + throw new UnsupportedOperationException( + "Attempt to deliver a timer to a DoFn, but timers are not supported here."); + } + }, + SYSTEM { + @Override + public void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception { + doFn.processSystemTimer(timer, sideInputProcessor.get()); + } + }; + + public abstract void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception; + }; + + void processTimers( + TimerType mode, + DataflowExecutionContext.DataflowStepContext context, + Coder windowCoder, + Runnable startKey, + Supplier> sideInputProcessor) + throws Exception { + TimerInternals.TimerData timer = context.getNextFiredTimer(windowCoder); + + if (timer != null && fnRunner == null) { + // If we need to run reallyStartBundle in here, we need to make sure to switch the state + // sampler into the start state. + try (Closeable start = operationContext.enterStart()) { + reallyStartBundle(); + startKey.run(); + } + } + + while (timer != null) { + mode.processTimer(this, timer, sideInputProcessor); + timer = context.getNextFiredTimer(windowCoder); + } + } + + protected void processUserTimer( + TimerInternals.TimerData timer, StreamingSideInputProcessor sideInputProcessor) { + if (fnSignature.timerDeclarations().containsKey(timer.getTimerId()) + || fnSignature.timerFamilyDeclarations().containsKey(timer.getTimerFamilyId())) { + BoundedWindow window = ((StateNamespaces.WindowNamespace) timer.getNamespace()).getWindow(); + if (sideInputProcessor != null) { + sideInputProcessor.handleProcessTimer(timer); + } + fnRunner.onTimer( + timer.getTimerId(), + timer.getTimerFamilyId(), + this.stepContext.stateInternals().getKey(), + window, + timer.getTimestamp(), + timer.getOutputTimestamp(), + timer.getDomain(), + timer.causedByDrain()); + } + } + + private void processSystemTimer( + TimerInternals.TimerData timer, StreamingSideInputProcessor sideInputProcessor) + throws Exception { + // Timer owned by this class, for cleaning up state in expired windows + if (timer.getTimerId().equals(CLEANUP_TIMER_ID)) { + checkState( + timer.getDomain().equals(TimeDomain.EVENT_TIME), + "%s received cleanup timer with domain not EVENT_TIME: %s", + this, + timer); + + checkState( + timer.getNamespace() instanceof StateNamespaces.WindowNamespace, + "%s received cleanup timer not for a %s: %s", + this, + StateNamespaces.WindowNamespace.class.getSimpleName(), + timer); + + if (sideInputProcessor != null) { + // We must call this to ensure the side-input is cached for onWindowExpiration. Since we + // don't set cleanup + // timers until we actually call processElement, the window must be unblocked here. + sideInputProcessor.handleProcessTimer(timer); + } + + BoundedWindow window = ((StateNamespaces.WindowNamespace) timer.getNamespace()).getWindow(); + Instant targetTime = earliestAllowableCleanupTime(window, fnInfo.getWindowingStrategy()); + + checkState( + !targetTime.isAfter(timer.getTimestamp()), + "%s received state cleanup timer for window %s " + + " that is before the appropriate cleanup time %s", + this, + window, + targetTime); + + fnRunner.onWindowExpiration( + window, timer.getOutputTimestamp(), this.stepContext.stateInternals().getKey()); + + // This is for a timer for a window that is expired, so clean it up. + for (DoFnSignature.StateDeclaration stateDecl : fnSignature.stateDeclarations().values()) { + StateTag tag; + try { + tag = + StateTags.tagForSpec( + stateDecl.id(), (StateSpec) stateDecl.field().get(fnInfo.getDoFn())); + } catch (IllegalAccessException e) { + throw new RuntimeException( + String.format( + "Error accessing %s for %s", + StateSpec.class.getName(), fnInfo.getDoFn().getClass().getName()), + e); + } + + StateInternals stateInternals = userStepContext.stateInternals(); + State state = stateInternals.state(timer.getNamespace(), tag); + state.clear(); + } + } + } + + private OutputsPerElementTracker createOutputsPerElementTracker() { + // TODO: Remove once Distributions has shipped. + if (!hasExperiment(OUTPUTS_PER_ELEMENT_EXPERIMENT)) { + return NoopOutputsPerElementTracker.INSTANCE; + } + + // TODO: Remove log statement when functionality is enabled by default. + LOG.info("{} counter enabled.", COUNTER_NAME); + + return new OutputsPerElementTrackerImpl(); + } + + private boolean hasExperiment(String experiment) { + List experiments = options.as(DataflowPipelineDebugOptions.class).getExperiments(); + return experiments != null && experiments.contains(experiment); + } + + /** Simple state tracker to calculate PerElementOutputCount counter. */ + interface OutputsPerElementTracker { + + void onOutput(); + + void onProcessElement(); + + void onProcessElementSuccess(); + } + + private class OutputsPerElementTrackerImpl implements OutputsPerElementTracker { + + private long outputsPerElement; + private final Counter counter; + + public OutputsPerElementTrackerImpl() { + this.counter = + counterFactory.distribution( + CounterName.named(COUNTER_NAME).withOriginalName(stepContext.getNameContext())); + } + + @Override + public void onProcessElement() { + reset(); + } + + @Override + public void onOutput() { + outputsPerElement++; + } + + @Override + public void onProcessElementSuccess() { + counter.addValue(outputsPerElement); + reset(); + } + + private void reset() { + outputsPerElement = 0L; + } + } + + /** No-op {@link OutputsPerElementTracker} implementation used when the counter is disabled. */ + private static class NoopOutputsPerElementTracker implements OutputsPerElementTracker { + + private NoopOutputsPerElementTracker() {} + + public static final OutputsPerElementTracker INSTANCE = new NoopOutputsPerElementTracker(); + + @Override + public void onOutput() {} + + @Override + public void onProcessElement() {} + + @Override + public void onProcessElementSuccess() {} + } + + Instant earliestAllowableCleanupTime(BoundedWindow window, WindowingStrategy windowingStrategy) { + Instant cleanupTime = + window + .maxTimestamp() + .plus(windowingStrategy.getAllowedLateness()) + .plus(Duration.millis(1L)); + return cleanupTime.isAfter(BoundedWindow.TIMESTAMP_MAX_VALUE) + ? BoundedWindow.TIMESTAMP_MAX_VALUE + : cleanupTime; + } + + protected void registerStateCleanup( + WindowingStrategy windowingStrategy, Collection windowsToCleanup) { + Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); + + for (W window : windowsToCleanup) { + // The stepContext is the thing that know if it is batch or streaming, hence + // whether state needs to be cleaned up or will simply be discarded so the + // timer can be ignored. + Instant cleanupTime = earliestAllowableCleanupTime(window, windowingStrategy); + // Set a cleanup timer for state at the end of the window to trigger onWindowExpiration and + // garbage collect state. We avoid doing this for the global window if there is no window + // expiration set as the state will be up when the pipeline terminates. Setting the timer + // leads to a unbounded growth of timers for pipelines with many unique keys in the global + // window. + if (cleanupTime.isBefore(GlobalWindow.INSTANCE.maxTimestamp()) + || fnSignature.onWindowExpiration() != null) { + // If the DoFn has OnWindowExpiration, then set the watermark hold so that the watermark + // does + // not advance until OnWindowExpiration completes. + Instant cleanupOutputTimestamp = + fnSignature.onWindowExpiration() == null + ? cleanupTime + : cleanupTime.minus(Duration.millis(1L)); + stepContext.setStateCleanupTimer( + CLEANUP_TIMER_ID, window, windowCoder, cleanupTime, cleanupOutputTimestamp); + } + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java index 3ad443ee2a2b..b55d73cf5793 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java @@ -44,7 +44,6 @@ import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.DoFnSchemaInformation; import org.apache.beam.sdk.util.DoFnInfo; @@ -65,7 +64,8 @@ }) class SplittableProcessFnFactory { static final ParDoFnFactory createDefault() { - return new UserParDoFnFactory(new ProcessFnExtractor(), new SplittableDoFnRunnerFactory()); + return new UserParDoFnFactory( + new ProcessFnExtractor(), new SplittableDoFnRunnerFactory(), true); } private static class ProcessFnExtractor implements UserParDoFnFactory.DoFnExtractor { @@ -174,22 +174,6 @@ public DoFnRunner>, OutputT> crea sideInputMapping); DoFnRunner>, OutputT> fnRunner = new DataflowProcessFnRunner<>(simpleRunner); - boolean hasStreamingSideInput = - options.as(StreamingOptions.class).isStreaming() && !sideInputReader.isEmpty(); - KeyedWorkItemCoder> kwiCoder = - (KeyedWorkItemCoder>) inputCoder; - if (hasStreamingSideInput) { - fnRunner = - new StreamingKeyedWorkItemSideInputDoFnRunner<>( - fnRunner, - ByteArrayCoder.of(), - new StreamingSideInputFetcher<>( - sideInputViews, - kwiCoder.getElementCoder(), - processFn.getInputWindowingStrategy(), - (StreamingModeExecutionContext.StreamingModeStepContext) userStepContext), - userStepContext); - } return fnRunner; } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFn.java new file mode 100644 index 000000000000..225bc6af0ea9 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFn.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import com.google.api.client.util.Lists; +import com.google.common.collect.Iterables; +import java.io.Closeable; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.beam.runners.core.KeyedWorkItem; +import org.apache.beam.runners.core.KeyedWorkItems; +import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.core.StateNamespaces; +import org.apache.beam.runners.core.StateTag; +import org.apache.beam.runners.core.StateTags; +import org.apache.beam.runners.dataflow.worker.util.ValueInEmptyWindows; +import org.apache.beam.runners.dataflow.worker.util.common.worker.ParDoFn; +import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.state.ValueState; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.util.DoFnInfo; +import org.apache.beam.sdk.values.PCollectionView; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.Nullable; + +@SuppressWarnings({ + "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) + "nullness" // TODO(https://github.com/apache/beam/issues/20497) +}) +/* Similar to {@link SimpleParDoFn} but for splittable ProcessFns. */ +public class StreamingKeyedWorkItemSideInputParDoFn + implements ParDoFn { + private final StateTag> keyAddr; + private final Coder inputCoder; + private final SimpleParDoFnHelpers, OutputT, W> helpers; + protected @Nullable StreamingSideInputProcessor sideInputProcessor; + + StreamingKeyedWorkItemSideInputParDoFn( + PipelineOptions options, + DoFnInstanceManager doFnInstanceManager, + SideInputReader sideInputReader, + TupleTag mainOutputTag, + Map, Integer> outputTupleTagsToReceiverIndices, + DataflowExecutionContext.DataflowStepContext stepContext, + DataflowOperationContext operationContext, + DoFnSchemaInformation doFnSchemaInformation, + Map> sideInputMapping, + DoFnRunnerFactory runnerFactory, + Coder keyCoder, + Coder inputCoder) { + helpers = + new SimpleParDoFnHelpers<>( + options, + doFnInstanceManager, + sideInputReader, + mainOutputTag, + outputTupleTagsToReceiverIndices, + stepContext, + operationContext, + doFnSchemaInformation, + sideInputMapping, + runnerFactory); + this.keyAddr = StateTags.makeSystemTagInternal(StateTags.value("key", keyCoder)); + this.inputCoder = inputCoder; + } + + ValueState keyValue() { + return helpers.stepContext.stateInternals().state(StateNamespaces.global(), keyAddr); + } + + @Override + public void startBundle(Receiver... receivers) throws Exception { + helpers.startBundle(receivers); + if (helpers.hasStreamingSideInput) { + // There is non-trivial setup that needs to be performed for watermark propagation + // even on empty bundles. + helpers.reallyStartBundle(); + onStartKey(); + } + } + + protected void onStartKey() { + if (helpers.hasStreamingSideInput) { + sideInputProcessor = + new StreamingSideInputProcessor<>( + new StreamingSideInputFetcher( + helpers.fnInfo.getSideInputViews(), + inputCoder, + (WindowingStrategy) helpers.fnInfo.getWindowingStrategy(), + (StreamingModeExecutionContext.StreamingModeStepContext) + helpers.userStepContext)); + } + + if (sideInputProcessor != null) { + boolean hasState = helpers.hasState(); + + // TODO(relax): We should be able to get this without writing it to state! + @Nullable K key = keyValue().read(); + if (key != null) { + sideInputProcessor.tryUnblockElementsAndTimers( + (unblockedElements, unblockedTimers) -> { + if (!Iterables.isEmpty(unblockedElements) || !Iterables.isEmpty(unblockedTimers)) { + helpers.fnRunner.processElement( + new ValueInEmptyWindows<>( + KeyedWorkItems.workItem(key, unblockedTimers, unblockedElements))); + } + if (hasState) { + List windows = + (List) + StreamSupport.stream(unblockedElements.spliterator(), false) + .flatMap(wv -> wv.getWindows().stream()) + .collect(Collectors.toList()); + // These elements are now processed. Register cleanup timers for all the unblocked + // windows. + helpers.registerStateCleanup( + (WindowingStrategy) getDoFnInfo().getWindowingStrategy(), windows); + } + }); + } + } + } + + @Override + @SuppressWarnings("unchecked") + public void processElement(Object untypedElem) throws Exception { + if (helpers.fnRunner == null) { + // If we need to run reallyStartBundle in here, we need to make sure to switch the state + // sampler into the start state. + try (Closeable start = helpers.operationContext.enterStart()) { + helpers.reallyStartBundle(); + onStartKey(); + } + } + helpers.outputsPerElementTracker.onProcessElement(); + + WindowedValue> elem = + (WindowedValue>) untypedElem; + onProcessWindowedValue(elem); + + helpers.outputsPerElementTracker.onProcessElementSuccess(); + } + + @Override + public void processTimers() throws Exception { + + // Note: We need to get windowCoder to decode the timers. If we haven't already deserialized + // the fnInfo, we peek at a new instance to retrieve that. If this extra deserialization becomes + // excessively costly, we could either (1) have the DoFnInstanceManager remember the associated + // windowCoder (allowing us to get it without a DoFnInfo instance) or (2) check whether timers + // exist without actually decoding them. + Coder windowCoder = + (Coder) + (helpers.fnInfo != null ? helpers.fnInfo : helpers.doFnInstanceManager.peek()) + .getWindowingStrategy() + .getWindowFn() + .windowCoder(); + helpers.processTimers( + SimpleParDoFnHelpers.TimerType.FAIL_USER, + helpers.userStepContext, + windowCoder, + this::onStartKey, + () -> sideInputProcessor); + helpers.processTimers( + SimpleParDoFnHelpers.TimerType.SYSTEM, + helpers.stepContext, + windowCoder, + this::onStartKey, + () -> sideInputProcessor); + } + + @Override + public void finishBundle() throws Exception { + helpers.finishBundle(sideInputProcessor); + this.sideInputProcessor = null; + } + + @Override + public void abort() throws Exception { + helpers.abort(); + } + + protected void onProcessWindowedValue(WindowedValue> elem) { + // TODO: Get rid of this! + final K key = elem.getValue().key(); + keyValue().write(key); + + boolean hasState = helpers.hasState(); + Collection windowsProcessed; + if (sideInputProcessor != null) { + windowsProcessed = hasState ? Lists.newArrayList() : Collections.emptyList(); + WindowedValue> unblocked = + sideInputProcessor.handleProcessKeyedWorkItem(elem); + if (!Iterables.isEmpty(unblocked.getValue().elementsIterable()) + || !Iterables.isEmpty(unblocked.getValue().timersIterable())) { + helpers.fnRunner.processElement(unblocked); + } + if (hasState) { + windowsProcessed.addAll((Collection) unblocked.getWindows()); + } + } else { + helpers.fnRunner.processElement(elem); + windowsProcessed = (Collection) elem.getWindows(); + } + if (hasState) { + helpers.registerStateCleanup( + (WindowingStrategy) getDoFnInfo().getWindowingStrategy(), windowsProcessed); + } + } + + /** + * Returns the {@link DoFnInfo} currently being used by this {@link SimpleParDoFn}. + * + *

    May be null if no element has been processed yet, or if the {@link SimpleParDoFn} has + * finished. + */ + @VisibleForTesting + @Nullable + DoFnInfo getDoFnInfo() { + return helpers.fnInfo; + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunner.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunner.java index 3b7891c5378d..a64d1a970d34 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunner.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunner.java @@ -17,9 +17,8 @@ */ package org.apache.beam.runners.dataflow.worker; -import java.util.Set; +import java.util.Iterator; import org.apache.beam.runners.core.DoFnRunner; -import org.apache.beam.sdk.state.BagState; import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; @@ -37,43 +36,33 @@ public class StreamingSideInputDoFnRunner implements DoFnRunner { private final DoFnRunner simpleDoFnRunner; - private final StreamingSideInputFetcher sideInputFetcher; + private final StreamingSideInputProcessor sideInputProcessor; public StreamingSideInputDoFnRunner( DoFnRunner simpleDoFnRunner, StreamingSideInputFetcher sideInputFetcher) { this.simpleDoFnRunner = simpleDoFnRunner; - this.sideInputFetcher = sideInputFetcher; + this.sideInputProcessor = new StreamingSideInputProcessor<>(sideInputFetcher); } @Override public void startBundle() { simpleDoFnRunner.startBundle(); - sideInputFetcher.prefetchBlockedMap(); - - // Find the set of ready windows. - Set readyWindows = sideInputFetcher.getReadyWindows(); - - Iterable>> elementsBags = - sideInputFetcher.prefetchElements(readyWindows); - - // Run the DoFn code now that all side inputs are ready. - for (BagState> elementsBag : elementsBags) { - Iterable> elements = elementsBag.read(); - for (WindowedValue elem : elements) { - simpleDoFnRunner.processElement(elem); - } - elementsBag.clear(); - } - sideInputFetcher.releaseBlockedWindows(readyWindows); + sideInputProcessor.tryUnblockElements( + unblocked -> { + for (WindowedValue elem : unblocked) { + simpleDoFnRunner.processElement(elem); + } + }); } @Override public void processElement(WindowedValue compressedElem) { - for (WindowedValue elem : compressedElem.explodeWindows()) { - if (!sideInputFetcher.storeIfBlocked(elem)) { - simpleDoFnRunner.processElement(elem); - } + for (Iterator> it = + sideInputProcessor.handleProcessElement(compressedElem); + it.hasNext(); ) { + WindowedValue elem = it.next(); + simpleDoFnRunner.processElement(elem); } } @@ -94,7 +83,7 @@ public void onTimer( @Override public void finishBundle() { simpleDoFnRunner.finishBundle(); - sideInputFetcher.persist(); + sideInputProcessor.handleFinishBundle(); } @Override diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputFetcher.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputFetcher.java index 76913baa6aa7..0369b82be730 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputFetcher.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputFetcher.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -83,7 +84,6 @@ public StreamingSideInputFetcher( this.stepContext = stepContext; this.mainWindowCoder = windowingStrategy.getWindowFn().windowCoder(); - this.sideInputViews = new HashMap<>(); for (PCollectionView view : views) { sideInputViews.put(view.getTagInternal().getId(), view); @@ -188,11 +188,7 @@ public Iterable> prefetchTimers(Iterable readyWindows) { return timers; } - /** Compute the set of side inputs that are not yet ready for the given main input window. */ - public boolean storeIfBlocked(WindowedValue elem) { - @SuppressWarnings("unchecked") - W window = (W) Iterables.getOnlyElement(elem.getWindows()); - + private Set checkIfBlocked(W window) { Set blocked = blockedMap().get(window); if (blocked == null) { for (PCollectionView view : sideInputViews.values()) { @@ -205,7 +201,16 @@ public boolean storeIfBlocked(WindowedValue elem) { } } } - if (blocked != null) { + return blocked == null ? Collections.emptySet() : blocked; + } + + /** Compute the set of side inputs that are not yet ready for the given main input window. */ + public boolean storeIfBlocked(WindowedValue elem) { + @SuppressWarnings("unchecked") + W window = (W) Iterables.getOnlyElement(elem.getWindows()); + + Set blocked = checkIfBlocked(window); + if (!blocked.isEmpty()) { elementBag(window).add(elem); watermarkHold(window).add(elem.getTimestamp()); stepContext.addBlockingSideInputs(blocked); @@ -223,17 +228,12 @@ public boolean storeIfBlocked(TimerData timer) { @SuppressWarnings("unchecked") WindowNamespace windowNamespace = (WindowNamespace) timer.getNamespace(); W window = windowNamespace.getWindow(); - - boolean blocked = false; - for (PCollectionView view : sideInputViews.values()) { - if (!stepContext.issueSideInputFetch(view, window, SideInputState.UNKNOWN)) { - blocked = true; - } - } - if (blocked) { + Set blocked = checkIfBlocked(window); + if (!blocked.isEmpty()) { timerBag(window).add(timer); + return true; } - return blocked; + return false; } public void persist() { @@ -332,7 +332,7 @@ private Windmill.GlobalDataRequest buildGlob .build(); } - private static class GlobalDataRequestCoder extends AtomicCoder { + static class GlobalDataRequestCoder extends AtomicCoder { private final Class protoMessageClass = Windmill.GlobalDataRequest.class; private transient Parser memoizedParser; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessor.java new file mode 100644 index 000000000000..34c1a06d54de --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessor.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.apache.beam.runners.core.KeyedWorkItem; +import org.apache.beam.runners.core.KeyedWorkItems; +import org.apache.beam.runners.core.TimerInternals; +import org.apache.beam.sdk.state.BagState; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; + +/** Helper class for handling elements blocked on side inputs. */ +@SuppressWarnings("nullness" // TODO(https://github.com/apache/beam/issues/20497) +) +class StreamingSideInputProcessor { + private final StreamingSideInputFetcher sideInputFetcher; + + public StreamingSideInputProcessor(StreamingSideInputFetcher sideInputFetcher) { + this.sideInputFetcher = sideInputFetcher; + } + + /** + * Handle's startBundle. If there are unblocked elements, process them and then return the set of + * windows that were unblocked. + */ + void tryUnblockElements(Consumer>> consumer) { + sideInputFetcher.prefetchBlockedMap(); + + // Find the set of ready windows. + Set readyWindows = sideInputFetcher.getReadyWindows(); + + Iterable>> elementBags = + sideInputFetcher.prefetchElements(readyWindows); + Iterable> releasedElements = + Iterables.concat(Iterables.transform(elementBags, BagState::read)); + consumer.accept(releasedElements); + elementBags.forEach(BagState::clear); + sideInputFetcher.releaseBlockedWindows(readyWindows); + } + + void tryUnblockElementsAndTimers( + BiConsumer>, Iterable> consumer) { + sideInputFetcher.prefetchBlockedMap(); + + // Find the set of ready windows. + Set readyWindows = sideInputFetcher.getReadyWindows(); + + Iterable> timerBags = + sideInputFetcher.prefetchTimers(readyWindows); + Iterable releasedTimers = + Iterables.concat( + Iterables.transform(sideInputFetcher.prefetchTimers(readyWindows), BagState::read)); + Iterable>> elementBags = + sideInputFetcher.prefetchElements(readyWindows); + Iterable> releasedElements = + Iterables.concat(Iterables.transform(elementBags, BagState::read)); + + consumer.accept(releasedElements, releasedTimers); + timerBags.forEach(BagState::clear); + elementBags.forEach(BagState::clear); + sideInputFetcher.releaseBlockedWindows(readyWindows); + } + + void handleFinishBundle() { + sideInputFetcher.persist(); + } + + /* + Handle process element. Runs the elements that have an available side input, and buffers elements for which the + side input is blocked. Returns the list of elements that are unblocked and should be processed. + */ + Iterator> handleProcessElement( + WindowedValue compressedElem) { + // Note: We could write this as a three-line stream expression, but side effects are discouraged + // in Java streams. + return Iterators.filter( + compressedElem.explodeWindows().iterator(), + (WindowedValue e) -> !sideInputFetcher.storeIfBlocked(e)); + } + + WindowedValue> handleProcessKeyedWorkItem( + WindowedValue> elem) { + List> readyInputs = + Lists.newArrayList( + Iterables.filter( + elem.getValue().elementsIterable(), + input -> !sideInputFetcher.storeIfBlocked(input))); + + List readyTimers = + Lists.newArrayList( + Iterables.filter( + elem.getValue().timersIterable(), + timer -> !sideInputFetcher.storeIfBlocked(timer))); + KeyedWorkItem keyedWorkItem = + KeyedWorkItems.workItem(elem.getValue().key(), readyTimers, readyInputs); + + return elem.withValue(keyedWorkItem); + } + + void handleProcessTimer(TimerInternals.TimerData timer) { + // We must call this to ensure the side-input is cached for the timer. However since a user + // timer can only + // be set via element processing (or another timer) in the same window, the window should be + // unblocked once + // we get here. + Preconditions.checkState(!sideInputFetcher.storeIfBlocked(timer)); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactory.java index a8d5975e45ea..9466ad60d414 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactory.java @@ -24,18 +24,22 @@ import com.google.api.services.dataflow.model.SideInputInfo; import java.util.List; import java.util.Map; +import org.apache.beam.runners.core.KeyedWorkItemCoder; import org.apache.beam.runners.core.SideInputReader; import org.apache.beam.runners.dataflow.BatchStatefulParDoOverrides; import org.apache.beam.runners.dataflow.DataflowRunner; import org.apache.beam.runners.dataflow.util.CloudObject; import org.apache.beam.runners.dataflow.util.PropertyNames; import org.apache.beam.runners.dataflow.worker.util.common.worker.ParDoFn; +import org.apache.beam.sdk.coders.ByteArrayCoder; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.DoFnInfo; import org.apache.beam.sdk.util.SerializableUtils; +import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; @@ -52,7 +56,7 @@ }) class UserParDoFnFactory implements ParDoFnFactory { static UserParDoFnFactory createDefault() { - return new UserParDoFnFactory(new UserDoFnExtractor(), SimpleDoFnRunnerFactory.INSTANCE); + return new UserParDoFnFactory(new UserDoFnExtractor(), SimpleDoFnRunnerFactory.INSTANCE, false); } interface DoFnExtractor { @@ -74,10 +78,15 @@ private static class UserDoFnExtractor implements DoFnExtractor { private final DoFnExtractor doFnExtractor; private final DoFnRunnerFactory runnerFactory; + private final boolean streamingKeyedWorkItem; - UserParDoFnFactory(DoFnExtractor doFnExtractor, DoFnRunnerFactory runnerFactory) { + UserParDoFnFactory( + DoFnExtractor doFnExtractor, + DoFnRunnerFactory runnerFactory, + boolean streamingKeyedWorkItem) { this.doFnExtractor = doFnExtractor; this.runnerFactory = runnerFactory; + this.streamingKeyedWorkItem = streamingKeyedWorkItem; } @Override @@ -144,17 +153,38 @@ public ParDoFn create( writerFn.getDataCoder(), (Coder) doFnInfo.getWindowingStrategy().getWindowFn().windowCoder()); } else { - return new SimpleParDoFn( - options, - instanceManager, - sideInputReader, - doFnInfo.getMainOutput(), - outputTupleTagsToReceiverIndices, - stepContext, - operationContext, - doFnInfo.getDoFnSchemaInformation(), - doFnInfo.getSideInputMapping(), - runnerFactory); + boolean hasStreamingSideInput = + options.as(StreamingOptions.class).isStreaming() && !sideInputReader.isEmpty(); + + if (streamingKeyedWorkItem && hasStreamingSideInput) { + KeyedWorkItemCoder> kwiCoder = + (KeyedWorkItemCoder>) doFnInfo.getInputCoder(); + return new StreamingKeyedWorkItemSideInputParDoFn<>( + options, + instanceManager, + sideInputReader, + doFnInfo.getMainOutput(), + outputTupleTagsToReceiverIndices, + stepContext, + operationContext, + doFnInfo.getDoFnSchemaInformation(), + doFnInfo.getSideInputMapping(), + runnerFactory, + ByteArrayCoder.of(), + kwiCoder.getElementCoder()); + } else { + return new SimpleParDoFn( + options, + instanceManager, + sideInputReader, + doFnInfo.getMainOutput(), + outputTupleTagsToReceiverIndices, + stepContext, + operationContext, + doFnInfo.getDoFnSchemaInformation(), + doFnInfo.getSideInputMapping(), + runnerFactory); + } } } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpersTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpersTest.java new file mode 100644 index 000000000000..6bbbf953967d --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpersTest.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.beam.runners.core.DoFnRunner; +import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.dataflow.worker.DataflowExecutionContext.DataflowStepContext; +import org.apache.beam.runners.dataflow.worker.counters.CounterFactory; +import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.util.DoFnInfo; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(JUnit4.class) +public class SimpleParDoFnHelpersTest { + private PipelineOptions options; + @Mock DoFnInstanceManager doFnInstanceManager; + @Mock SideInputReader sideInputReader; + @Mock DataflowStepContext stepContext; + @Mock DataflowStepContext userStepContext; + @Mock DataflowOperationContext operationContext; + @Mock DoFnRunnerFactory runnerFactory; + @Mock DoFnRunner mockRunner; + + @Mock StreamingSideInputProcessor sideInputProcessor; + + @Mock DoFnInfo doFnInfo; + @Mock CounterFactory counterFactory; + + private static class TestDoFn extends DoFn { + @ProcessElement + public void processElement() {} + } + + private TestDoFn doFn = new TestDoFn(); + + private SimpleParDoFnHelpers helpers; + + @Before + @SuppressWarnings("unchecked") + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + options = PipelineOptionsFactory.create(); + when(stepContext.namespacedToUser()).thenReturn(userStepContext); + when(operationContext.counterFactory()).thenReturn(counterFactory); + + when(doFnInstanceManager.get()).thenReturn((DoFnInfo) doFnInfo); + when(doFnInfo.getDoFn()).thenReturn(doFn); + + when(runnerFactory.createRunner( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any())) + .thenReturn(mockRunner); + + helpers = + new SimpleParDoFnHelpers<>( + options, + doFnInstanceManager, + sideInputReader, + new TupleTag<>("main"), + ImmutableMap.of(new TupleTag<>("main"), 0), + stepContext, + operationContext, + DoFnSchemaInformation.create(), + ImmutableMap.of(), + runnerFactory); + } + + @Test + public void testReallyStartBundle() throws Exception { + helpers.startBundle(mock(Receiver.class)); + helpers.reallyStartBundle(); + + verify(runnerFactory) + .createRunner( + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any()); + verify(mockRunner).startBundle(); + } + + @Test + public void testFinishBundle() throws Exception { + helpers.startBundle(mock(Receiver.class)); + helpers.reallyStartBundle(); + + helpers.finishBundle(sideInputProcessor); + + verify(mockRunner).finishBundle(); + verify(sideInputProcessor).handleFinishBundle(); + verify(doFnInstanceManager).complete(any()); + } + + @Test + public void testAbort() throws Exception { + helpers.startBundle(mock(Receiver.class)); + helpers.reallyStartBundle(); + + helpers.abort(); + + verify(doFnInstanceManager).abort(any()); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnTest.java index 9c9f5386f443..5b1d7a8d3664 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnTest.java @@ -92,7 +92,7 @@ public void setUp() { // TODO: Remove once Distributions has shipped. options .as(DataflowPipelineDebugOptions.class) - .setExperiments(Lists.newArrayList(SimpleParDoFn.OUTPUTS_PER_ELEMENT_EXPERIMENT)); + .setExperiments(Lists.newArrayList(SimpleParDoFnHelpers.OUTPUTS_PER_ELEMENT_EXPERIMENT)); operationContext = TestOperationContext.create(); stepContext = @@ -489,7 +489,7 @@ public void startBundle() throws Exception { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { + public void processElement() throws Exception { assertThat(startCalled, equalTo(true)); assertThat(tracker.getCurrentState(), equalTo(operationContext.getProcessState())); } @@ -558,7 +558,7 @@ public void testOutputsPerElementCounter() throws Exception { public void testOutputsPerElementCounterDisabledViaExperiment() throws Exception { DataflowPipelineDebugOptions debugOptions = options.as(DataflowPipelineDebugOptions.class); List experiments = debugOptions.getExperiments(); - experiments.remove(SimpleParDoFn.OUTPUTS_PER_ELEMENT_EXPERIMENT); + experiments.remove(SimpleParDoFnHelpers.OUTPUTS_PER_ELEMENT_EXPERIMENT); debugOptions.setExperiments(experiments); List counterUpdates = executeParDoFnCounterTest(0); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunnerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunnerTest.java index e12ddd95f913..654707aae912 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunnerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunnerTest.java @@ -202,8 +202,7 @@ private IntervalWindow window(long start, long end) { (WindowedValue> windowedValue) -> outputManager.output(mainOutputTag, windowedValue), stepContext); - return new StreamingKeyedWorkItemSideInputDoFnRunner< - String, Integer, KV, IntervalWindow>( + return new StreamingKeyedWorkItemSideInputDoFnRunner<>( simpleDoFnRunner, keyCoder, sideInputFetcher, stepContext); } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFnTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFnTest.java new file mode 100644 index 000000000000..2fed6fd405a5 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFnTest.java @@ -0,0 +1,488 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import org.apache.beam.runners.core.DoFnRunner; +import org.apache.beam.runners.core.InMemoryStateInternals; +import org.apache.beam.runners.core.KeyedWorkItem; +import org.apache.beam.runners.core.KeyedWorkItems; +import org.apache.beam.runners.core.OutputAndTimeBoundedSplittableProcessElementInvoker; +import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.core.SimpleDoFnRunner; +import org.apache.beam.runners.core.SplittableParDoViaKeyedWorkItems.ProcessFn; +import org.apache.beam.runners.core.StateInternals; +import org.apache.beam.runners.core.StateNamespaces; +import org.apache.beam.runners.core.TimerInternals; +import org.apache.beam.runners.core.TimerInternals.TimerData; +import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputState; +import org.apache.beam.runners.dataflow.worker.util.ValueInEmptyWindows; +import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.Timer; +import org.apache.beam.sdk.coders.BigEndianIntegerCoder; +import org.apache.beam.sdk.coders.ByteArrayCoder; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.options.StreamingOptions; +import org.apache.beam.sdk.state.TimeDomain; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.View; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.transforms.splittabledofn.SplitResult; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.util.CoderUtils; +import org.apache.beam.sdk.util.DoFnInfo; +import org.apache.beam.sdk.util.WindowedValueMultiReceiver; +import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollectionView; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link StreamingKeyedWorkItemSideInputParDoFn}. */ +@RunWith(JUnit4.class) +public class StreamingKeyedWorkItemSideInputParDoFnTest { + private static final FixedWindows WINDOW_FN = FixedWindows.of(Duration.millis(10)); + private static final TupleTag> MAIN_OUTPUT_TAG = new TupleTag<>(); + + private final InMemoryStateInternals state = InMemoryStateInternals.forKey("a"); + + @Mock private StreamingModeExecutionContext.StepContext stepContext; + @Mock private TimerInternals mockTimerInternals; + @Mock private SideInputReader mockSideInputReader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(stepContext.stateInternals()).thenReturn((StateInternals) state); + when(stepContext.timerInternals()).thenReturn(mockTimerInternals); + when(stepContext.namespacedToUser()).thenReturn(stepContext); + when(mockSideInputReader.isEmpty()).thenReturn(false); + } + + @Test + public void testInvokeProcessElement() throws Exception { + PCollectionView view = createView(); + + when(stepContext.issueSideInputFetch( + eq(view), any(BoundedWindow.class), eq(SideInputState.UNKNOWN))) + .thenReturn(false); + when(stepContext.issueSideInputFetch( + eq(view), any(BoundedWindow.class), eq(SideInputState.KNOWN_READY))) + .thenReturn(true); + + when(mockTimerInternals.currentInputWatermarkTime()).thenReturn(Instant.ofEpochMilli(15L)); + StreamingKeyedWorkItemSideInputParDoFn, IntervalWindow> + runner = createRunner(view); + + TestReceiver receiver = new TestReceiver(); + runner.startBundle(receiver); + + KeyedWorkItem elemsWorkItem = + KeyedWorkItems.elementsWorkItem( + "a", + ImmutableList.of( + createDatum(13, 13L), + createDatum(16, 16L), // side inputs non-ready element + createDatum(18, 18L))); + + runner.processElement(new ValueInEmptyWindows<>(elemsWorkItem)); + + // Initially blocked! No output. + assertEquals(0, receiver.outputs.size()); + + when(mockTimerInternals.currentInputWatermarkTime()).thenReturn(Instant.ofEpochMilli(20)); + runner.processElement( + new ValueInEmptyWindows<>( + KeyedWorkItems.timersWorkItem( + "a", + ImmutableList.of( + timerData(window(10, 20), Instant.ofEpochMilli(19), Timer.Type.WATERMARK))))); + + // Timer is blocked too! + assertEquals(0, receiver.outputs.size()); + + // Now make it ready! + IntervalWindow readyWindow = window(10, 20); + Windmill.GlobalDataId id = + Windmill.GlobalDataId.newBuilder() + .setTag(view.getTagInternal().getId()) + .setVersion( + ByteString.copyFrom( + CoderUtils.encodeToByteArray(IntervalWindow.getCoder(), readyWindow))) + .build(); + + when(stepContext.getSideInputNotifications()) + .thenReturn(Arrays.asList(id)); + + runner.finishBundle(); + + runner.startBundle(receiver); + + // We don't check for output here because we just wanted to see if the runner works + // without exceptions. The issue was lifecycle of the runner bundle (finishBundle, startBundle). + } + + static class TestSplittableDoFn extends DoFn> { + @ProcessElement + public void processElement(ProcessContext c, RestrictionTracker tracker) { + c.output(KV.of(tracker.currentRestriction(), c.element())); + } + + @GetInitialRestriction + public String getInitialRestriction(@Element Integer element) { + return "restriction"; + } + + @NewTracker + public RestrictionTracker newTracker(@Restriction String restriction) { + return new RestrictionTracker() { + @Override + public boolean tryClaim(Object position) { + return true; + } + + @Override + public String currentRestriction() { + return restriction; + } + + @Override + public SplitResult trySplit(double fractionOfRemainder) { + return null; + } + + @Override + public void checkDone() {} + + @Override + public IsBounded isBounded() { + return IsBounded.BOUNDED; + } + }; + } + } + + @Test + public void testSplittableProcessElement() throws Exception { + PCollectionView view = createView(); + + when(stepContext.issueSideInputFetch( + eq(view), any(BoundedWindow.class), eq(SideInputState.UNKNOWN))) + .thenReturn(false); + when(stepContext.issueSideInputFetch( + eq(view), any(BoundedWindow.class), eq(SideInputState.KNOWN_READY))) + .thenReturn(true); + + when(mockTimerInternals.currentInputWatermarkTime()).thenReturn(Instant.ofEpochMilli(15L)); + StreamingKeyedWorkItemSideInputParDoFn< + byte[], KV, KV, IntervalWindow> + runner = createSplittableRunner(view); + + TestReceiver receiver = new TestReceiver(); + runner.startBundle(receiver); + + KeyedWorkItem> elemsWorkItem = + KeyedWorkItems.elementsWorkItem( + new byte[] {1}, ImmutableList.of(createDatum(KV.of(13, "restriction"), 13L))); + + runner.processElement(new ValueInEmptyWindows<>(elemsWorkItem)); + + // Initially blocked! No output. + assertEquals(0, receiver.outputs.size()); + runner.finishBundle(); + + // Now make it ready! + IntervalWindow readyWindow = window(10, 20); + Windmill.GlobalDataId id = + Windmill.GlobalDataId.newBuilder() + .setTag(view.getTagInternal().getId()) + .setVersion( + ByteString.copyFrom( + CoderUtils.encodeToByteArray(IntervalWindow.getCoder(), readyWindow))) + .build(); + + when(stepContext.getSideInputNotifications()) + .thenReturn(Arrays.asList(id)); + + runner.startBundle(receiver); + + // Note: unblocking logic would run here if the environment is fully mocked to push + // blocked items back into processing. For the purpose of testing SplittableDoFn initialization, + // this suffices. + } + + private WindowedValue createDatum(T element, long timestampMillis) { + Instant timestamp = Instant.ofEpochMilli(timestampMillis); + return WindowedValues.of( + element, timestamp, Arrays.asList(WINDOW_FN.assignWindow(timestamp)), PaneInfo.NO_FIRING); + } + + private TimerData timerData(IntervalWindow window, Instant timestamp, Timer.Type type) { + return TimerData.of( + StateNamespaces.window(IntervalWindow.getCoder(), window), + timestamp, + timestamp, + type == Windmill.Timer.Type.WATERMARK ? TimeDomain.EVENT_TIME : TimeDomain.PROCESSING_TIME, + CausedByDrain.NORMAL); + } + + private IntervalWindow window(long start, long end) { + return new IntervalWindow(Instant.ofEpochMilli(start), Instant.ofEpochMilli(end)); + } + + private PCollectionView createView() { + return TestPipeline.create() + .apply(Create.empty(StringUtf8Coder.of())) + .apply(Window.into(WINDOW_FN)) + .apply(View.asSingleton()); + } + + static class TestReceiver implements Receiver { + List>> outputs = new ArrayList<>(); + + @SuppressWarnings("unchecked") + @Override + public void process(Object outputElem) { + outputs.add((WindowedValue>) outputElem); + } + } + + @SuppressWarnings("unchecked") + private StreamingKeyedWorkItemSideInputParDoFn< + String, Integer, KV, IntervalWindow> + createRunner(PCollectionView view) throws Exception { + Coder keyCoder = StringUtf8Coder.of(); + Coder inputCoder = BigEndianIntegerCoder.of(); + + WindowingStrategy windowingStrategy = WindowingStrategy.of(WINDOW_FN); + + DoFn, KV> theDoFn = + new DoFn, KV>() { + @ProcessElement + public void processElement(ProcessContext c) { + KeyedWorkItem kwi = c.element(); + for (WindowedValue wv : kwi.elementsIterable()) { + c.output(KV.of(kwi.key(), wv.getValue())); + } + } + }; + + DoFnInfo, KV> fnInfo = + DoFnInfo.forFn( + theDoFn, + windowingStrategy, + ImmutableList.of(view), + (Coder) null, + MAIN_OUTPUT_TAG, + DoFnSchemaInformation.create(), + Collections.emptyMap()); + + DoFnRunnerFactory, KV> runnerFactory = + new DoFnRunnerFactory, KV>() { + @Override + public DoFnRunner, KV> createRunner( + DoFn, KV> fn, + PipelineOptions options, + TupleTag> mainOutputTag, + List> sideOutputTags, + Iterable> sideInputViews, + SideInputReader sideInputReader, + Coder> inputCoder, + Map, Coder> outputCoders, + WindowingStrategy windowingStrategy, + DataflowExecutionContext.DataflowStepContext stepContext, + DataflowExecutionContext.DataflowStepContext userStepContext, + WindowedValueMultiReceiver outputManager2, + DoFnSchemaInformation doFnSchemaInformation, + Map> sideInputMapping) { + return new SimpleDoFnRunner<>( + options, + fn, + sideInputReader, + outputManager2, + mainOutputTag, + sideOutputTags, + stepContext, + inputCoder, + outputCoders, + windowingStrategy, + doFnSchemaInformation, + sideInputMapping); + } + }; + + PipelineOptions options = PipelineOptionsFactory.create(); + options.as(StreamingOptions.class).setStreaming(true); + + return new StreamingKeyedWorkItemSideInputParDoFn<>( + options, + DoFnInstanceManagers.singleInstance(fnInfo), + mockSideInputReader, + MAIN_OUTPUT_TAG, + ImmutableMap.of(MAIN_OUTPUT_TAG, 0), + stepContext, + TestOperationContext.create(), + DoFnSchemaInformation.create(), + Collections.emptyMap(), + runnerFactory, + keyCoder, + inputCoder); + } + + @SuppressWarnings("unchecked") + private StreamingKeyedWorkItemSideInputParDoFn< + byte[], KV, KV, IntervalWindow> + createSplittableRunner(PCollectionView view) throws Exception { + ByteArrayCoder keyCoder = ByteArrayCoder.of(); + Coder> inputCoder = + KvCoder.of(BigEndianIntegerCoder.of(), StringUtf8Coder.of()); + + WindowingStrategy windowingStrategy = + (WindowingStrategy) WindowingStrategy.of(WINDOW_FN); + + TestSplittableDoFn theDoFn = new TestSplittableDoFn(); + + ProcessFn, String, Object, Object> processFn = + new ProcessFn, String, Object, Object>( + theDoFn, + BigEndianIntegerCoder.of(), + StringUtf8Coder.of(), + (Coder) StringUtf8Coder.of(), // watermarkEstimatorStateCoder + windowingStrategy, + Collections.emptyMap()); + processFn.setup(PipelineOptionsFactory.create()); + + DoFnInfo>, KV> fnInfo = + DoFnInfo.forFn( + processFn, + windowingStrategy, + ImmutableList.of(view), + (Coder) null, + MAIN_OUTPUT_TAG, + DoFnSchemaInformation.create(), + Collections.emptyMap()); + + DoFnRunnerFactory>, KV> + runnerFactory = + new DoFnRunnerFactory< + KeyedWorkItem>, KV>() { + @Override + public DoFnRunner>, KV> + createRunner( + DoFn>, KV> fn, + PipelineOptions options, + TupleTag> mainOutputTag, + List> sideOutputTags, + Iterable> sideInputViews, + SideInputReader sideInputReader, + Coder>> inputCoder, + Map, Coder> outputCoders, + WindowingStrategy windowingStrategy, + DataflowExecutionContext.DataflowStepContext stepContext, + DataflowExecutionContext.DataflowStepContext userStepContext, + WindowedValueMultiReceiver outputManager2, + DoFnSchemaInformation doFnSchemaInformation, + Map> sideInputMapping) { + + ProcessFn, String, Object, Object> fn2 = + (ProcessFn, String, Object, Object>) fn; + fn2.setStateInternalsFactory(key -> (StateInternals) stepContext.stateInternals()); + fn2.setTimerInternalsFactory(key -> stepContext.timerInternals()); + fn2.setSideInputReader(sideInputReader); + fn2.setProcessElementInvoker( + new OutputAndTimeBoundedSplittableProcessElementInvoker< + Integer, KV, String, Object, Object>( + fn2.getFn(), + options, + outputManager2, + mainOutputTag, + sideInputReader, + Executors.newSingleThreadScheduledExecutor(), + 10000, + Duration.standardSeconds(10), + () -> null)); + + return new SimpleDoFnRunner<>( + options, + fn, + sideInputReader, + outputManager2, + mainOutputTag, + sideOutputTags, + stepContext, + inputCoder, + outputCoders, + windowingStrategy, + doFnSchemaInformation, + sideInputMapping); + } + }; + + PipelineOptions options = PipelineOptionsFactory.create(); + options.as(StreamingOptions.class).setStreaming(true); + + return new StreamingKeyedWorkItemSideInputParDoFn<>( + options, + DoFnInstanceManagers.singleInstance(fnInfo), + mockSideInputReader, + MAIN_OUTPUT_TAG, + ImmutableMap.of(MAIN_OUTPUT_TAG, 0), + stepContext, + TestOperationContext.create(), + DoFnSchemaInformation.create(), + Collections.emptyMap(), + runnerFactory, + keyCoder, + inputCoder); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessorTest.java new file mode 100644 index 000000000000..19e22b038839 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputProcessorTest.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.util.Lists; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import org.apache.beam.runners.core.StateNamespaces; +import org.apache.beam.runners.core.TimerInternals.TimerData; +import org.apache.beam.sdk.state.BagState; +import org.apache.beam.sdk.state.TimeDomain; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link StreamingSideInputProcessor}. */ +@RunWith(JUnit4.class) +public class StreamingSideInputProcessorTest { + + @Mock private StreamingSideInputFetcher mockFetcher; + private StreamingSideInputProcessor processor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + processor = new StreamingSideInputProcessor<>(mockFetcher); + } + + @Test + public void testTryUnblockElementsNoReadyWindows() { + // Given + doNothing().when(mockFetcher).prefetchBlockedMap(); + when(mockFetcher.getReadyWindows()).thenReturn(Collections.emptySet()); + + // When + processor.tryUnblockElements(unblocked -> assertThat(unblocked, emptyIterable())); + + // Then + verify(mockFetcher).prefetchBlockedMap(); + verify(mockFetcher).getReadyWindows(); + } + + @Test + public void testTryUnblockElementsWithReadyWindows() { + // Given + IntervalWindow window1 = new IntervalWindow(Instant.ofEpochMilli(0), Instant.ofEpochMilli(10)); + IntervalWindow window2 = new IntervalWindow(Instant.ofEpochMilli(10), Instant.ofEpochMilli(20)); + Set readyWindows = new HashSet<>(Arrays.asList(window1, window2)); + + WindowedValue element1 = + WindowedValues.of( + "e1", Instant.ofEpochMilli(5), Arrays.asList(window1), PaneInfo.NO_FIRING); + WindowedValue element2 = + WindowedValues.of( + "e2", Instant.ofEpochMilli(15), Arrays.asList(window2), PaneInfo.NO_FIRING); + + @SuppressWarnings("unchecked") + BagState> mockBag1 = mock(BagState.class); + @SuppressWarnings("unchecked") + BagState> mockBag2 = mock(BagState.class); + + when(mockBag1.read()).thenReturn(Arrays.asList(element1)); + when(mockBag2.read()).thenReturn(Arrays.asList(element2)); + + doNothing().when(mockFetcher).prefetchBlockedMap(); + when(mockFetcher.getReadyWindows()).thenReturn(readyWindows); + when(mockFetcher.prefetchElements(readyWindows)).thenReturn(Arrays.asList(mockBag1, mockBag2)); + doNothing().when(mockFetcher).releaseBlockedWindows(readyWindows); + + // When + processor.tryUnblockElements( + unblocked -> assertThat(unblocked, containsInAnyOrder(element1, element2))); + + // Then + verify(mockFetcher).prefetchBlockedMap(); + verify(mockFetcher).getReadyWindows(); + verify(mockFetcher).prefetchElements(readyWindows); + verify(mockBag1).read(); + verify(mockBag1).clear(); + verify(mockBag2).read(); + verify(mockBag2).clear(); + verify(mockFetcher).releaseBlockedWindows(readyWindows); + } + + @Test + public void testHandleFinishBundle() { + // Given + doNothing().when(mockFetcher).persist(); + + // When + processor.handleFinishBundle(); + + // Then + verify(mockFetcher).persist(); + } + + @Test + public void testHandleProcessElementBlocked() { + // Given + IntervalWindow window = new IntervalWindow(Instant.ofEpochMilli(0), Instant.ofEpochMilli(10)); + WindowedValue compressedElement = + WindowedValues.of("e", Instant.ofEpochMilli(5), Arrays.asList(window), PaneInfo.NO_FIRING); + + when(mockFetcher.storeIfBlocked(any(WindowedValue.class))).thenReturn(true); + + // When + Iterator> unblocked = + processor.handleProcessElement(compressedElement); + + // Then + assertFalse(unblocked.hasNext()); + for (WindowedValue exploded : compressedElement.explodeWindows()) { + verify(mockFetcher).storeIfBlocked(exploded); + } + } + + @Test + public void testHandleProcessElementUnblocked() { + // Given + IntervalWindow window1 = new IntervalWindow(Instant.ofEpochMilli(0), Instant.ofEpochMilli(10)); + IntervalWindow window2 = new IntervalWindow(Instant.ofEpochMilli(10), Instant.ofEpochMilli(20)); + WindowedValue compressedElement = + WindowedValues.of( + "e", Instant.ofEpochMilli(5), Arrays.asList(window1, window2), PaneInfo.NO_FIRING); + + when(mockFetcher.storeIfBlocked(any(WindowedValue.class))).thenReturn(false); + + // When + Iterator> unblocked = + processor.handleProcessElement(compressedElement); + // Then + assertThat( + Lists.newArrayList(unblocked), + containsInAnyOrder( + Iterables.toArray(compressedElement.explodeWindows(), WindowedValue.class))); + for (WindowedValue exploded : compressedElement.explodeWindows()) { + verify(mockFetcher).storeIfBlocked(exploded); + } + } + + @Test + public void testHandleProcessTimerSuccess() { + // Given + TimerData testTimer = + TimerData.of( + StateNamespaces.global(), + Instant.ofEpochMilli(1000), + Instant.ofEpochMilli(2000), + TimeDomain.EVENT_TIME, + CausedByDrain.NORMAL); + when(mockFetcher.storeIfBlocked(testTimer)).thenReturn(false); + + // When + processor.handleProcessTimer(testTimer); + + // Then + verify(mockFetcher).storeIfBlocked(testTimer); + } + + @Test + public void testHandleProcessTimerThrowsPreconditionFail() { + // Given + TimerData testTimer = + TimerData.of( + StateNamespaces.global(), + Instant.ofEpochMilli(1000), + Instant.ofEpochMilli(2000), + TimeDomain.EVENT_TIME, + CausedByDrain.NORMAL); + when(mockFetcher.storeIfBlocked(testTimer)).thenReturn(true); + + // When & Then + assertThrows(IllegalStateException.class, () -> processor.handleProcessTimer(testTimer)); + verify(mockFetcher).storeIfBlocked(testTimer); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactoryTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactoryTest.java index 9d3fa9b211b1..43331d11a7ee 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactoryTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/UserParDoFnFactoryTest.java @@ -323,10 +323,6 @@ private CloudObject getCloudObject(DoFn fn) { private CloudObject getCloudObject(DoFn fn, WindowingStrategy windowingStrategy) { CloudObject object = CloudObject.forClassName("DoFn"); - @SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "unchecked" - }) DoFnInfo info = DoFnInfo.forFn( fn, @@ -377,13 +373,14 @@ public void testCleanupRegistered() throws Exception { Receiver rcvr = new OutputReceiver(); parDoFn.startBundle(rcvr); - IntervalWindow firstWindow = new IntervalWindow(new Instant(0), new Instant(10)); + IntervalWindow firstWindow = + new IntervalWindow(Instant.ofEpochMilli(0), Instant.ofEpochMilli(10)); parDoFn.processElement( - WindowedValues.of("foo", new Instant(1), firstWindow, PaneInfo.NO_FIRING)); + WindowedValues.of("foo", Instant.ofEpochMilli(1), firstWindow, PaneInfo.NO_FIRING)); verify(stepContext) .setStateCleanupTimer( - SimpleParDoFn.CLEANUP_TIMER_ID, + SimpleParDoFnHelpers.CLEANUP_TIMER_ID, firstWindow, IntervalWindow.getCoder(), firstWindow.maxTimestamp().plus(Duration.millis(1L)), @@ -436,14 +433,14 @@ public void testCleanupTimerForGlobalWindowWithAllowedLateness() throws Exceptio GlobalWindow globalWindow = GlobalWindow.INSTANCE; parDoFn.processElement( - WindowedValues.of("foo", new Instant(1), globalWindow, PaneInfo.NO_FIRING)); + WindowedValues.of("foo", Instant.ofEpochMilli(1), globalWindow, PaneInfo.NO_FIRING)); assertThat( globalWindow.maxTimestamp().plus(allowedLateness), greaterThan(BoundedWindow.TIMESTAMP_MAX_VALUE)); verify(stepContext) .setStateCleanupTimer( - SimpleParDoFn.CLEANUP_TIMER_ID, + SimpleParDoFnHelpers.CLEANUP_TIMER_ID, globalWindow, GlobalWindow.Coder.INSTANCE, BoundedWindow.TIMESTAMP_MAX_VALUE, @@ -459,7 +456,7 @@ public void testCleanupTimerForGlobalWindowWithAllowedLateness() throws Exceptio when(stepContext.getNextFiredTimer((Coder) GlobalWindow.Coder.INSTANCE)) .thenReturn( TimerData.of( - SimpleParDoFn.CLEANUP_TIMER_ID, + SimpleParDoFnHelpers.CLEANUP_TIMER_ID, globalWindowNamespace, BoundedWindow.TIMESTAMP_MAX_VALUE, BoundedWindow.TIMESTAMP_MAX_VALUE.minus(Duration.millis(1)), @@ -516,8 +513,10 @@ public void testCleanupWorks() throws Exception { Receiver rcvr = new OutputReceiver(); parDoFn.startBundle(rcvr); - IntervalWindow firstWindow = new IntervalWindow(new Instant(0), new Instant(9)); - IntervalWindow secondWindow = new IntervalWindow(new Instant(10), new Instant(19)); + IntervalWindow firstWindow = + new IntervalWindow(Instant.ofEpochMilli(0), Instant.ofEpochMilli(9)); + IntervalWindow secondWindow = + new IntervalWindow(Instant.ofEpochMilli(10), Instant.ofEpochMilli(19)); Coder windowCoder = IntervalWindow.getCoder(); StateNamespace firstWindowNamespace = StateNamespaces.window(windowCoder, firstWindow); @@ -535,7 +534,7 @@ public void testCleanupWorks() throws Exception { when(stepContext.getNextFiredTimer(windowCoder)) .thenReturn( TimerData.of( - SimpleParDoFn.CLEANUP_TIMER_ID, + SimpleParDoFnHelpers.CLEANUP_TIMER_ID, firstWindowNamespace, firstWindow.maxTimestamp().plus(Duration.millis(1L)), firstWindow.maxTimestamp().plus(Duration.millis(1L)), @@ -552,7 +551,7 @@ public void testCleanupWorks() throws Exception { when(stepContext.getNextFiredTimer((Coder) windowCoder)) .thenReturn( TimerData.of( - SimpleParDoFn.CLEANUP_TIMER_ID, + SimpleParDoFnHelpers.CLEANUP_TIMER_ID, secondWindowNamespace, secondWindow.maxTimestamp().plus(Duration.millis(1L)), secondWindow.maxTimestamp().plus(Duration.millis(1L)), diff --git a/runners/prism/java/build.gradle b/runners/prism/java/build.gradle index 9357515f36c2..03049f293317 100644 --- a/runners/prism/java/build.gradle +++ b/runners/prism/java/build.gradle @@ -162,6 +162,9 @@ def sickbayTests = [ // java.lang.IllegalStateException: java.io.EOFException 'org.apache.beam.sdk.transforms.ViewTest.testSideInputWithNestedIterables', + // Triggers index-out-of-bound error in Prism + 'org.apache.beam.sdk.transforms.ParDoTest$StateTests.testTimerSideInput', + // Missing output due to processing time timer skew. 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testProcessElementSkew', diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/UsesSideInputsInTimer.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/UsesSideInputsInTimer.java new file mode 100644 index 000000000000..8320c1451d17 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/UsesSideInputsInTimer.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.testing; + +import org.apache.beam.sdk.annotations.Internal; + +/** + * Category tag for validation tests which use sideinputs in OnTimer and OnWindowExpiration. Tests + * tagged with {@link UsesSideInputsInTimer} should be run for runners which support sideinputs. + */ +@Internal +public class UsesSideInputsInTimer {} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java index a366ded4fe2d..bfd04908b990 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java @@ -359,6 +359,14 @@ public abstract class OnTimerContext extends WindowedContext { /** Returns the time domain of the current timer. */ public abstract TimeDomain timeDomain(); + /** + * Returns the value of the side input. + * + * @throws IllegalArgumentException if this is not a side input + */ + @Pure + public abstract T sideInput(PCollectionView view); + @Pure public abstract org.apache.beam.sdk.values.CausedByDrain causedByDrain(); } @@ -368,6 +376,14 @@ public abstract class OnWindowExpirationContext extends WindowedContext { /** Returns the window in which the window expiration is firing. */ @Pure public abstract BoundedWindow window(); + + /** + * Returns the value of the side input. + * + * @throws IllegalArgumentException if this is not a side input + */ + @Pure + public abstract T sideInput(PCollectionView view); } /** diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java index 0bd2c1c888f0..2983fc94021c 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java @@ -197,7 +197,8 @@ private DoFnSignatures() {} Parameter.TimerIdParameter.class, Parameter.FireTimestampParameter.class, Parameter.CausedByDrainParameter.class, - Parameter.KeyParameter.class); + Parameter.KeyParameter.class, + Parameter.SideInputParameter.class); private static final ImmutableList> ALLOWED_ON_TIMER_FAMILY_PARAMETERS = @@ -215,7 +216,8 @@ private DoFnSignatures() {} Parameter.TimerIdParameter.class, Parameter.FireTimestampParameter.class, Parameter.CausedByDrainParameter.class, - Parameter.KeyParameter.class); + Parameter.KeyParameter.class, + Parameter.SideInputParameter.class); private static final Collection> ALLOWED_ON_WINDOW_EXPIRATION_PARAMETERS = @@ -226,7 +228,8 @@ private DoFnSignatures() {} Parameter.TaggedOutputReceiverParameter.class, Parameter.StateParameter.class, Parameter.TimestampParameter.class, - Parameter.KeyParameter.class); + Parameter.KeyParameter.class, + Parameter.SideInputParameter.class); private static final Collection> ALLOWED_GET_INITIAL_RESTRICTION_PARAMETERS = diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java index 8a273127b4fc..0c984d01c8f0 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java @@ -114,6 +114,7 @@ import org.apache.beam.sdk.testing.UsesRequiresTimeSortedInput; import org.apache.beam.sdk.testing.UsesSetState; import org.apache.beam.sdk.testing.UsesSideInputs; +import org.apache.beam.sdk.testing.UsesSideInputsInTimer; import org.apache.beam.sdk.testing.UsesSideInputsWithDifferentCoders; import org.apache.beam.sdk.testing.UsesStatefulParDo; import org.apache.beam.sdk.testing.UsesStrictTimerOrdering; @@ -3678,6 +3679,154 @@ public void processElement( pipeline.run(); } + @Test + @Category({ + ValidatesRunner.class, + UsesStatefulParDo.class, + UsesSideInputs.class, + UsesSideInputsInTimer.class, + UsesTestStream.class, + UsesTimersInParDo.class, + UsesTriggeredSideInputs.class, + UsesOnWindowExpiration.class + }) + public void testTimerSideInput() { + // SideInput tag id + final String sideInputTag1 = "tag1"; + + final PCollectionView sideInput = + pipeline + .apply("CreateSideInput1", Create.of(2)) + .apply("ViewSideInput1", View.asSingleton()); + + DoFn, KV> doFn = + new DoFn, KV>() { + @TimerId("timer") + private final TimerSpec timerSpec = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @StateId("foo") + private final StateSpec> stateSpec = StateSpecs.value(); + + @ProcessElement + public void process(@Timestamp Instant ts, @TimerId("timer") Timer timer) { + timer.align(Duration.standardSeconds(10)).setRelative(); + } + + @OnTimer("timer") + public void onTimer( + OutputReceiver> o, + @DoFn.SideInput(sideInputTag1) Integer sideInput, + @Key Integer key) { + o.output(KV.of(key, sideInput)); + } + + @OnWindowExpiration + public void onWindowExpiration( + @DoFn.SideInput(sideInputTag1) Integer sideInput, + OutputReceiver> o, + @Key Integer key) { + o.output(KV.of(key, sideInput)); + } + }; + + final int numTestElements = 10; + final Instant now = new Instant(0); + TestStream.Builder> builder = + TestStream.create(KvCoder.of(VarIntCoder.of(), VarIntCoder.of())) + .advanceWatermarkTo(new Instant(0)); + + for (int i = 0; i < numTestElements; i++) { + builder = + builder.addElements( + TimestampedValue.of(KV.of(i % 2, i), now.plus(Duration.millis(i * 1000)))); + if ((i + 1) % 10 == 0) { + builder = builder.advanceWatermarkTo(now.plus(Duration.millis((i + 1) * 1000))); + } + } + List> expected = + IntStream.rangeClosed(0, 1) + .boxed() + .flatMap(i -> ImmutableList.of(KV.of(i, 2), KV.of(i, 2)).stream()) + .collect(Collectors.toList()); + + PCollection> output = + pipeline + .apply(builder.advanceWatermarkToInfinity()) + .apply(ParDo.of(doFn).withSideInput(sideInputTag1, sideInput)); + PAssert.that(output).containsInAnyOrder(expected); + pipeline.run(); + } + + @Test + @Category({ + ValidatesRunner.class, + UsesStatefulParDo.class, + UsesSideInputs.class, + UsesSideInputsInTimer.class, + UsesTimersInParDo.class, + UsesTriggeredSideInputs.class + }) + public void testSideInputNotReadyTimer() { + final String sideInputTag = "tag1"; + + // Create a side input that is delayed by 5 seconds using Thread.sleep + DoFn, String> delayFn = + new DoFn, String>() { + @ProcessElement + public void process(OutputReceiver o) throws InterruptedException { + Thread.sleep(java.time.Duration.ofSeconds(15).toMillis()); + o.output("side-value"); + } + }; + + PCollectionView sideInput = + pipeline + .apply("CreateSideSource", Create.of(KV.of("dummyKey", ""))) + .apply("DelaySideInput", ParDo.of(delayFn)) + .apply(View.asSingleton()); + + // Main input in global window + DoFn, String> fn = + new DoFn, String>() { + @TimerId("timer") + private final TimerSpec timerSpec = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @StateId("dummy") + private final StateSpec> dummy = StateSpecs.value(); + + @ProcessElement + public void process( + @Timestamp Instant ts, + @TimerId("timer") Timer timer, + @DoFn.SideInput(sideInputTag) String sideInputValue, + OutputReceiver o) { + // Set timer to fire at current timestamp + 1 millis + timer.offset(Duration.millis(1)).setRelative(); + o.output(sideInputValue); + } + + @OnTimer("timer") + public void onTimer( + OutputReceiver o, @DoFn.SideInput(sideInputTag) String sideInputValue) { + o.output(sideInputValue); + } + + @OnWindowExpiration + public void onWindowExpiration( + OutputReceiver o, @DoFn.SideInput(sideInputTag) String sideInputValue) { + o.output(sideInputValue); + } + }; + + PCollection output = + pipeline + .apply("CreateMainKV", Create.of(KV.of("key", "main-elem"))) + .apply(ParDo.of(fn).withSideInput(sideInputTag, sideInput)); + + PAssert.that(output).containsInAnyOrder("side-value", "side-value", "side-value"); + pipeline.run(); + } + @Test @Category({ ValidatesRunner.class, diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java index d92a84ea9ff7..3e4675ab074a 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java @@ -2362,6 +2362,11 @@ public BoundedWindow window() { return currentWindow; } + @Override + public T sideInput(PCollectionView view) { + return stateAccessor.get(view, currentWindow); + } + @Override public OutputBuilder builder(OutputT value) { return WindowedValues.builder() @@ -2489,6 +2494,15 @@ public K key() { return (K) currentTimer.getUserKey(); } + @Override + public @Nullable Object sideInput(String tagId) { + PCollectionView view = sideInputMapping.get(tagId); + if (view == null) { + throw new IllegalArgumentException("Unknown side input: " + tagId); + } + return stateAccessor.get(view, currentWindow); + } + @Override public OutputReceiver outputReceiver(DoFn doFn) { return context; @@ -2649,6 +2663,11 @@ public BoundedWindow window() { return currentWindow; } + @Override + public T sideInput(PCollectionView view) { + return stateAccessor.get(view, currentWindow); + } + @Override public CausedByDrain causedByDrain() { return causedByDrain; @@ -2800,6 +2819,15 @@ public Instant fireTimestamp(DoFn doFn) { return currentTimer.getFireTimestamp(); } + @Override + public @Nullable Object sideInput(String tagId) { + PCollectionView view = sideInputMapping.get(tagId); + if (view == null) { + throw new IllegalArgumentException("Unknown side input: " + tagId); + } + return stateAccessor.get(view, currentWindow); + } + @Override public K key() { return (K) currentTimer.getUserKey(); From dd45831322afd4797db7ef5be2a4b21f73047df9 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Tue, 9 Jun 2026 14:27:00 -0400 Subject: [PATCH 340/490] Support global sort and improve UDF overload resolution in Beam SQL (#38832) * Support global sort and improve UDF overload resolution in Beam SQL - Enable ORDER BY without LIMIT (global sort) in BeamSortRel by sorting in-memory. - Add AssertSorted test helper and testOrderBy_noLimit to verify global sort. - Map common Java classes to Calcite SqlTypeName in CalciteUtils.sqlTypeWithAutoCast, and add testSqlTypeWithAutoCast. - Prioritize overloaded methods with maximum parameter count in UdfImpl lookup, and add UdfImplTest. - Update LazyAggregateCombineFnTest to expect SQL BIGINT type instead of Java Long class. TAG=agy CONV=0df243da-2867-4795-9889-6334ba7d1599 * Address PR comments: simplify CalciteUtils type mapping and ensure deterministic UdfImpl lookup - Simplify Java class to Calcite type mapping in CalciteUtils using a switch statement and dynamic nullability check. - Use deterministic tie-breaker in UdfImpl.findMethod when resolving overloaded methods with the same parameter count. TAG=agy CONV=0df243da-2867-4795-9889-6334ba7d1599 * Optimize Java to SQL type mapping in CalciteUtils Use a static ImmutableMap for Java to SQL type mapping instead of a switch statement, improving lookup performance and code readability. TAG=agy CONV=0df243da-2867-4795-9889-6334ba7d1599 * Apply suggestion from @damccorm * Update sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/UdfImpl.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Run spotless to fix formatting in UdfImpl.java --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../beam/sdk/extensions/sql/impl/UdfImpl.java | 19 ++++-- .../extensions/sql/impl/rel/BeamSortRel.java | 58 ++++++++++++++--- .../sql/impl/utils/CalciteUtils.java | 31 ++++++++- .../sql/impl/LazyAggregateCombineFnTest.java | 5 +- .../sdk/extensions/sql/impl/UdfImplTest.java | 65 +++++++++++++++++++ .../sql/impl/rel/BeamSortRelTest.java | 43 ++++++++++++ .../sql/impl/utils/CalciteUtilsTest.java | 63 ++++++++++++++++++ 7 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/UdfImpl.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/UdfImpl.java index 7ebd3faea782..d268d5494053 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/UdfImpl.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/UdfImpl.java @@ -70,17 +70,28 @@ public static Function create(Method method) { } /* - * Finds a method in a given class by name. + * Finds a method in a given class by name. In case of overloaded methods with the same name, + * this prioritizes the overload with the maximum number of parameters. This ensures Calcite + * can resolve optional/default trailing parameters correctly when binding UDF overloads. + * * @param clazz class to search method in * @param name name of the method to find - * @return the first method with matching name or null when no method found + * @return the matching method with the highest parameter count or null when no method found */ static @Nullable Method findMethod(Class clazz, String name) { + Method bestMethod = null; for (Method method : clazz.getMethods()) { if (method.getName().equals(name) && !method.isBridge()) { - return method; + if (bestMethod == null) { + bestMethod = method; + } else { + int cmp = Integer.compare(method.getParameterCount(), bestMethod.getParameterCount()); + if (cmp > 0 || (cmp == 0 && method.toString().compareTo(bestMethod.toString()) < 0)) { + bestMethod = method; + } + } } } - return null; + return bestMethod; } } diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRel.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRel.java index aaa4d66011a6..c7a0b9136bbb 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRel.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRel.java @@ -40,6 +40,7 @@ import org.apache.beam.sdk.state.ValueState; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.Flatten; +import org.apache.beam.sdk.transforms.GroupByKey; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Top; @@ -74,15 +75,12 @@ *

    {@code
      * SELECT * FROM t ORDER BY id DESC LIMIT 10;
      * SELECT * FROM t ORDER BY id DESC LIMIT 10 OFFSET 5;
    - * }
    - * - *

    but an ORDER BY without a LIMIT is NOT supported. For example, the following will throw an - * exception: - * - *

    {@code
      * SELECT * FROM t ORDER BY id DESC;
      * }
    * + *

    Note: ORDER BY without a LIMIT is supported by keying all rows to a single key and sorting + * them in memory. This can be memory-intensive and may fail for large datasets. + * *

    Constraints

    * *
      @@ -134,12 +132,12 @@ public BeamSortRel( } if (fetch == null) { - throw new UnsupportedOperationException("ORDER BY without a LIMIT is not supported!"); + count = -1; + } else { + RexLiteral fetchLiteral = (RexLiteral) fetch; + count = ((BigDecimal) fetchLiteral.getValue()).intValue(); } - RexLiteral fetchLiteral = (RexLiteral) fetch; - count = ((BigDecimal) fetchLiteral.getValue()).intValue(); - if (offset != null) { RexLiteral offsetLiteral = (RexLiteral) offset; startIndex = ((BigDecimal) offsetLiteral.getValue()).intValue(); @@ -209,6 +207,21 @@ public PCollection expand(PCollectionList pinput) { GlobalWindows.class.getSimpleName(), windowingStrategy)); } + // When no limit is specified (count == -1), we must sort the entire dataset. + // To achieve this globally, we key all rows by a single dummy key, group them together + // using GroupByKey to ensure they are processed together, and then sort them in-memory + // via SortInMemoryFn. Note: This can be memory-intensive for large datasets. It should + // only be done as a final step when the remaining data is small + if (count == -1) { + BeamSqlRowComparator comparator = + new BeamSqlRowComparator(fieldIndices, orientation, nullsFirst); + return upstream + .apply("WithDummyKey", WithKeys.of("DummyKey")) + .apply("GroupByKey", GroupByKey.create()) + .apply("SortInMemory", ParDo.of(new SortInMemoryFn(comparator))) + .setRowSchema(CalciteUtils.toSchema(getRowType())); + } + ReversedBeamSqlRowComparator comparator = new ReversedBeamSqlRowComparator(fieldIndices, orientation, nullsFirst); @@ -303,6 +316,31 @@ public void processElement(ProcessContext ctx) { } } + /** + * A {@link DoFn} that sorts all elements in-memory. Expects input grouped by a dummy key, sorts + * the iterable values, and outputs them. + */ + private static class SortInMemoryFn extends DoFn>, Row> { + private final BeamSqlRowComparator comparator; + + public SortInMemoryFn(BeamSqlRowComparator comparator) { + this.comparator = comparator; + } + + @ProcessElement + public void processElement(ProcessContext ctx) { + Iterable input = ctx.element().getValue(); + List list = new ArrayList<>(); + for (Row r : input) { + list.add(r); + } + list.sort(comparator); + for (Row r : list) { + ctx.output(r); + } + } + } + @Override public Sort copy( RelTraitSet traitSet, diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java index 3aaa91680999..d55c227e7b45 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java @@ -170,6 +170,25 @@ public static boolean isStringType(FieldType fieldType) { FieldType.DATETIME, SqlTypeName.TIMESTAMP, FieldType.STRING, SqlTypeName.VARCHAR); + private static final Map, SqlTypeName> JAVA_TO_SQL_TYPE_MAPPING = + ImmutableMap., SqlTypeName>builder() + .put(String.class, SqlTypeName.VARCHAR) + .put(Integer.class, SqlTypeName.INTEGER) + .put(int.class, SqlTypeName.INTEGER) + .put(Long.class, SqlTypeName.BIGINT) + .put(long.class, SqlTypeName.BIGINT) + .put(Double.class, SqlTypeName.DOUBLE) + .put(double.class, SqlTypeName.DOUBLE) + .put(Float.class, SqlTypeName.FLOAT) + .put(float.class, SqlTypeName.FLOAT) + .put(Short.class, SqlTypeName.SMALLINT) + .put(short.class, SqlTypeName.SMALLINT) + .put(Byte.class, SqlTypeName.TINYINT) + .put(byte.class, SqlTypeName.TINYINT) + .put(Boolean.class, SqlTypeName.BOOLEAN) + .put(boolean.class, SqlTypeName.BOOLEAN) + .build(); + // Associating FieldType to generated RelDataType objects for Beam logical types. Used for // recovering the original type in output schema after full Beam FieldType->Calcite Type->Beam // FieldType trip @@ -365,7 +384,9 @@ private static RelDataType toRelDataType( * SQL-Java type mapping, with specified Beam rules:
      * 1. redirect {@link AbstractInstant} to {@link Date} so Calcite can recognize it.
      * 2. For a list, the component type is needed to create a Sql array type.
      - * 3. For a Map, the component type is needed to create a Sql map type. + * 3. For a Map, the component type is needed to create a Sql map type.
      + * 4. For standard Java classes (String, Integer, etc.), map them to corresponding Calcite SQL + * type with appropriate nullability. * * @param type * @return Calcite RelDataType @@ -396,6 +417,14 @@ public static RelDataType sqlTypeWithAutoCast(RelDataTypeFactory typeFactory, Ty + ". This is currently unsupported, use List instead " + "of Array."); } + if (type instanceof Class) { + Class clazz = (Class) type; + SqlTypeName sqlTypeName = JAVA_TO_SQL_TYPE_MAPPING.get(clazz); + if (sqlTypeName != null) { + return typeFactory.createTypeWithNullability( + typeFactory.createSqlType(sqlTypeName), !clazz.isPrimitive()); + } + } return typeFactory.createJavaType((Class) type); } } diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java index 17636c628eb8..408118ff09e9 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java @@ -34,6 +34,7 @@ import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.type.RelDataTypeSystem; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.AggregateFunction; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.FunctionParameter; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.SqlTypeName; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Rule; @@ -77,7 +78,9 @@ public void subclassGetUdafImpl() { LazyAggregateCombineFn combiner = new LazyAggregateCombineFn<>(aggregateFn); AggregateFunction aggregateFunction = combiner.getUdafImpl(); RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT); - RelDataType expectedType = typeFactory.createJavaType(Long.class); + RelDataType expectedType = + typeFactory.createTypeWithNullability( + typeFactory.createSqlType(SqlTypeName.BIGINT), true); List params = aggregateFunction.getParameters(); assertThat(params, hasSize(1)); diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java new file mode 100644 index 000000000000..3746b9ac8471 --- /dev/null +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.extensions.sql.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.lang.reflect.Method; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link UdfImpl}. */ +@RunWith(JUnit4.class) +public class UdfImplTest { + + @Test + public void testFindMethod_overloaded_prioritizesMaxParams() { + Method method = UdfImpl.findMethod(OverloadedFn.class, "eval"); + assertNotNull(method); + assertEquals(3, method.getParameterTypes().length); + } + + @Test + public void testFindMethod_singleMethod() { + Method method = UdfImpl.findMethod(SingleFn.class, "eval"); + assertNotNull(method); + assertEquals(1, method.getParameterTypes().length); + } + + public static class OverloadedFn { + public String eval(String a) { + return a; + } + + public String eval(String a, String b) { + return a + b; + } + + public String eval(String a, String b, String c) { + return a + b + c; + } + } + + public static class SingleFn { + public String eval(String a) { + return a; + } + } +} diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java index dbe8be441ac6..0730f17d9c84 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.extensions.sql.impl.rel; +import java.util.ArrayList; +import java.util.List; import org.apache.beam.sdk.extensions.sql.TestUtils; import org.apache.beam.sdk.extensions.sql.impl.planner.BeamRelMetadataQuery; import org.apache.beam.sdk.extensions.sql.impl.planner.NodeStats; @@ -24,6 +26,7 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.RelNode; @@ -316,4 +319,44 @@ public void testNodeStatsEstimation() { Assert.assertEquals(10., estimate.getRowCount(), 0.01); Assert.assertEquals(10., estimate.getWindow(), 0.01); } + + @Test + public void testOrderBy_noLimit() { + String sql = + "SELECT order_id, site_id, price " + + "FROM ORDER_DETAILS " + + "ORDER BY order_id asc, site_id desc"; + + PCollection rows = compilePipeline(sql, pipeline); + PAssert.that(rows).satisfies(new AssertSorted()); + pipeline.run().waitUntilFinish(); + } + + private static class AssertSorted implements SerializableFunction, Void> { + @Override + public Void apply(Iterable input) { + List list = new ArrayList<>(); + for (Row r : input) { + list.add(r); + } + Assert.assertEquals(10, list.size()); + for (int i = 0; i < list.size() - 1; i++) { + Row r1 = list.get(i); + Row r2 = list.get(i + 1); + Long id1 = r1.getInt64("order_id"); + Long id2 = r2.getInt64("order_id"); + int comp = id1.compareTo(id2); + if (comp > 0) { + Assert.fail("Rows not sorted by order_id asc: " + list); + } else if (comp == 0) { + Integer site1 = r1.getInt32("site_id"); + Integer site2 = r2.getInt32("site_id"); + if (site1 < site2) { + Assert.fail("Rows not sorted by site_id desc when order_id is equal: " + list); + } + } + } + return null; + } + } } diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtilsTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtilsTest.java index 481a700c0c99..2cee34350249 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtilsTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtilsTest.java @@ -198,4 +198,67 @@ public void testToRelDataTypeWithRowBackedLogicalType() { assertEquals(1, relDataType.getFieldCount()); assertEquals("nested_f1", relDataType.getFieldList().get(0).getName()); } + + @Test + public void testSqlTypeWithAutoCast() { + RelDataType type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, String.class); + assertEquals(SqlTypeName.VARCHAR, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Integer.class); + assertEquals(SqlTypeName.INTEGER, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, int.class); + assertEquals(SqlTypeName.INTEGER, type.getSqlTypeName()); + assertFalse(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Long.class); + assertEquals(SqlTypeName.BIGINT, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, long.class); + assertEquals(SqlTypeName.BIGINT, type.getSqlTypeName()); + assertFalse(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Double.class); + assertEquals(SqlTypeName.DOUBLE, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, double.class); + assertEquals(SqlTypeName.DOUBLE, type.getSqlTypeName()); + assertFalse(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Float.class); + assertEquals(SqlTypeName.FLOAT, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, float.class); + assertEquals(SqlTypeName.FLOAT, type.getSqlTypeName()); + assertFalse(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Short.class); + assertEquals(SqlTypeName.SMALLINT, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, short.class); + assertEquals(SqlTypeName.SMALLINT, type.getSqlTypeName()); + assertFalse(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Byte.class); + assertEquals(SqlTypeName.TINYINT, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, byte.class); + assertEquals(SqlTypeName.TINYINT, type.getSqlTypeName()); + assertFalse(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Boolean.class); + assertEquals(SqlTypeName.BOOLEAN, type.getSqlTypeName()); + assertTrue(type.isNullable()); + + type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, boolean.class); + assertEquals(SqlTypeName.BOOLEAN, type.getSqlTypeName()); + assertFalse(type.isNullable()); + } } From df72065870770da2445179c976139d5e0b58d214 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 9 Jun 2026 15:16:45 -0400 Subject: [PATCH 341/490] Update huggingface model handler info (#38789) * add hugging face info * update HuggingFace Model Handler info to be consistent with VertexAI --- .../yaml/tests/runinference_huggingface.yaml | 2 +- sdks/python/apache_beam/yaml/yaml_ml.py | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml index 8728a6f544ad..011ed96b9ae3 100644 --- a/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml +++ b/sdks/python/apache_beam/yaml/tests/runinference_huggingface.yaml @@ -25,7 +25,7 @@ pipelines: - type: RunInference config: model_handler: - type: "HuggingFacePipeline" + type: "HuggingFacePipelineModelHandler" config: task: "text-classification" inference_fn: diff --git a/sdks/python/apache_beam/yaml/yaml_ml.py b/sdks/python/apache_beam/yaml/yaml_ml.py index 188530180c46..a4437b2ebebf 100644 --- a/sdks/python/apache_beam/yaml/yaml_ml.py +++ b/sdks/python/apache_beam/yaml/yaml_ml.py @@ -282,8 +282,8 @@ def inference_output_type(self): ('model_id', Optional[str])]) -@ModelHandlerProvider.register_handler_type('HuggingFacePipeline') -class HuggingFacePipelineProvider(ModelHandlerProvider): +@ModelHandlerProvider.register_handler_type('HuggingFacePipelineModelHandler') +class HuggingFacePipelineModelHandlerProvider(ModelHandlerProvider): def __init__( self, task: Optional[str] = None, @@ -294,6 +294,49 @@ def __init__( inference_fn: Optional[dict[str, str]] = None, load_pipeline_args: Optional[dict[str, Any]] = None, **kwargs): + """ + ModelHandler for Hugging Face Pipelines. + + This Model Handler can be used with RunInference to load a model using + Hugging Face pipelines. Hugging Face pipelines provide a simple way to + perform inference on various tasks (e.g. text classification, token + classification, text generation). + + This Model Handler requires either a `task` or `model` to be specified. + Preprocessing and Postprocessing are described in more detail in the + RunInference docs: + https://beam.apache.org/releases/yamldoc/current/#runinference + + For example: :: + + - type: RunInference + config: + model_handler: + type: HuggingFacePipelineModelHandler + config: + task: text-classification + model: distilbert-base-uncased-finetuned-sst-2-english + preprocess: + callable: 'lambda x: x.text' + + Args: + task: The task for the pipeline. See Hugging Face documentation for + a list of supported tasks. + model: The model name on Hugging Face hub or a path to a local directory. + If the model already defines the task, no need to specify the task. + preprocess: A python callable, defined either inline, or using a file, + that is invoked on the input row before sending to the model to be + loaded by this ModelHandler. + postprocess: A python callable, defined either inline, or using a file, + that is invoked on the PredictionResult output by the ModelHandler + before parsing into the output Beam Row. + device: The device to run the pipeline on (e.g., 'cpu', 'cuda', 'cuda:0'). + Defaults to CPU. + inference_fn: The custom inference function to use. + load_pipeline_args: Extra arguments to pass to the Hugging Face pipeline + loader (e.g. `transformers.pipeline`). + **kwargs: Extra arguments to pass to the model handler. + """ try: from apache_beam.ml.inference.huggingface_inference import HuggingFacePipelineModelHandler except ImportError: @@ -324,7 +367,7 @@ def __init__( def validate(config): if not config or (not config.get('task') and not config.get('model')): raise ValueError( - "HuggingFacePipeline requires either 'task' or " + "HuggingFacePipelineModelHandler requires either 'task' or " "'model' to be specified.") def inference_output_type(self): @@ -488,10 +531,11 @@ def fn(x: PredictionResult): Args: model_handler: Specifies the parameters for the respective - enrichment_handler in a YAML/JSON format. To see the full set of + model_handler in a YAML/JSON format. To see the full set of handler_config parameters, see their corresponding doc pages: - [VertexAIModelHandlerJSON](https://beam.apache.org/releases/pydoc/current/apache_beam.yaml.yaml_ml.VertexAIModelHandlerJSONProvider) # pylint: disable=line-too-long + - [HuggingFacePipelineModelHandler](https://beam.apache.org/releases/pydoc/current/apache_beam.yaml.yaml_ml.HuggingFacePipelineModelHandlerProvider) # pylint: disable=line-too-long inference_tag: The tag to use for the returned inference. Default is 'inference'. inference_args: Extra arguments for models whose inference call requires From b731448fb2c01304fc555c1903d255a7cff836b7 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 9 Jun 2026 15:53:03 -0400 Subject: [PATCH 342/490] Fix yaml doc generation (#38874) --- sdks/python/apache_beam/yaml/standard_io.yaml | 1 - sdks/python/build.gradle | 28 +++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index 781d3de193ec..4f679c4a77c4 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -418,7 +418,6 @@ catalog_properties: 'catalog_properties' config_properties: 'config_properties' triggering_frequency_seconds: 'triggering_frequency_seconds' - append_batch_size: 'append_batch_size' location_prefix: 'location_prefix' partition_fields: 'partition_fields' table_properties: 'table_properties' diff --git a/sdks/python/build.gradle b/sdks/python/build.gradle index 5f09dff57e8f..b39b12f198e9 100644 --- a/sdks/python/build.gradle +++ b/sdks/python/build.gradle @@ -101,16 +101,32 @@ tasks.register("generateManagedIOPage") { } } +tasks.register("prepareExpansionServicesForYamlDocs") { + description "Builds all expansion services referenced in apache_beam/yaml/*.*" + + dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" + dependsOn ":sdks:java:extensions:sql:expansion-service:shadowJar" + dependsOn ":sdks:java:io:expansion-service:shadowJar" + dependsOn ":sdks:java:io:google-cloud-platform:expansion-service:shadowJar" + + doLast { + // Copy expansion service jar into cache path (.apache_beam/cache/jars/). + copy { + from project.tasks.findByPath(":sdks:java:extensions:schemaio-expansion-service:shadowJar") + from project.tasks.findByPath(":sdks:java:extensions:sql:expansion-service:shadowJar") + from project.tasks.findByPath(":sdks:java:io:expansion-service:shadowJar") + from project.tasks.findByPath(":sdks:java:io:google-cloud-platform:expansion-service:shadowJar") + into "${System.getProperty('user.home')}/.apache_beam/cache/jars/" + } + } +} + tasks.register("generateYamlDocs") { description "Generates the reference documentation for all YAML transforms." dependsOn buildPython - // Need to build all expansion services referenced in apache_beam/yaml/*.* - // grep -oh 'sdk.*Jar' sdks/python/apache_beam/yaml/*.yaml | sort | uniq - dependsOn ":sdks:java:extensions:schemaio-expansion-service:shadowJar" - dependsOn ":sdks:java:extensions:sql:expansion-service:shadowJar" - dependsOn ":sdks:java:io:expansion-service:build" - dependsOn ":sdks:java:io:google-cloud-platform:expansion-service:build" + dependsOn prepareExpansionServicesForYamlDocs + def extraPackages = "pyyaml markdown docstring_parser pandas pygments Jinja2 virtualenv-clone" doLast { From f8240a51699738f039fcd1e94e0f5ba117a34fbf Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Tue, 9 Jun 2026 17:04:26 -0400 Subject: [PATCH 343/490] Fix race condition in statesampler_fast.pyx (#38851) * fix race condition in reads * rename test case - doc string * move import to top * address gemini * address gemini comments * address gemini comments --- .../runners/worker/statesampler_fast.pyx | 8 +++- .../runners/worker/statesampler_test.py | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/worker/statesampler_fast.pyx b/sdks/python/apache_beam/runners/worker/statesampler_fast.pyx index 45700a0b0f81..7075ef47017d 100644 --- a/sdks/python/apache_beam/runners/worker/statesampler_fast.pyx +++ b/sdks/python/apache_beam/runners/worker/statesampler_fast.pyx @@ -217,7 +217,13 @@ cdef class ScopedState(object): @property def nsecs(self): - return self._nsecs + cdef pythread.PyThread_type_lock lock = self.sampler.lock + cdef int64_t val + with nogil: + pythread.PyThread_acquire_lock(lock, pythread.WAIT_LOCK) + val = self._nsecs + pythread.PyThread_release_lock(lock) + return val def sampled_seconds(self): return 1e-9 * self.nsecs diff --git a/sdks/python/apache_beam/runners/worker/statesampler_test.py b/sdks/python/apache_beam/runners/worker/statesampler_test.py index 0d0ce1d2c8dc..0495dc507da0 100644 --- a/sdks/python/apache_beam/runners/worker/statesampler_test.py +++ b/sdks/python/apache_beam/runners/worker/statesampler_test.py @@ -19,6 +19,7 @@ # pytype: skip-file import logging +import threading import time import unittest from unittest import mock @@ -312,6 +313,48 @@ def test_do_operation_process_timer_with_exception(self, mock_get_dofn_specs): actual_value, state_duration_ms * (1.0 - margin_of_error)) _LOGGER.info("Exception test finished successfully.") + def test_concurrent_nsecs_reads(self): + """Verify that concurrent reads of nsecs behave correctly under thread contention. + + This test runs state transitions on the main thread and reads `nsecs` properties + from a secondary Python thread, while the background sampler thread is concurrently + updating counter states. + """ + if not statesampler.FAST_SAMPLER: + self.skipTest('test_concurrent_nsecs_reads requires FAST_SAMPLER') + + counter_factory = CounterFactory() + sampler = statesampler.StateSampler( + 'concurrent', counter_factory, sampling_period_ms=1) + + sampler.start() + reader_thread = None + try: + state_a = sampler.scoped_state('step1', 'statea') + state_b = sampler.scoped_state('step1', 'stateb') + + stop_signal = False + + def read_nsecs_loop(): + while not stop_signal: + _ = state_a.nsecs + _ = state_b.nsecs + time.sleep(0.001) + + reader_thread = threading.Thread(target=read_nsecs_loop) + reader_thread.start() + + for _ in range(100): + with state_a: + time.sleep(0.001) + with state_b: + time.sleep(0.001) + finally: + if reader_thread is not None: + stop_signal = True + reader_thread.join() + sampler.stop() + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) From 0f847646030a0a11dfb25b807b9e24d0793e95a1 Mon Sep 17 00:00:00 2001 From: reuvenlax Date: Tue, 9 Jun 2026 15:19:23 -0700 Subject: [PATCH 344/490] Merge pull request #38878: Fix OnWindowExpirationContext. * foo * fix compilation * fix compilation --- .../beam/runners/core/SimpleDoFnRunner.java | 20 +++++++++++++++++++ .../reflect/ByteBuddyDoFnInvokerFactory.java | 12 +++++++++++ .../sdk/transforms/reflect/DoFnInvoker.java | 17 ++++++++++++++++ .../sdk/transforms/reflect/DoFnSignature.java | 9 +++++++++ .../transforms/reflect/DoFnSignatures.java | 3 ++- .../SplittableParDoNaiveBounded.java | 6 ++++++ .../apache/beam/sdk/transforms/ParDoTest.java | 4 ++++ .../reflect/DoFnSignaturesTest.java | 8 ++++++-- .../beam/fn/harness/FnApiDoFnRunner.java | 13 ++++++++++++ 9 files changed, 89 insertions(+), 3 deletions(-) diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java index 470e22a66991..1825b77b65fb 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java @@ -649,6 +649,13 @@ public DoFn.OnTimerContext onTimerContext(DoFn "Cannot access OnTimerContext outside of @OnTimer methods."); } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access OnWindowExpirationContext outside of @OnWindowExpiration methods."); + } + @Override public RestrictionTracker restrictionTracker() { throw new UnsupportedOperationException("RestrictionTracker parameters are not supported."); @@ -958,6 +965,13 @@ public DoFn.OnTimerContext onTimerContext(DoFn return this; } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access OnWindowExpirationContext outside of @OnWindowExpiration methods."); + } + @Override public RestrictionTracker restrictionTracker() { throw new UnsupportedOperationException("RestrictionTracker parameters are not supported."); @@ -1299,6 +1313,12 @@ public DoFn.OnTimerContext onTimerContext(DoFn throw new UnsupportedOperationException("OnTimerContext parameters are not supported."); } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + return this; + } + @Override public RestrictionTracker restrictionTracker() { throw new UnsupportedOperationException("RestrictionTracker parameters are not supported."); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java index 3ebabb6e3c37..c08243fda5c0 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java @@ -142,6 +142,8 @@ class ByteBuddyDoFnInvokerFactory implements DoFnInvokerFactory { public static final String OUTPUT_PARAMETER_METHOD = "outputReceiver"; public static final String TAGGED_OUTPUT_PARAMETER_METHOD = "taggedOutputReceiver"; public static final String ON_TIMER_CONTEXT_PARAMETER_METHOD = "onTimerContext"; + public static final String ON_WINDOW_EXPIRATION_CONTEXT_PARAMETER_METHOD = + "onWindowExpirationContext"; public static final String WINDOW_PARAMETER_METHOD = "window"; public static final String PANE_INFO_PARAMETER_METHOD = "paneInfo"; public static final String PIPELINE_OPTIONS_PARAMETER_METHOD = "pipelineOptions"; @@ -1170,6 +1172,16 @@ public StackManipulation dispatch(OnTimerContextParameter p) { ON_TIMER_CONTEXT_PARAMETER_METHOD, DoFn.class))); } + @Override + public StackManipulation dispatch( + DoFnSignature.Parameter.OnWindowExpirationContextParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + ON_WINDOW_EXPIRATION_CONTEXT_PARAMETER_METHOD, DoFn.class))); + } + @Override public StackManipulation dispatch(WindowParameter p) { return new StackManipulation.Compound( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java index eaabdff907c7..c8c7ddf24b69 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java @@ -185,6 +185,10 @@ interface ArgumentProvider { /** Provide a {@link DoFn.OnTimerContext} to use with the given {@link DoFn}. */ DoFn.OnTimerContext onTimerContext(DoFn doFn); + /** Provide a {@link DoFn.OnWindowExpirationContext} to use with the given {@link DoFn}. */ + DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn); + /** Provide a reference to the input element. */ InputT element(DoFn doFn); @@ -447,6 +451,13 @@ public DoFn.OnTimerContext onTimerContext(DoFn String.format("OnTimerContext unsupported in %s", getErrorContext())); } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + throw new UnsupportedOperationException( + String.format("OnWindowExpirationContext unsupported in %s", getErrorContext())); + } + @Override public State state(String stateId, boolean alwaysFetched) { throw new UnsupportedOperationException( @@ -538,6 +549,12 @@ public DoFn.OnTimerContext onTimerContext(DoFn return delegate.onTimerContext(doFn); } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + return delegate.onWindowExpirationContext(doFn); + } + @Override public InputT element(DoFn doFn) { return delegate.element(doFn); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java index 51dadd178a6f..99b002c1106d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignature.java @@ -305,6 +305,8 @@ public ResultT match(Cases cases) { return cases.dispatch((ProcessContextParameter) this); } else if (this instanceof OnTimerContextParameter) { return cases.dispatch((OnTimerContextParameter) this); + } else if (this instanceof OnWindowExpirationContextParameter) { + return cases.dispatch((OnWindowExpirationContextParameter) this); } else if (this instanceof WindowParameter) { return cases.dispatch((WindowParameter) this); } else if (this instanceof PaneInfoParameter) { @@ -391,6 +393,8 @@ public interface Cases { ResultT dispatch(OnTimerContextParameter p); + ResultT dispatch(OnWindowExpirationContextParameter p); + ResultT dispatch(WindowParameter p); ResultT dispatch(PaneInfoParameter p); @@ -498,6 +502,11 @@ public ResultT dispatch(OnTimerContextParameter p) { return dispatchDefault(p); } + @Override + public ResultT dispatch(OnWindowExpirationContextParameter p) { + return dispatchDefault(p); + } + @Override public ResultT dispatch(WindowParameter p) { return dispatchDefault(p); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java index 2983fc94021c..9f3491bca7b9 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java @@ -229,7 +229,8 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.TimestampParameter.class, Parameter.KeyParameter.class, - Parameter.SideInputParameter.class); + Parameter.SideInputParameter.class, + Parameter.OnWindowExpirationContextParameter.class); private static final Collection> ALLOWED_GET_INITIAL_RESTRICTION_PARAMETERS = diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java index d1fb23e77c47..52b1174e3a03 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/SplittableParDoNaiveBounded.java @@ -506,6 +506,12 @@ public DoFn.OnTimerContext onTimerContext(DoFn throw new IllegalStateException(); } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + throw new IllegalStateException(); + } + @Override public InputT element(DoFn doFn) { return element; diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java index 0c984d01c8f0..6beea338689b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/ParDoTest.java @@ -7335,11 +7335,15 @@ public void onTimer( @OnWindowExpiration public void onWindowExpiration( @AlwaysFetched @StateId(stateId) ValueState state, + BoundedWindow window, @Key String key, + OnWindowExpirationContext context, OutputReceiver r) { Integer currentValue = MoreObjects.firstNonNull(state.read(), 0); // verify state assertEquals(1, (int) currentValue); + Preconditions.checkNotNull(context); + assertEquals(window, context.window()); // To check output is received from OnWindowExpiration r.output(currentValue); } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java index 5a5353482c95..330bb5b94418 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java @@ -1422,16 +1422,20 @@ public void bar( @StateId("foo") ValueState s, PipelineOptions p, OutputReceiver o, - MultiOutputReceiver m) {} + MultiOutputReceiver m, + OnWindowExpirationContext c) {} }.getClass()); List params = sig.onWindowExpiration().extraParameters(); - assertThat(params.size(), equalTo(5)); + assertThat(params.size(), equalTo(6)); assertThat(params.get(0), instanceOf(WindowParameter.class)); assertThat(params.get(1), instanceOf(StateParameter.class)); assertThat(params.get(2), instanceOf(PipelineOptionsParameter.class)); assertThat(params.get(3), instanceOf(OutputReceiverParameter.class)); assertThat(params.get(4), instanceOf(TaggedOutputReceiverParameter.class)); + assertThat( + params.get(5), + instanceOf(DoFnSignature.Parameter.OnWindowExpirationContextParameter.class)); } private interface FeatureTest { diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java index 3e4675ab074a..d5b5cebadb34 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java @@ -2250,6 +2250,13 @@ public DoFn.OnTimerContext onTimerContext(DoFn "Cannot access OnTimerContext outside of @OnTimer methods."); } + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access OnWindowExpirationContext outside of @OnWindowExpiration methods."); + } + @Override public RestrictionTracker restrictionTracker() { return currentTracker; @@ -2469,6 +2476,12 @@ private void checkOnWindowExpirationTimestamp(Instant timestamp) { private final OnWindowExpirationContext.Context context = new OnWindowExpirationContext.Context(); + @Override + public DoFn.OnWindowExpirationContext onWindowExpirationContext( + DoFn doFn) { + return context; + } + @Override public BoundedWindow window() { return currentWindow; From da0a6a04b2826f0eeee9009ddeb5c3ffaf04138f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:13:38 -0400 Subject: [PATCH 345/490] Bump github.com/aws/aws-sdk-go-v2/config in /sdks (#38884) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.23 to 1.32.24. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.23...config/v1.32.24) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.24 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index e110296eb540..d24339c95592 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,7 +33,7 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.3 github.com/aws/aws-sdk-go-v2 v1.42.0 - github.com/aws/aws-sdk-go-v2/config v1.32.23 + github.com/aws/aws-sdk-go-v2/config v1.32.24 github.com/aws/aws-sdk-go-v2/credentials v1.19.23 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 diff --git a/sdks/go.sum b/sdks/go.sum index b8a399fadc1f..f18a8eaa0d5a 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,8 +207,8 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7z github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.23 h1:PYDobtcsJXK6bQe9I8RQk6s19Bz3xa3xRU08Hy1Em3Y= -github.com/aws/aws-sdk-go-v2/config v1.32.23/go.mod h1:QID4dqUQVgEOYPKsPWd1sNWCCR2c5g7o3jeEtIXPOZU= +github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs= +github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= From f4d7946836c605f16fc93a7fbab5a64a20bacdfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:14:16 -0400 Subject: [PATCH 346/490] Bump google.golang.org/api from 0.283.0 to 0.284.0 in /sdks (#38887) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.283.0 to 0.284.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.283.0...v0.284.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.284.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index d24339c95592..0ebeac749be7 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.21.0 golang.org/x/sys v0.46.0 golang.org/x/text v0.38.0 - google.golang.org/api v0.283.0 + google.golang.org/api v0.284.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 @@ -204,5 +204,5 @@ require ( golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index f18a8eaa0d5a..0d3c8b0d1cf8 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1324,8 +1324,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik= -google.golang.org/api v0.283.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= +google.golang.org/api v0.284.0 h1:i+cKTgeQRcRySkP7QTl5PDO7/pAm8EcMFIUMlNbk4Vc= +google.golang.org/api v0.284.0/go.mod h1:AU44fU+XVZOCcd8uLaBIa/ZgzgPf/0qqY3+m7lQaado= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1423,8 +1423,8 @@ google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 h1:cTHF8xtqtBN5sQ4 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc= google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw= google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From 432e0f7b3b4412da996ca5d5d66cd3cf723aacd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:14:44 -0400 Subject: [PATCH 347/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38886) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.25 to 1.22.26. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.25...feature/s3/manager/v1.22.26) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.26 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 0ebeac749be7..bd415ede1b52 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.24 github.com/aws/aws-sdk-go-v2/credentials v1.19.23 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 github.com/aws/smithy-go v1.27.2 github.com/docker/go-connections v0.7.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 0d3c8b0d1cf8..a0db92fd7ec8 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEt github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25 h1:xJ3WVH3J0xESIqkavgbNfvQdMzB98bSkGwFXCyM2Tdw= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.25/go.mod h1:0EOIx2v10rBBcaoTpOsRRNKDxhtRecRbN42YMrJaKZE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26 h1:YNUEPr7Yiako/MzR/h3woMREbdwj0hGiBsZc5ZM90yE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26/go.mod h1:KswdJ4xh+tUgW5CWx7sarhQuD+3iKgg46wojfmCA8q4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= From 338927c9922be500d3a34e4656c02ee0e3505d94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:15:21 -0400 Subject: [PATCH 348/490] Bump actions/checkout from 4 to 6 (#38885) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 30539e8ec08a..95d128d53658 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -74,7 +74,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` From df32f87f6761fff98f0c5def1e43cae2ee499441 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 10 Jun 2026 14:30:28 -0400 Subject: [PATCH 349/490] update iceberg add files parameters (#38879) --- sdks/python/apache_beam/yaml/standard_io.yaml | 2 ++ sdks/python/apache_beam/yaml/tests/iceberg_add_files.yaml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index 4f679c4a77c4..1790dc0f5a9a 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -418,9 +418,11 @@ catalog_properties: 'catalog_properties' config_properties: 'config_properties' triggering_frequency_seconds: 'triggering_frequency_seconds' + manifest_file_size: 'manifest_file_size' location_prefix: 'location_prefix' partition_fields: 'partition_fields' table_properties: 'table_properties' + sort_fields: 'sort_fields' error_handling: 'error_handling' underlying_provider: type: beamJar diff --git a/sdks/python/apache_beam/yaml/tests/iceberg_add_files.yaml b/sdks/python/apache_beam/yaml/tests/iceberg_add_files.yaml index 1e089f19ac0e..278bbe390d86 100644 --- a/sdks/python/apache_beam/yaml/tests/iceberg_add_files.yaml +++ b/sdks/python/apache_beam/yaml/tests/iceberg_add_files.yaml @@ -55,6 +55,9 @@ pipelines: catalog_properties: type: "hadoop" warehouse: "{TEMP_DIR}/dir" + manifest_file_size: 50 + sort_fields: + - "rank desc" # Pipeline 3: Read from Iceberg and verify the contents - pipeline: From 3f93241627911ecb7e8b2bf378665d4a6f1af774 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 10 Jun 2026 15:51:32 -0400 Subject: [PATCH 350/490] add datadog io normalization and yaml (#38362) * draft files * currently works * initial test file * add more test cases * add doc strings and slight code improvements * adding more test cases and working out errorHandling * works with one test failure * updated error handling logic and add more tests * fix conflict with standard_io * all tests pass - need to add write verification through Datadog agent/store * first draft of yaml test file * some minor changes to schematransformer and write error schema; also adding initil yaml test file * working snapshot before simplification * combine errors * import constants * rollback DatadogIO cleanups * rollback DatadogWriteError cleanups * rollback SYNCHRONIZED_PROCESSING_TIME in timeutil.py * simplify expansion-service datadog dependency to runtimeOnly * revert some previous cleanup operations * restore proper trailing newline to build.gradle * fix lint issues * run standard external transform script * fix gemini review comments * update logic for better performance * fix yaml row failure * Trigger fresh CI/CD run * update coder * old design parts * revert datadogio coder change and try to cover in integration_tests * fix coder * fix lint * address more geminic comments * fix error output * fix another gemini review * remove some dead code * fix spotless * revert venv * address comments * address agent to fake server comment * format * address Cham's comments --- .../beam/sdk/io/datadog/DatadogEvent.java | 6 + ...adogWriteSchemaTransformConfiguration.java | 114 ++++ .../DatadogWriteSchemaTransformProvider.java | 294 ++++++++++ ...tadogWriteSchemaTransformProviderTest.java | 535 ++++++++++++++++++ sdks/java/io/expansion-service/build.gradle | 1 + .../apache_beam/yaml/integration_tests.py | 34 ++ sdks/python/apache_beam/yaml/standard_io.yaml | 21 + .../apache_beam/yaml/test_utils/__init__.py | 18 + .../yaml/test_utils/datadog_test_utils.py | 131 +++++ .../apache_beam/yaml/tests/datadog.yaml | 63 +++ sdks/standard_external_transforms.yaml | 35 ++ 11 files changed, 1252 insertions(+) create mode 100644 sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformConfiguration.java create mode 100644 sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProvider.java create mode 100644 sdks/java/io/datadog/src/test/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProviderTest.java create mode 100644 sdks/python/apache_beam/yaml/test_utils/__init__.py create mode 100644 sdks/python/apache_beam/yaml/test_utils/datadog_test_utils.py create mode 100644 sdks/python/apache_beam/yaml/tests/datadog.yaml diff --git a/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogEvent.java b/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogEvent.java index 80334b5e4664..e9a2546d9d9e 100644 --- a/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogEvent.java +++ b/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogEvent.java @@ -26,6 +26,12 @@ @AutoValue public abstract class DatadogEvent { + public static final String SOURCE = "ddsource"; + public static final String TAGS = "ddtags"; + public static final String HOSTNAME = "hostname"; + public static final String SERVICE = "service"; + public static final String MESSAGE = "message"; + public static Builder newBuilder() { return new AutoValue_DatadogEvent.Builder(); } diff --git a/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformConfiguration.java b/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformConfiguration.java new file mode 100644 index 000000000000..059b3b33ed4f --- /dev/null +++ b/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformConfiguration.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.datadog; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; + +/** + * Configuration for writing to Datadog. + * + *

      This class is meant to be used with {@link DatadogWriteSchemaTransformProvider}. + */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class DatadogWriteSchemaTransformConfiguration { + + public void validate() { + String invalidConfigMessage = "Invalid Datadog Write configuration: "; + checkArgument(!getUrl().isEmpty(), invalidConfigMessage + "url must be specified."); + checkArgument(!getApiKey().isEmpty(), invalidConfigMessage + "apiKey must be specified."); + Integer batchCount = getBatchCount(); + if (batchCount != null) { + checkArgument(batchCount > 0, invalidConfigMessage + "batchCount must be greater than 0."); + } + Integer minBatchCount = getMinBatchCount(); + if (minBatchCount != null) { + checkArgument( + minBatchCount > 0, invalidConfigMessage + "minBatchCount must be greater than 0."); + } + Long maxBufferSize = getMaxBufferSize(); + if (maxBufferSize != null) { + checkArgument( + maxBufferSize > 0, invalidConfigMessage + "maxBufferSize must be greater than 0."); + } + Integer parallelism = getParallelism(); + if (parallelism != null) { + checkArgument(parallelism > 0, invalidConfigMessage + "parallelism must be greater than 0."); + } + ErrorHandling errorHandling = getErrorHandling(); + if (errorHandling != null) { + checkArgument( + !Strings.isNullOrEmpty(errorHandling.getOutput()), + invalidConfigMessage + "Output must not be empty if error handling specified."); + } + } + + /** Instantiates a {@link DatadogWriteSchemaTransformConfiguration.Builder} instance. */ + public static DatadogWriteSchemaTransformConfiguration.Builder builder() { + return new AutoValue_DatadogWriteSchemaTransformConfiguration.Builder(); + } + + @SchemaFieldDescription("The Datadog API URL.") + public abstract String getUrl(); + + @SchemaFieldDescription("The Datadog API key.") + public abstract String getApiKey(); + + @SchemaFieldDescription("The minimum number of events to batch together for each write.") + public abstract @Nullable Integer getMinBatchCount(); + + @SchemaFieldDescription("The number of events to batch together for each write.") + public abstract @Nullable Integer getBatchCount(); + + @SchemaFieldDescription("The maximum buffer size in bytes.") + public abstract @Nullable Long getMaxBufferSize(); + + @SchemaFieldDescription("The degree of parallelism for writing.") + public abstract @Nullable Integer getParallelism(); + + @SchemaFieldDescription("Specifies how to handle errors.") + public abstract @Nullable ErrorHandling getErrorHandling(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setUrl(String url); + + public abstract Builder setApiKey(String apiKey); + + public abstract Builder setMinBatchCount(Integer minBatchCount); + + public abstract Builder setBatchCount(Integer batchCount); + + public abstract Builder setMaxBufferSize(Long maxBufferSize); + + public abstract Builder setParallelism(Integer parallelism); + + public abstract Builder setErrorHandling(@Nullable ErrorHandling errorHandling); + + /** Builds the {@link DatadogWriteSchemaTransformConfiguration} configuration. */ + public abstract DatadogWriteSchemaTransformConfiguration build(); + } +} diff --git a/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProvider.java b/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProvider.java new file mode 100644 index 000000000000..d0013ac04225 --- /dev/null +++ b/sdks/java/io/datadog/src/main/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProvider.java @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.datadog; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.service.AutoService; +import java.util.Collections; +import java.util.List; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; + +@AutoService(SchemaTransformProvider.class) +public class DatadogWriteSchemaTransformProvider + extends TypedSchemaTransformProvider { + private static final String IDENTIFIER = "beam:schematransform:org.apache.beam:datadog_write:v1"; + static final String INPUT = "input"; + static final String OUTPUT = "output"; + static final String ERROR = "errors"; + public static final TupleTag ERROR_TAG = new TupleTag() {}; + public static final TupleTag OUTPUT_TAG = new TupleTag() {}; + public static final TupleTag EVENT_TAG = new TupleTag() {}; + + @Override + protected Class configurationClass() { + return DatadogWriteSchemaTransformConfiguration.class; + } + + /** Returns the expected {@link SchemaTransform} of the configuration. */ + @Override + protected SchemaTransform from(DatadogWriteSchemaTransformConfiguration configuration) { + return new DatadogWriteSchemaTransform(configuration); + } + + /** Implementation of the {@link TypedSchemaTransformProvider} identifier method. */ + @Override + public String identifier() { + return IDENTIFIER; + } + + /** Implementation of the {@link TypedSchemaTransformProvider} input collection names method. */ + @Override + public List inputCollectionNames() { + return Collections.singletonList(INPUT); + } + + /** Implementation of the {@link TypedSchemaTransformProvider} output collection names method. */ + @Override + public List outputCollectionNames() { + return Collections.singletonList(ERROR); + } + + /** + * An implementation of {@link SchemaTransform} for Datadog Write jobs configured using {@link + * DatadogWriteSchemaTransformConfiguration}. + */ + static class DatadogWriteSchemaTransform extends SchemaTransform { + private final DatadogWriteSchemaTransformConfiguration configuration; + + DatadogWriteSchemaTransform(DatadogWriteSchemaTransformConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + // Validate configuration parameters + configuration.validate(); + + // Obtain input rows + PCollection inputRows = input.get(INPUT); + + // Check for errors + boolean handleErrors = ErrorHandling.hasOutput(configuration.getErrorHandling()); + + Schema inputSchema = inputRows.getSchema(); + Schema dynamicErrorSchema = + Schema.builder() + .addNullableRowField("failed_row", inputSchema) + .addNullableField("payload", Schema.FieldType.STRING) + .addNullableField("statusCode", Schema.FieldType.INT32) + .addNullableField("statusMessage", Schema.FieldType.STRING) + .build(); + + PCollectionTuple convertResult = + inputRows.apply( + "Convert to DatadogEvent", + ParDo.of(new RowToEventFn(handleErrors, ERROR_TAG, dynamicErrorSchema)) + .withOutputTags(EVENT_TAG, TupleTagList.of(ERROR_TAG))); + + PCollection datadogEvents = + convertResult.get(EVENT_TAG).setCoder(DatadogEventCoder.of()); + PCollection conversionErrors = + convertResult + .get(ERROR_TAG) + .setCoder(org.apache.beam.sdk.coders.RowCoder.of(dynamicErrorSchema)); + + // Configure DatadogIO.Write + DatadogIO.Write.Builder builder = + DatadogIO.writeBuilder(configuration.getMinBatchCount()) + .withUrl(configuration.getUrl()) + .withApiKey(configuration.getApiKey()); + + Integer batchCount = configuration.getBatchCount(); + if (batchCount != null) { + builder = builder.withBatchCount(batchCount); + } + Long maxBufferSize = configuration.getMaxBufferSize(); + if (maxBufferSize != null) { + builder = builder.withMaxBufferSize(maxBufferSize); + } + Integer parallelism = configuration.getParallelism(); + if (parallelism != null) { + builder = builder.withParallelism(parallelism); + } + + DatadogIO.Write write = builder.build(); + + // Apply DatadogIO.Write + PCollection writeErrors = datadogEvents.apply("Write To Datadog", write); + + // Handle errors + ErrorHandling errorHandling = configuration.getErrorHandling(); + if (errorHandling != null) { + PCollection writeErrorRows = + writeErrors + .apply( + "Convert Write Errors to Rows", + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.rows()) + .via( + error -> + Row.withSchema(dynamicErrorSchema) + .addValue(null) + .addValue(error.payload()) + .addValue(error.statusCode()) + .addValue(error.statusMessage()) + .build())) + .setCoder(org.apache.beam.sdk.coders.RowCoder.of(dynamicErrorSchema)); + + PCollection allErrors = + org.apache.beam.sdk.values.PCollectionList.of(conversionErrors) + .and(writeErrorRows) + .apply("Flatten Errors", org.apache.beam.sdk.transforms.Flatten.pCollections()) + .setCoder(org.apache.beam.sdk.coders.RowCoder.of(dynamicErrorSchema)); + + return PCollectionRowTuple.of(errorHandling.getOutput(), allErrors); + } else { + writeErrors.apply("Fail on Write Error", ParDo.of(new FailOnWriteErrorFn())); + PCollection emptyErrors = + input + .getPipeline() + .apply("Empty Errors Placeholder", Create.empty(RowCoder.of(dynamicErrorSchema))); + return PCollectionRowTuple.of(ERROR, emptyErrors); + } + } + } + + static final Schema WRITE_ERROR_SCHEMA = + Schema.builder() + .addNullableField("payload", Schema.FieldType.STRING) + .addNullableField("statusCode", Schema.FieldType.INT32) + .addNullableField("statusMessage", Schema.FieldType.STRING) + .build(); + + static final Schema DATADOG_EVENT_SCHEMA = + Schema.builder() + .addNullableField(DatadogEvent.SOURCE, Schema.FieldType.STRING) + .addNullableField(DatadogEvent.TAGS, Schema.FieldType.STRING) + .addNullableField(DatadogEvent.HOSTNAME, Schema.FieldType.STRING) + .addNullableField(DatadogEvent.SERVICE, Schema.FieldType.STRING) + .addNullableField(DatadogEvent.MESSAGE, Schema.FieldType.STRING) + .build(); + + static Row eventToRow(DatadogEvent event) { + return Row.withSchema(DATADOG_EVENT_SCHEMA) + .addValue(event.ddsource()) + .addValue(event.ddtags()) + .addValue(event.hostname()) + .addValue(event.service()) + .addValue(event.message()) + .build(); + } + + static DatadogEvent rowToEvent(Row row) { + DatadogEvent.Builder builder = DatadogEvent.newBuilder(); + Schema schema = row.getSchema(); + + String ddsource = + schema.hasField(DatadogEvent.SOURCE) ? row.getString(DatadogEvent.SOURCE) : null; + if (ddsource != null) { + builder.withSource(ddsource); + } + String ddtags = schema.hasField(DatadogEvent.TAGS) ? row.getString(DatadogEvent.TAGS) : null; + if (ddtags != null) { + builder.withTags(ddtags); + } + String hostname = + schema.hasField(DatadogEvent.HOSTNAME) ? row.getString(DatadogEvent.HOSTNAME) : null; + if (hostname != null) { + builder.withHostname(hostname); + } + String service = + schema.hasField(DatadogEvent.SERVICE) ? row.getString(DatadogEvent.SERVICE) : null; + if (service != null) { + builder.withService(service); + } + String message = + schema.hasField(DatadogEvent.MESSAGE) ? row.getString(DatadogEvent.MESSAGE) : null; + builder.withMessage(checkNotNull(message, "Message is required.")); + + return builder.build(); + } + + static class RowToEventFn extends DoFn { + private final boolean handleErrors; + private final TupleTag errorOutputTag; + private final Schema errorSchema; + + RowToEventFn(boolean handleErrors, TupleTag errorOutputTag, Schema errorSchema) { + this.handleErrors = handleErrors; + this.errorOutputTag = errorOutputTag; + this.errorSchema = errorSchema; + } + + @ProcessElement + public void processElement(ProcessContext c) { + try { + c.output(rowToEvent(c.element())); + } catch (Exception e) { + if (handleErrors) { + String rowString = c.element().toString(); + String payload = rowString.length() <= 1024 ? rowString : rowString.substring(0, 1024); + c.output( + errorOutputTag, + Row.withSchema(errorSchema) + .addValue(c.element()) + .addValue(payload) + .addValue(java.net.HttpURLConnection.HTTP_BAD_REQUEST) + .addValue(e.getMessage()) + .build()); + } else { + throw new RuntimeException(e); + } + } + } + } + + /** + * A {@link DoFn} that throws a {@link RuntimeException} when a write error is encountered, + * causing the pipeline to fail. This is the default error handling behavior when no error output + * is configured. + */ + static class FailOnWriteErrorFn extends DoFn { + @ProcessElement + public void processElement(@Element DatadogWriteError error) { + String message = error.statusMessage(); + if (error.statusCode() != null) { + throw new RuntimeException( + String.format( + "Datadog write failed with status code %d: %s", error.statusCode(), message)); + } else { + throw new RuntimeException("Datadog write failed: " + message); + } + } + } +} diff --git a/sdks/java/io/datadog/src/test/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProviderTest.java b/sdks/java/io/datadog/src/test/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProviderTest.java new file mode 100644 index 000000000000..534251fb4c19 --- /dev/null +++ b/sdks/java/io/datadog/src/test/java/org/apache/beam/sdk/io/datadog/DatadogWriteSchemaTransformProviderTest.java @@ -0,0 +1,535 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.datadog; + +import static org.apache.beam.sdk.io.datadog.DatadogWriteSchemaTransformProvider.ERROR; +import static org.apache.beam.sdk.io.datadog.DatadogWriteSchemaTransformProvider.INPUT; +import static org.apache.beam.sdk.io.datadog.DatadogWriteSchemaTransformProvider.OUTPUT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.beam.sdk.Pipeline.PipelineExecutionException; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RunWith(JUnit4.class) +public class DatadogWriteSchemaTransformProviderTest { + + private static final Logger LOG = + LoggerFactory.getLogger(DatadogWriteSchemaTransformProviderTest.class); + + @Rule public TestPipeline p = TestPipeline.create(); + + private static final Schema SCHEMA = + Schema.builder() + .addStringField("ddsource") + .addNullableField("ddtags", Schema.FieldType.STRING) + .addStringField("hostname") + .addNullableField("service", Schema.FieldType.STRING) + .addStringField("message") + .build(); + + private static final List ROWS = + Arrays.asList( + Row.withSchema(SCHEMA) + .withFieldValue("ddsource", "my-source") + .withFieldValue("ddtags", "tag1:value1,tag2") + .withFieldValue("hostname", "my-host") + .withFieldValue("service", "my-service") + .withFieldValue("message", "Hello World 1") + .build(), + Row.withSchema(SCHEMA) + .withFieldValue("ddsource", "my-source-2") + .withFieldValue("ddtags", null) + .withFieldValue("hostname", "my-host-2") + .withFieldValue("service", null) + .withFieldValue("message", "Hello World 2") + .build()); + + private List events; + + @org.junit.Before + public void setUp() { + events = + Arrays.asList( + DatadogEvent.newBuilder() + .withSource("my-source") + .withTags("tag1:value1,tag2") + .withHostname("my-host") + .withService("my-service") + .withMessage("Hello World 1") + .build(), + DatadogEvent.newBuilder() + .withSource("my-source-2") + .withHostname("my-host-2") + .withMessage("Hello World 2") + .build()); + } + + @Test + public void testWriteInvalidConfigurations() { + + // apiKey not set + assertThrows( + IllegalStateException.class, + () -> { + DatadogWriteSchemaTransformConfiguration.builder() + .setUrl("http://localhost:8080") + // .setApiKey("test-api-key") # ApiKey is mandatory + .build() + .validate(); + }); + + // url not set + assertThrows( + IllegalStateException.class, + () -> { + DatadogWriteSchemaTransformConfiguration.builder() + // .setUrl("http://localhost:8080") # Url is mandatory + .setApiKey("test-api-key") + .build() + .validate(); + }); + } + + @Test + public void testWriteBuildTransform() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + DatadogWriteSchemaTransformConfiguration configuration = + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .build(); + + provider.from(configuration); + } + + @Test + public void testWriteBuildTransformAndRun() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + DatadogWriteSchemaTransformConfiguration configuration = + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .build(); + + SchemaTransform transform = provider.from(configuration); + + PCollection input = p.apply("Create", Create.of(ROWS).withRowSchema(SCHEMA)); + PCollectionRowTuple inputTuple = PCollectionRowTuple.of(INPUT, input); + PCollectionRowTuple output = transform.expand(inputTuple); + assertEquals(1, output.getAll().size()); + assertTrue(output.has(ERROR)); + + assertThrows(PipelineExecutionException.class, () -> p.run().waitUntilFinish()); + } + + @Test + public void testWriteBuildTransformWithCorrectFields() { + ServiceLoader serviceLoader = + ServiceLoader.load(SchemaTransformProvider.class); + List providers = + StreamSupport.stream(serviceLoader.spliterator(), false) + .filter(provider -> provider.getClass() == DatadogWriteSchemaTransformProvider.class) + .collect(Collectors.toList()); + SchemaTransformProvider datadogProvider = providers.get(0); + assertEquals(datadogProvider.outputCollectionNames(), Lists.newArrayList(ERROR)); + + assertEquals( + Sets.newHashSet( + "url", + "api_key", + "min_batch_count", + "batch_count", + "max_buffer_size", + "parallelism", + "error_handling"), + datadogProvider.configurationSchema().getFields().stream() + .map(field -> field.getName()) + .collect(Collectors.toSet())); + } + + @Test + public void testRowToDatadogEvent() { + for (int i = 0; i < ROWS.size(); i++) { + DatadogEvent actual = DatadogWriteSchemaTransformProvider.rowToEvent(ROWS.get(i)); + assertEquals(events.get(i), actual); + } + } + + @Test + public void testRowToDatadogEventWithMissingOptionalFields() { + Schema missingFieldsSchema = + Schema.builder() + .addStringField("ddsource") + .addStringField("hostname") + .addStringField("message") + .build(); + + Row row = + Row.withSchema(missingFieldsSchema) + .withFieldValue("ddsource", "my-source") + .withFieldValue("hostname", "my-host") + .withFieldValue("message", "Hello World 1") + .build(); + + DatadogEvent expectedEvent = + DatadogEvent.newBuilder() + .withSource("my-source") + .withHostname("my-host") + .withMessage("Hello World 1") + .build(); + + DatadogEvent actual = DatadogWriteSchemaTransformProvider.rowToEvent(row); + assertEquals(expectedEvent, actual); + } + + @Test + public void testRowToDatadogEventWithExtraFields_DiscardsExtraFields() { + Schema extraFieldsSchema = + Schema.builder() + .addStringField("ddsource") + .addNullableField("ddtags", Schema.FieldType.STRING) + .addStringField("hostname") + .addNullableField("service", Schema.FieldType.STRING) + .addStringField("message") + .addStringField("extra_field") + .build(); + + Row row = + Row.withSchema(extraFieldsSchema) + .withFieldValue("ddsource", "my-source") + .withFieldValue("ddtags", "tag1:value1,tag2") + .withFieldValue("hostname", "my-host") + .withFieldValue("service", "my-service") + .withFieldValue("message", "Hello World 1") + .withFieldValue("extra_field", "extra_value") + .build(); + + DatadogEvent expectedEvent = + DatadogEvent.newBuilder() + .withSource("my-source") + .withTags("tag1:value1,tag2") + .withHostname("my-host") + .withService("my-service") + .withMessage("Hello World 1") + .build(); + + DatadogEvent actual = DatadogWriteSchemaTransformProvider.rowToEvent(row); + assertEquals(expectedEvent, actual); + } + + @Test(expected = NullPointerException.class) + public void testRowToDatadogEventWithNullRequiredField() { + Schema nullSchema = + Schema.builder() + .addStringField("ddsource") + .addNullableField("ddtags", Schema.FieldType.STRING) + .addStringField("hostname") + .addNullableField("service", Schema.FieldType.STRING) + .addNullableField("message", Schema.FieldType.STRING) + .build(); + + Row row = + Row.withSchema(nullSchema) + .withFieldValue("ddsource", "my-source") + .withFieldValue("ddtags", "tag1:value1,tag2") + .withFieldValue("hostname", "my-host") + .withFieldValue("service", "my-service") + .withFieldValue("message", null) + .build(); + + DatadogWriteSchemaTransformProvider.rowToEvent(row); + } + + @Test + public void testBuildTransformWithAllParameters() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + DatadogWriteSchemaTransformConfiguration configuration = + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .setBatchCount(10) + .setMaxBufferSize(100L) + .setParallelism(2) + .build(); + + SchemaTransform transform = provider.from(configuration); + + PCollection input = p.apply("Create", Create.of(ROWS).withRowSchema(SCHEMA)); + PCollectionRowTuple inputTuple = PCollectionRowTuple.of(INPUT, input); + PCollectionRowTuple output = transform.expand(inputTuple); + assertEquals(1, output.getAll().size()); + assertTrue(output.has(ERROR)); + + assertThrows(PipelineExecutionException.class, () -> p.run().waitUntilFinish()); + } + + @Test(expected = IllegalStateException.class) + public void testBuildTransformMissingUrl() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + provider.from( + DatadogWriteSchemaTransformConfiguration.builder().setApiKey("test-api-key").build()); + } + + @Test(expected = IllegalStateException.class) + public void testBuildTransformMissingApiKey() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + provider.from(DatadogWriteSchemaTransformConfiguration.builder().setUrl("test-url").build()); + } + + @Test + public void testBuildTransformWithInvalidParallelism() { + assertThrows( + IllegalArgumentException.class, + () -> { + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .setParallelism(0) + .build() + .validate(); + }); + } + + @Test + public void testBuildTransformWithInvalidBatchCount() { + assertThrows( + IllegalArgumentException.class, + () -> { + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .setBatchCount(0) + .setMinBatchCount(1) + .build() + .validate(); + }); + } + + @Test + public void testBuildTransformFromRowConfiguration() throws NoSuchSchemaException { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + Schema configSchema = provider.configurationSchema(); + Schema errorHandlingSchema = configSchema.getField("error_handling").getType().getRowSchema(); + + Row errorHandlingRow = + Row.withSchema(errorHandlingSchema).withFieldValue(OUTPUT, ERROR).build(); + + Row configRow = + Row.withSchema(configSchema) + .withFieldValue("url", "http://localhost:8080") + .withFieldValue("api_key", "test-api-key") + .withFieldValue("min_batch_count", null) + .withFieldValue("batch_count", 10) + .withFieldValue("max_buffer_size", 100L) + .withFieldValue("parallelism", 2) + .withFieldValue("error_handling", errorHandlingRow) + .build(); + + SchemaTransform transform = provider.from(configRow); + + PCollection input = p.apply("Create", Create.of(ROWS).withRowSchema(SCHEMA)); + PCollectionRowTuple inputTuple = PCollectionRowTuple.of(INPUT, input); + PCollectionRowTuple output = transform.expand(inputTuple); + assertEquals(1, output.getAll().size()); + assertTrue(output.has(ERROR)); + + p.run().waitUntilFinish(); + } + + @Test(expected = ClassCastException.class) + public void testRowToDatadogEventWithWrongType() { + Schema wrongSchema = + Schema.builder() + .addStringField("ddsource") + .addInt64Field("ddtags") + .addStringField("hostname") + .addStringField("message") + .build(); + + Row row = + Row.withSchema(wrongSchema) + .withFieldValue("ddsource", "my-source") + .withFieldValue("ddtags", 123L) + .withFieldValue("hostname", "my-host") + .withFieldValue("message", "Hello World 1") + .build(); + + DatadogWriteSchemaTransformProvider.rowToEvent(row); + } + + @Test + public void testErrorHandling() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + ErrorHandling errorHandling = ErrorHandling.builder().setOutput(ERROR).build(); + DatadogWriteSchemaTransformConfiguration configuration = + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .setBatchCount(10) + .setMaxBufferSize(1L) + .setParallelism(1) + .setErrorHandling(errorHandling) + .build(); + + Schema nullSchema = + Schema.builder() + .addStringField("ddsource") + .addNullableField("ddtags", Schema.FieldType.STRING) + .addStringField("hostname") + .addNullableField("service", Schema.FieldType.STRING) + .addNullableField("message", Schema.FieldType.STRING) + .build(); + + Row row = + Row.withSchema(nullSchema) + .withFieldValue("ddsource", "my-source") + .withFieldValue("ddtags", "tag1:value1,tag2") + .withFieldValue("hostname", "my-host") + .withFieldValue("service", "my-service") + .withFieldValue("message", null) + .build(); + + PCollection input = p.apply(Create.of(row).withRowSchema(nullSchema)); + PCollectionRowTuple inputTuple = PCollectionRowTuple.of(INPUT, input); + + SchemaTransform transform = provider.from(configuration); + PCollectionRowTuple outputTuple = transform.expand(inputTuple); + + assertTrue(outputTuple.has(ERROR)); + PAssert.that(outputTuple.get(ERROR)) + .satisfies( + (errors) -> { + assertEquals(1, errors.spliterator().getExactSizeIfKnown()); + Row error = errors.iterator().next(); + assertEquals(row.toString(), error.getString("payload")); + assertEquals( + (Integer) java.net.HttpURLConnection.HTTP_BAD_REQUEST, + error.getInt32("statusCode")); + assertTrue( + "Expected status message to contain 'Message is required.'", + error.getString("statusMessage").contains("Message is required.")); + return null; + }); + + p.run().waitUntilFinish(); + } + + @Test + public void testErrorHandlingWithMissingRequiredField() { + DatadogWriteSchemaTransformProvider provider = new DatadogWriteSchemaTransformProvider(); + ErrorHandling errorHandling = ErrorHandling.builder().setOutput(ERROR).build(); + DatadogWriteSchemaTransformConfiguration configuration = + DatadogWriteSchemaTransformConfiguration.builder() + .setApiKey("test-api-key") + .setUrl("http://localhost:8080") + .setErrorHandling(errorHandling) + .build(); + + Schema missingFieldSchema = + Schema.builder().addStringField("ddsource").addStringField("hostname").build(); + + Row row = + Row.withSchema(missingFieldSchema) + .withFieldValue("ddsource", "my-source") + .withFieldValue("hostname", "my-host") + .build(); + + PCollection input = p.apply(Create.of(row).withRowSchema(missingFieldSchema)); + PCollectionRowTuple inputTuple = PCollectionRowTuple.of(INPUT, input); + + SchemaTransform transform = provider.from(configuration); + PCollectionRowTuple outputTuple = transform.expand(inputTuple); + + assertTrue(outputTuple.has(ERROR)); + PAssert.that(outputTuple.get(ERROR)) + .satisfies( + (errors) -> { + assertEquals(1, errors.spliterator().getExactSizeIfKnown()); + Row error = errors.iterator().next(); + assertEquals(row.toString(), error.getString("payload")); + assertEquals( + (Integer) java.net.HttpURLConnection.HTTP_BAD_REQUEST, + error.getInt32("statusCode")); + assertTrue( + "Expected status message to contain 'Message is required.'", + error.getString("statusMessage").contains("Message is required.")); + return null; + }); + + p.run().waitUntilFinish(); + } + + @Test + public void testConfigurationSchema() throws NoSuchSchemaException { + SchemaRegistry registry = SchemaRegistry.createDefault(); + registry.registerSchemaProvider(ErrorHandling.class, new AutoValueSchema()); + Schema schema = registry.getSchema(DatadogWriteSchemaTransformConfiguration.class); + Schema errorHandlingSchema = registry.getSchema(ErrorHandling.class); + + LOG.info("Schema fields: {}", schema.getFieldNames()); + + assertEquals(7, schema.getFieldCount()); + assertTrue(schema.hasField("url")); + assertTrue(schema.hasField("apiKey")); + assertTrue(schema.hasField("minBatchCount")); + assertTrue(schema.hasField("batchCount")); + assertTrue(schema.hasField("maxBufferSize")); + assertTrue(schema.hasField("parallelism")); + assertTrue(schema.hasField("errorHandling")); + + LOG.info("URL field type: {}", schema.getField("url").getType().getTypeName()); + assertEquals(Schema.FieldType.STRING.withNullable(false), schema.getField("url").getType()); + assertEquals(Schema.FieldType.STRING.withNullable(false), schema.getField("apiKey").getType()); + assertEquals( + Schema.FieldType.INT32.withNullable(true), schema.getField("batchCount").getType()); + assertEquals( + Schema.FieldType.INT64.withNullable(true), schema.getField("maxBufferSize").getType()); + assertEquals( + Schema.FieldType.INT32.withNullable(true), schema.getField("parallelism").getType()); + assertEquals( + Schema.FieldType.row(errorHandlingSchema).withNullable(true), + schema.getField("errorHandling").getType()); + } +} diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 32894b978094..60ef89ed223b 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -96,6 +96,7 @@ dependencies { runtimeOnly project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") runtimeOnly library.java.bigdataoss_util_hadoop + runtimeOnly project(":sdks:java:io:datadog") runtimeOnly project(":sdks:java:io:mongodb") runtimeOnly library.java.kafka_clients runtimeOnly library.java.slf4j_jdk14 diff --git a/sdks/python/apache_beam/yaml/integration_tests.py b/sdks/python/apache_beam/yaml/integration_tests.py index 150c0ca86254..e319a3d3a9bd 100644 --- a/sdks/python/apache_beam/yaml/integration_tests.py +++ b/sdks/python/apache_beam/yaml/integration_tests.py @@ -21,18 +21,52 @@ import copy import glob import itertools +import json import logging import os import random import secrets import sqlite3 import string +import struct import unittest import uuid from datetime import datetime from datetime import timezone import mock + +from apache_beam.coders import Coder +from apache_beam.coders.coder_impl import CoderImpl +from apache_beam.yaml.test_utils.datadog_test_utils import temp_fake_datadog_server + + +class BigEndianIntegerCoderImpl(CoderImpl): + """Coder implementation for big-endian integers used in cross-language tests. + + This is needed because Java's BigEndianIntegerCoder falls back to the generic + 'beam:coders:javasdk:0.1' URN when used in cross-language pipelines, and + Python's FnApiRunner needs to know how to decode it. + """ + def encode_to_stream(self, value, stream, nested): + stream.write(struct.pack('>i', value)) + + def decode_from_stream(self, stream, nested): + return struct.unpack('>i', stream.read(4))[0] + + +class BigEndianIntegerCoder(Coder): + def get_impl(self): + return BigEndianIntegerCoderImpl() + + +# Register the coder with the fallback URN used by the Java SDK for this coder. +# This allows the Python FnApiRunner to handle data sharded by Java transforms +# using BigEndianIntegerCoder in integration tests. +Coder.register_urn( + 'beam:coders:javasdk:0.1', + None, lambda payload, components, context: BigEndianIntegerCoder()) + import psycopg2 import pytds import sqlalchemy diff --git a/sdks/python/apache_beam/yaml/standard_io.yaml b/sdks/python/apache_beam/yaml/standard_io.yaml index 1790dc0f5a9a..520c466b600b 100644 --- a/sdks/python/apache_beam/yaml/standard_io.yaml +++ b/sdks/python/apache_beam/yaml/standard_io.yaml @@ -431,6 +431,27 @@ config: gradle_target: 'sdks:java:io:expansion-service:shadowJar' +#Datadog +- type: renaming + transforms: + 'WriteToDatadog': 'WriteToDatadog' + config: + mappings: + 'WriteToDatadog': + 'url': 'url' + 'api_key': 'api_key' + 'min_batch_count': 'min_batch_count' + 'batch_count': 'batch_count' + 'max_buffer_size': 'max_buffer_size' + 'parallelism': 'parallelism' + 'error_handling': 'error_handling' + underlying_provider: + type: beamJar + transforms: + 'WriteToDatadog': 'beam:schematransform:org.apache.beam:datadog_write:v1' + config: + gradle_target: 'sdks:java:io:expansion-service:shadowJar' + #MongoDB - type: renaming transforms: diff --git a/sdks/python/apache_beam/yaml/test_utils/__init__.py b/sdks/python/apache_beam/yaml/test_utils/__init__.py new file mode 100644 index 000000000000..89aea21adcf0 --- /dev/null +++ b/sdks/python/apache_beam/yaml/test_utils/__init__.py @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Helper utilities for YAML integration tests.""" diff --git a/sdks/python/apache_beam/yaml/test_utils/datadog_test_utils.py b/sdks/python/apache_beam/yaml/test_utils/datadog_test_utils.py new file mode 100644 index 000000000000..4a87a3466689 --- /dev/null +++ b/sdks/python/apache_beam/yaml/test_utils/datadog_test_utils.py @@ -0,0 +1,131 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Helper utilities for Datadog integration tests.""" + +import contextlib +import gzip +import http.server +import io +import json +import logging +import threading + +_LOGGER = logging.getLogger(__name__) + + +class DatadogConnection: + def __init__(self, url, api_key): + self.url = url + self.api_key = api_key + + +class MockDatadogHandler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + if self.path == "/api/v2/logs": + is_chunked = self.headers.get('Transfer-Encoding', + '').lower() == 'chunked' + is_gzip = self.headers.get('Content-Encoding', '').lower() == 'gzip' + content_len = int(self.headers.get('Content-Length', 0)) + + try: + raw_data = b'' + if is_chunked: + while True: + line = self.rfile.readline().strip() + if not line: + break + chunk_len = int(line, 16) + if chunk_len == 0: + self.rfile.readline() # Clear trail + break + raw_data += self.rfile.read(chunk_len) + self.rfile.readline() # Clear trail + elif content_len > 0: + raw_data = self.rfile.read(content_len) + + if raw_data and is_gzip: + with gzip.GzipFile(fileobj=io.BytesIO(raw_data)) as f: + raw_data = f.read() + + if raw_data: + data = json.loads(raw_data) + with self.server.record_lock: + if isinstance(data, list): + self.server.received_records.extend(data) + else: + self.server.received_records.append(data) + except Exception as e: + logging.error("CRITICAL: Failure unpacking mock datadog payload: %s", e) + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(b'{"status": "ok"}') + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + pass + + +@contextlib.contextmanager +def temp_datadog_mock_server(received_records): + server = http.server.ThreadingHTTPServer(('localhost', 0), MockDatadogHandler) + server.received_records = received_records + server.record_lock = threading.Lock() + ip, port = server.server_address + thread = threading.Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + try: + yield f"http://{ip}:{port}" + finally: + server.shutdown() + server.server_close() + thread.join() + + +@contextlib.contextmanager +def temp_fake_datadog_server(expected_records=None): + """Context manager to provide a temporary fake Datadog server for testing. + """ + received = [] + with temp_datadog_mock_server(received) as mock_url: + try: + yield DatadogConnection( + url=mock_url, + api_key="dummy_key_for_testing", + ) + except Exception as err: + logging.error( + "Error interacting with temporary fake Datadog server: %s", err) + raise err + finally: + if expected_records is not None: + + canonicalize = lambda rec: json.dumps(rec, sort_keys=True) + + actual_strs = sorted([canonicalize(r) for r in received]) + expected_strs = sorted([canonicalize(e) for e in expected_records]) + + assert actual_strs == expected_strs, ( + f"Mismatch in recorded Datadog events!\n" + f"Expected: {expected_strs}\n" + f"Actual: {actual_strs}" + ) diff --git a/sdks/python/apache_beam/yaml/tests/datadog.yaml b/sdks/python/apache_beam/yaml/tests/datadog.yaml new file mode 100644 index 000000000000..24485bfbaf4d --- /dev/null +++ b/sdks/python/apache_beam/yaml/tests/datadog.yaml @@ -0,0 +1,63 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +fixtures: + - name: FAKE_DATADOG_SERVER + type: "apache_beam.yaml.test_utils.datadog_test_utils.temp_fake_datadog_server" + config: + expected_records: + - { ddsource: "apache-beam1", ddtags: "test-tags1", hostname: "test-host", service: "test-service", message: "Event for label 11a" } + - { ddsource: "apache-beam2", ddtags: "test-tags2", hostname: "test-host", service: "test-service", message: "Event for label 37a" } + - { ddsource: "apache-beam3", ddtags: "test-tags3", hostname: "test-host", service: "test-service", message: "Event for label 389a" } + - { ddsource: "apache-beam4", ddtags: "test-tags4", hostname: "test-host", service: "test-service", message: "Event for label 3821b" } + +pipelines: + - pipeline: + type: composite + transforms: + - type: Create + config: + elements: + - { ddsource: "apache-beam1", ddtags: "test-tags1", hostname: "test-host", service: "test-service", message: "Event for label 11a" } + - { ddsource: "apache-beam2", ddtags: "test-tags2", hostname: "test-host", service: "test-service", message: "Event for label 37a" } + - { ddsource: "apache-beam3", ddtags: "test-tags3", hostname: "test-host", service: "test-service", message: "Event for label 389a" } + - { ddsource: "apache-beam4", ddtags: "test-tags4", hostname: "test-host", service: "test-service", message: "Event for label 3821b" } + - { ddsource: "apache-beam-broken", hostname: "test-host" } # Triggers mandatory field failure + - type: WriteToDatadog + input: Create + config: + url: "{FAKE_DATADOG_SERVER.url}" + api_key: "{FAKE_DATADOG_SERVER.api_key}" + min_batch_count: 1 + batch_count: 2 + max_buffer_size: 1000 + parallelism: 1 + error_handling: + output: error_output + - type: MapToFields + input: WriteToDatadog.error_output + config: + language: python + fields: + failed_source: "failed_row.ddsource" + - type: AssertEqual + input: MapToFields + config: + elements: + - { failed_source: "apache-beam-broken" } + # Asserting good records is taken care of by the fixture + diff --git a/sdks/standard_external_transforms.yaml b/sdks/standard_external_transforms.yaml index 057c4e3f47d1..b50402a64d54 100644 --- a/sdks/standard_external_transforms.yaml +++ b/sdks/standard_external_transforms.yaml @@ -21,6 +21,41 @@ # # Last updated on: 2026-05-06 +- default_service: sdks:java:io:expansion-service:shadowJar + description: '' + destinations: + python: apache_beam/io + fields: + - description: The Datadog API key. + name: api_key + nullable: false + type: str + - description: The number of events to batch together for each write. + name: batch_count + nullable: true + type: int32 + - description: Specifies how to handle errors. + name: error_handling + nullable: true + type: Row(output=) + - description: The maximum buffer size in bytes. + name: max_buffer_size + nullable: true + type: int64 + - description: The minimum number of events to batch together for each write. + name: min_batch_count + nullable: true + type: int32 + - description: The degree of parallelism for writing. + name: parallelism + nullable: true + type: int32 + - description: The Datadog API URL. + name: url + nullable: false + type: str + identifier: beam:schematransform:org.apache.beam:datadog_write:v1 + name: DatadogWrite - default_service: sdks:java:io:expansion-service:shadowJar description: 'Outputs a PCollection of Beam Rows, each containing a single INT64 number called "value". The count is produced from the given "start" value and From ed261abb992668dd7106b8327350930500a4c68f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:15:13 -0400 Subject: [PATCH 351/490] Bump torch (#38896) Bumps [torch](https://github.com/pytorch/pytorch) from 1.13.1 to 2.12.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v1.13.1...v2.12.0) --- updated-dependencies: - dependency-name: torch dependency-version: 2.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../inference/online_clustering/clustering_pipeline/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/examples/inference/online_clustering/clustering_pipeline/setup.py b/sdks/python/apache_beam/examples/inference/online_clustering/clustering_pipeline/setup.py index 572763492f41..69f6aecf2d5f 100644 --- a/sdks/python/apache_beam/examples/inference/online_clustering/clustering_pipeline/setup.py +++ b/sdks/python/apache_beam/examples/inference/online_clustering/clustering_pipeline/setup.py @@ -29,7 +29,7 @@ REQUIREMENTS = [ "apache-beam[gcp]==2.40.0", "transformers==4.38.0", - "torch==1.13.1", + "torch==2.12.0", "scikit-learn==1.0.2", ] From df386d67842d809a5b28cba277540d60bbcc6298 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 10 Jun 2026 17:37:16 -0400 Subject: [PATCH 352/490] Revert "[Python] Honor disableCounterMetrics, disableStringSetMetrics, and disableBoundedTrieMetrics experiments" (#38901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "[Python] Honor disableCounterMetrics, disableStringSetMetrics, and di…" This reverts commit c01ceeab24c7cae7167dbc08b21529339659094f. * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- CHANGES.md | 1 - sdks/python/apache_beam/metrics/metric.py | 54 +-------- .../python/apache_beam/metrics/metric_test.py | 104 ------------------ sdks/python/apache_beam/pipeline.py | 2 - .../runners/worker/sdk_worker_main.py | 2 - 5 files changed, 3 insertions(+), 160 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ac8215f35494..698d88b01fab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,7 +69,6 @@ ## New Features / Improvements -* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). ## Breaking Changes diff --git a/sdks/python/apache_beam/metrics/metric.py b/sdks/python/apache_beam/metrics/metric.py index a66eed640be6..6e6757be11d9 100644 --- a/sdks/python/apache_beam/metrics/metric.py +++ b/sdks/python/apache_beam/metrics/metric.py @@ -46,13 +46,11 @@ from apache_beam.metrics.metricbase import Histogram from apache_beam.metrics.metricbase import MetricName from apache_beam.metrics.metricbase import StringSet -from apache_beam.options.pipeline_options import DebugOptions if TYPE_CHECKING: from apache_beam.internal.metrics.metric import MetricLogger from apache_beam.metrics.execution import MetricKey from apache_beam.metrics.metricbase import Metric - from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.utils.histogram import BucketType __all__ = ['Metrics', 'MetricsFilter', 'Lineage'] @@ -60,37 +58,6 @@ _LOGGER = logging.getLogger(__name__) -class MetricsFlag(object): - """Process-wide flags controlling which user metric kinds are emitted.""" - counter_disabled = False - string_set_disabled = False - bounded_trie_disabled = False - _initialized = False - - @classmethod - def set_default_pipeline_options(cls, options: 'PipelineOptions') -> None: - if cls._initialized: - return - debug_options = options.view_as(DebugOptions) - if debug_options.lookup_experiment('disableCounterMetrics'): - cls.counter_disabled = True - _LOGGER.info('Counter metrics are disabled.') - if debug_options.lookup_experiment('disableStringSetMetrics'): - cls.string_set_disabled = True - _LOGGER.info('StringSet metrics are disabled.') - if debug_options.lookup_experiment('disableBoundedTrieMetrics'): - cls.bounded_trie_disabled = True - _LOGGER.info('BoundedTrie metrics are disabled.') - cls._initialized = True - - @classmethod - def reset(cls) -> None: - cls.counter_disabled = False - cls.string_set_disabled = False - cls.bounded_trie_disabled = False - cls._initialized = False - - class Metrics(object): """Lets users create/access metric objects during pipeline execution.""" @staticmethod @@ -237,17 +204,12 @@ class DelegatingCounter(Counter): def __init__( self, metric_name: MetricName, process_wide: bool = False) -> None: super().__init__(metric_name) - self._updater = MetricUpdater( + self.inc = MetricUpdater( # type: ignore[method-assign] cells.CounterCell, metric_name, default_value=1, process_wide=process_wide) - def inc(self, n: int = 1) -> None: - if MetricsFlag.counter_disabled: - return - self._updater(n) - class DelegatingDistribution(Distribution): """Metrics Distribution Delegates functionality to MetricsEnvironment.""" def __init__( @@ -269,23 +231,13 @@ class DelegatingStringSet(StringSet): """Metrics StringSet that Delegates functionality to MetricsEnvironment.""" def __init__(self, metric_name: MetricName) -> None: super().__init__(metric_name) - self._updater = MetricUpdater(cells.StringSetCell, metric_name) - - def add(self, value: str) -> None: - if MetricsFlag.string_set_disabled: - return - self._updater(value) + self.add = MetricUpdater(cells.StringSetCell, metric_name) # type: ignore[method-assign] class DelegatingBoundedTrie(BoundedTrie): """Metrics BoundedTrie that Delegates functionality to MetricsEnvironment.""" def __init__(self, metric_name: MetricName) -> None: super().__init__(metric_name) - self._updater = MetricUpdater(cells.BoundedTrieCell, metric_name) - - def add(self, value) -> None: - if MetricsFlag.bounded_trie_disabled: - return - self._updater(value) + self.add = MetricUpdater(cells.BoundedTrieCell, metric_name) # type: ignore[method-assign] class MetricResults(object): diff --git a/sdks/python/apache_beam/metrics/metric_test.py b/sdks/python/apache_beam/metrics/metric_test.py index 6937236a8aad..ae66200737b5 100644 --- a/sdks/python/apache_beam/metrics/metric_test.py +++ b/sdks/python/apache_beam/metrics/metric_test.py @@ -32,9 +32,7 @@ from apache_beam.metrics.metric import MetricResults from apache_beam.metrics.metric import Metrics from apache_beam.metrics.metric import MetricsFilter -from apache_beam.metrics.metric import MetricsFlag from apache_beam.metrics.metricbase import MetricName -from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.runners.direct.direct_runner import BundleBasedDirectRunner from apache_beam.runners.worker import statesampler from apache_beam.testing.metric_result_matchers import DistributionMatcher @@ -123,108 +121,6 @@ def test_get_namespace_error(self): with self.assertRaises(ValueError): Metrics.get_namespace(object()) - def test_metrics_flag(self): - MetricsFlag.reset() - try: - self.assertFalse(MetricsFlag.counter_disabled) - self.assertFalse(MetricsFlag.string_set_disabled) - self.assertFalse(MetricsFlag.bounded_trie_disabled) - - options = PipelineOptions(['--experiments=disableCounterMetrics']) - MetricsFlag.set_default_pipeline_options(options) - self.assertTrue(MetricsFlag.counter_disabled) - self.assertFalse(MetricsFlag.string_set_disabled) - self.assertFalse(MetricsFlag.bounded_trie_disabled) - - MetricsFlag.reset() - options = PipelineOptions(['--experiments=disableStringSetMetrics']) - MetricsFlag.set_default_pipeline_options(options) - self.assertFalse(MetricsFlag.counter_disabled) - self.assertTrue(MetricsFlag.string_set_disabled) - self.assertFalse(MetricsFlag.bounded_trie_disabled) - - MetricsFlag.reset() - options = PipelineOptions(['--experiments=disableBoundedTrieMetrics']) - MetricsFlag.set_default_pipeline_options(options) - self.assertFalse(MetricsFlag.counter_disabled) - self.assertFalse(MetricsFlag.string_set_disabled) - self.assertTrue(MetricsFlag.bounded_trie_disabled) - - MetricsFlag.reset() - options = PipelineOptions([ - '--experiments=disableCounterMetrics', - '--experiments=disableStringSetMetrics', - '--experiments=disableBoundedTrieMetrics', - ]) - MetricsFlag.set_default_pipeline_options(options) - self.assertTrue(MetricsFlag.counter_disabled) - self.assertTrue(MetricsFlag.string_set_disabled) - self.assertTrue(MetricsFlag.bounded_trie_disabled) - finally: - MetricsFlag.reset() - - def test_disabled_counter_is_noop(self): - sampler = statesampler.StateSampler('', counters.CounterFactory()) - statesampler.set_current_tracker(sampler) - state = sampler.scoped_state( - 'mystep', 'myState', metrics_container=MetricsContainer('mystep')) - MetricsFlag.reset() - try: - sampler.start() - with state: - container = MetricsEnvironment.current_container() - Metrics.counter('ns', 'baseline').inc() - self.assertEqual(len(container.metrics), 1) - options = PipelineOptions(['--experiments=disableCounterMetrics']) - MetricsFlag.set_default_pipeline_options(options) - Metrics.counter('ns', 'after_disable').inc() - Metrics.counter('ns', 'after_disable').inc(5) - Metrics.counter('ns', 'after_disable').dec() - self.assertEqual(len(container.metrics), 1) - finally: - sampler.stop() - MetricsFlag.reset() - - def test_disabled_string_set_is_noop(self): - sampler = statesampler.StateSampler('', counters.CounterFactory()) - statesampler.set_current_tracker(sampler) - state = sampler.scoped_state( - 'mystep', 'myState', metrics_container=MetricsContainer('mystep')) - MetricsFlag.reset() - try: - sampler.start() - with state: - container = MetricsEnvironment.current_container() - Metrics.string_set('ns', 'baseline').add('seed') - self.assertEqual(len(container.metrics), 1) - options = PipelineOptions(['--experiments=disableStringSetMetrics']) - MetricsFlag.set_default_pipeline_options(options) - Metrics.string_set('ns', 'after_disable').add('value') - self.assertEqual(len(container.metrics), 1) - finally: - sampler.stop() - MetricsFlag.reset() - - def test_disabled_bounded_trie_is_noop(self): - sampler = statesampler.StateSampler('', counters.CounterFactory()) - statesampler.set_current_tracker(sampler) - state = sampler.scoped_state( - 'mystep', 'myState', metrics_container=MetricsContainer('mystep')) - MetricsFlag.reset() - try: - sampler.start() - with state: - container = MetricsEnvironment.current_container() - Metrics.bounded_trie('ns', 'baseline').add(['a']) - self.assertEqual(len(container.metrics), 1) - options = PipelineOptions(['--experiments=disableBoundedTrieMetrics']) - MetricsFlag.set_default_pipeline_options(options) - Metrics.bounded_trie('ns', 'after_disable').add(['a', 'b']) - self.assertEqual(len(container.metrics), 1) - finally: - sampler.stop() - MetricsFlag.reset() - def test_counter_empty_name(self): with self.assertRaises(ValueError): Metrics.counter("namespace", "") diff --git a/sdks/python/apache_beam/pipeline.py b/sdks/python/apache_beam/pipeline.py index 594660d9bea9..750868f7443a 100644 --- a/sdks/python/apache_beam/pipeline.py +++ b/sdks/python/apache_beam/pipeline.py @@ -73,7 +73,6 @@ from apache_beam.coders import typecoders from apache_beam.internal import pickler from apache_beam.io.filesystems import FileSystems -from apache_beam.metrics.metric import MetricsFlag from apache_beam.options.pipeline_options import CrossLanguageOptions from apache_beam.options.pipeline_options import DebugOptions from apache_beam.options.pipeline_options import PipelineOptions @@ -193,7 +192,6 @@ def __init__( self._options = PipelineOptions([]) FileSystems.set_options(self._options) - MetricsFlag.set_default_pipeline_options(self._options) if runner is None: runner = self._options.view_as(StandardOptions).runner diff --git a/sdks/python/apache_beam/runners/worker/sdk_worker_main.py b/sdks/python/apache_beam/runners/worker/sdk_worker_main.py index 58beda96d63d..754a631eaf33 100644 --- a/sdks/python/apache_beam/runners/worker/sdk_worker_main.py +++ b/sdks/python/apache_beam/runners/worker/sdk_worker_main.py @@ -33,7 +33,6 @@ from apache_beam.internal import pickler from apache_beam.io import filesystems -from apache_beam.metrics import metric from apache_beam.options.pipeline_options import DebugOptions from apache_beam.options.pipeline_options import GoogleCloudOptions from apache_beam.options.pipeline_options import PipelineOptions @@ -124,7 +123,6 @@ def create_harness(environment, dry_run=False): RuntimeValueProvider.set_runtime_options(pipeline_options_dict) sdk_pipeline_options = PipelineOptions.from_dictionary(pipeline_options_dict) filesystems.FileSystems.set_options(sdk_pipeline_options) - metric.MetricsFlag.set_default_pipeline_options(sdk_pipeline_options) pickle_library = sdk_pipeline_options.view_as(SetupOptions).pickle_library pickler.set_library(pickle_library) From 991eb654a1956e99f54fee8ee5251cf42c0c142d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:06:03 -0400 Subject: [PATCH 353/490] Bump torch (#38897) Bumps [torch](https://github.com/pytorch/pytorch) from 1.13.1 to 2.12.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v1.13.1...v2.12.0) --- updated-dependencies: - dependency-name: torch dependency-version: 2.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../anomaly_detection/anomaly_detection_pipeline/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/examples/inference/anomaly_detection/anomaly_detection_pipeline/setup.py b/sdks/python/apache_beam/examples/inference/anomaly_detection/anomaly_detection_pipeline/setup.py index a415648cdf99..ccc2f97b0486 100644 --- a/sdks/python/apache_beam/examples/inference/anomaly_detection/anomaly_detection_pipeline/setup.py +++ b/sdks/python/apache_beam/examples/inference/anomaly_detection/anomaly_detection_pipeline/setup.py @@ -33,7 +33,7 @@ "hdbscan==0.8.28", "scikit-learn==1.7.1", "transformers==4.36.0", - "torch==1.13.1", + "torch==2.12.0", "pandas==1.3.5", "yagmail==0.15.283", ] From 539a3691c7e5da22cc4a73df2b9e65adcafe121a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:06:55 -0400 Subject: [PATCH 354/490] Bump torch (#38903) Bumps [torch](https://github.com/pytorch/pytorch) from 2.2.0 to 2.12.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.2.0...v2.12.0) --- updated-dependencies: - dependency-name: torch dependency-version: 2.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ml-orchestration/kfp/components/train/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt index ba1103dd1ef9..5b996fdfc70a 100644 --- a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt @@ -13,6 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -torch==2.2.0 +torch==2.12.0 numpy==1.22.4 Pillow==10.2.0 \ No newline at end of file From c7d3db83b8828fedccc4fa71769e6b330aabb837 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 11 Jun 2026 02:09:03 +0200 Subject: [PATCH 355/490] Fix Xlang IO Direct installGcpTest uv cache lock (#38862) * Fix Xlang IO Direct installGcpTest uv cache lock * use with per env cache dir * Added quote paths * test workflows --- .github/trigger_files/beam_PostCommit_Python.json | 2 +- .../trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json | 2 +- .../groovy/org/apache/beam/gradle/BeamModulePlugin.groovy | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 213813294758..def2ac984082 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "38069", - "modification": 48 + "modification": 49 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json index c537844dc84a..455144f02a35 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 3 + "modification": 6 } diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 0db2d1b27ab3..abeede24709a 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -3141,9 +3141,11 @@ class BeamModulePlugin implements Plugin { if (extra) { installTargets = "${distTarBall}[${packages},${extra}]" } + def uvCacheDir = "${project.ext.envdir}/.uv_cache" project.exec { executable 'sh' - args '-c', ". ${project.ext.envdir}/bin/activate && pip install uv && uv pip install --pre ${installTargets}" + // Default uv cache is global; py310/py314 installGcpTest fight over the same lock. + args '-c', ". \"${project.ext.envdir}/bin/activate\" && pip install uv && uv pip install --cache-dir \"${uvCacheDir}\" --pre \"${installTargets}\"" } } } From ad620368b7e9606dca50615eb09887b098b24c7f Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 10 Jun 2026 20:50:14 -0400 Subject: [PATCH 356/490] Turn to manual build for Go for CodeQL (#38907) * Turn to manual build for Go * update build sequence commands --- .github/workflows/codeql.yml | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 95d128d53658..ce9363b3d669 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -55,7 +55,7 @@ jobs: # - language: c-cpp # build-mode: autobuild - language: go - build-mode: autobuild + build-mode: manual - language: java-kotlin build-mode: autobuild - language: javascript-typescript @@ -76,6 +76,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Set up Go + if: matrix.language == 'go' + uses: actions/setup-go@v6 + with: + go-version-file: 'sdks/go.mod' + # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` # or others). This is typically only required for manual builds. @@ -105,12 +111,22 @@ jobs: if: matrix.build-mode == 'manual' shell: bash run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + if [ "${{ matrix.language }}" = "go" ]; then + echo "Building sdks module..." + (cd sdks && go build ./...) + + echo "Building playground/backend module..." + (cd playground/backend && go build ./...) + + echo "Building learning/katas/go module..." + (cd learning/katas/go && go mod tidy && go build ./...) + + echo "Building learning/tour-of-beam/backend module..." + (cd learning/tour-of-beam/backend && go build ./cmd/... ./internal/... ./playground_api/...) + else + echo "No manual build steps defined for ${{ matrix.language }}" + exit 1 + fi - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 From 67038c67fc651782c852fda56446cfb3e69338cf Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 11 Jun 2026 04:46:24 -0400 Subject: [PATCH 357/490] Fix Dataflow legacy worker abort loop thread death issue (#38894) Previously, when the service asked the worker to abort, it threw ReadLoopAbortedException, which extends InterruptedException. MapTaskExecutor caught this and, in an attempt to preserve the interrupted status, set the interrupted bit on the thread. However, since this was a logical abort and not a real thread interrupt, setting the interrupted bit caused subsequent operations on the thread (like the backoff sleep in DataflowBatchWorkerHarness) to immediately fail with InterruptedException, leading to all worker threads dying and the harness hanging. This fix changes the interruption handling in MapTaskExecutor to not set the interrupted bit if it is just rethrowing the InterruptedException. Since we are throwing the exception, we can rely on the caller to set the bit if they swallow it and need to preserve it. Ref: b/512366613 --- .../dataflow/worker/util/common/worker/MapTaskExecutor.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java index 3c33e1904069..c6e1ae209b98 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/MapTaskExecutor.java @@ -100,9 +100,6 @@ public void execute() throws Exception { } catch (Exception closeExn) { exn.addSuppressed(closeExn); } - if (exn instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } throw exn; } } From 76083e2f1f2bc35f2cc076e9cd20d73b924e4185 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:53:17 -0400 Subject: [PATCH 358/490] Bump cloud.google.com/go/bigtable from 1.48.0 to 1.49.0 in /sdks (#38913) Bumps [cloud.google.com/go/bigtable](https://github.com/googleapis/google-cloud-go) from 1.48.0 to 1.49.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/documentai/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.48.0...pubsub/v1.49.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/bigtable dependency-version: 1.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 18 +++++++++--------- sdks/go.sum | 42 ++++++++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index bd415ede1b52..899db12bb968 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -26,7 +26,7 @@ toolchain go1.26.2 require ( cloud.google.com/go/bigquery v1.77.0 - cloud.google.com/go/bigtable v1.48.0 + cloud.google.com/go/bigtable v1.49.0 cloud.google.com/go/datastore v1.24.0 cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 @@ -87,8 +87,8 @@ require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect @@ -128,11 +128,11 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 // indirect golang.org/x/time v0.15.0 // indirect @@ -203,6 +203,6 @@ require ( golang.org/x/mod v0.36.0 // indirect golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index a0db92fd7ec8..61ca73ba7ce2 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -46,8 +46,8 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.77.0 h1:L5AW3jhzEKpFVg4i0mVHxKpxogrqT7dczWBSr4m9MKU= cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= -cloud.google.com/go/bigtable v1.48.0 h1:+K6difi14hvfuh+19tvWVkvD6zYjN5mc3rJ1+q8ETyw= -cloud.google.com/go/bigtable v1.48.0/go.mod h1:6TjVhBmzk7N01MZwjxn/YsSnlaw96AYzsFDYhfyBqDs= +cloud.google.com/go/bigtable v1.49.0 h1:kdA1jbO8EJIMI752zD50o5z6Wu82cvCwIibhrpJK0tI= +cloud.google.com/go/bigtable v1.49.0/go.mod h1:RTannV5mvoJM8KscLTfRYMPo84u9/j+C3PSyYJGf5Ic= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= @@ -165,12 +165,12 @@ github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0 h1:ZIT85vKP7LBS84XJ0WdJ3dPOX3iz4j3c0+lpajGQMyo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0/go.mod h1:rqP9UEhOXv9WhQ7Gjz+G5y/pf8+BJZW5/Ts0AhE0PwE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -867,18 +867,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k= @@ -1421,8 +1423,8 @@ google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2I google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 h1:cTHF8xtqtBN5sQ4dcoNwOS6FFejvFTkWQbZXsTU3trM= google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc= -google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw= -google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= From ff8017555bda8eb875a3141ff9a18f3c57f8bb04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:53:55 -0400 Subject: [PATCH 359/490] Bump github.com/aws/aws-sdk-go-v2/config in /sdks (#38916) Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.32.24 to 1.32.25. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.32.24...config/v1.32.25) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.25 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 6 +++--- sdks/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 899db12bb968..f7ab91a66819 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -33,8 +33,8 @@ require ( cloud.google.com/go/spanner v1.91.0 cloud.google.com/go/storage v1.62.3 github.com/aws/aws-sdk-go-v2 v1.42.0 - github.com/aws/aws-sdk-go-v2/config v1.32.24 - github.com/aws/aws-sdk-go-v2/credentials v1.19.23 + github.com/aws/aws-sdk-go-v2/config v1.32.25 + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 github.com/aws/smithy-go v1.27.2 @@ -91,7 +91,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 61ca73ba7ce2..1b69218f8ca9 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -207,12 +207,12 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7z github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs= -github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= -github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= @@ -256,8 +256,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU66 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= From c14503cb5f02ef8bec807c183f0c64cd739a6f3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:17:23 -0400 Subject: [PATCH 360/490] Bump golang.org/x/net from 0.55.0 to 0.56.0 in /sdks (#38915) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0. - [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index f7ab91a66819..8817a26cd1f6 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -55,7 +55,7 @@ require ( github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20241021075129-b732d2ac9c9b go.mongodb.org/mongo-driver v1.17.9 - golang.org/x/net v0.55.0 + golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.21.0 golang.org/x/sys v0.46.0 @@ -199,7 +199,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 1b69218f8ca9..7ed4b94c6337 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -925,8 +925,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1036,8 +1036,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1172,8 +1172,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 5fde991f47f5bea33c25d6c3323c1bdf61ab20b3 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:23:01 -0400 Subject: [PATCH 361/490] [IcebergIO] Improve TableCache (#38882) * improve table cache * address comments * cleanups --- .../IO_Iceberg_Integration_Tests.json | 2 +- .../AssignDestinationsAndPartitions.java | 5 +- .../sdk/io/iceberg/CreateReadTasksDoFn.java | 7 +- .../apache/beam/sdk/io/iceberg/IcebergIO.java | 2 +- .../sdk/io/iceberg/IcebergScanConfig.java | 3 +- .../sdk/io/iceberg/IncrementalScanSource.java | 6 +- .../beam/sdk/io/iceberg/ReadFromTasks.java | 7 +- .../sdk/io/iceberg/RecordWriterManager.java | 140 +++++-------- .../beam/sdk/io/iceberg/ScanSource.java | 3 +- .../beam/sdk/io/iceberg/TableCache.java | 198 +++++++++++++----- .../sdk/io/iceberg/WatchForSnapshots.java | 7 +- .../io/iceberg/WriteDirectRowsToFiles.java | 12 +- .../io/iceberg/WriteGroupedRowsToFiles.java | 12 +- .../iceberg/WritePartitionedRowsToFiles.java | 166 ++++++--------- .../io/iceberg/WriteUngroupedRowsToFiles.java | 12 +- .../io/iceberg/RecordWriterManagerTest.java | 121 ++++++----- .../beam/sdk/io/iceberg/TableCacheTest.java | 126 +++++++++++ 17 files changed, 479 insertions(+), 350 deletions(-) create mode 100644 sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TableCacheTest.java diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index 7ab7bcd9a9c6..b73af5e61a43 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 2 + "modification": 1 } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java index e5d70d85d875..99cd07b23c8e 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java @@ -147,7 +147,10 @@ public void processElement( try { // see if table already exists with a spec - spec = catalogConfig.catalog().loadTable(TableIdentifier.parse(tableIdentifier)).spec(); + spec = + TableCache.getAndRefreshIfStale( + catalogConfig, TableIdentifier.parse(tableIdentifier)) + .spec(); } catch (NoSuchTableException ignored) { // no partition to apply diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CreateReadTasksDoFn.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CreateReadTasksDoFn.java index a40e0e13f78a..d9781859a889 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CreateReadTasksDoFn.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CreateReadTasksDoFn.java @@ -52,18 +52,13 @@ class CreateReadTasksDoFn this.scanConfig = scanConfig; } - @Setup - public void setup() { - TableCache.setup(scanConfig); - } - @ProcessElement public void process( @Element KV> element, OutputReceiver> out) throws IOException, ExecutionException { // force refresh because the table must be updated before scanning snapshots - Table table = TableCache.getRefreshed(element.getKey()); + Table table = TableCache.getRefreshed(scanConfig.getCatalogConfig(), element.getKey()); // scan snapshots individually and assign commit timestamp to files for (SnapshotInfo snapshot : element.getValue()) { diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java index a5a3beef8f51..5c5f934ea205 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java @@ -655,7 +655,7 @@ public PCollection expand(PBegin input) { TableIdentifier tableId = checkStateNotNull(getTableIdentifier(), "Must set a table to read from."); - Table table = getCatalogConfig().catalog().loadTable(tableId); + Table table = TableCache.get(getCatalogConfig(), tableId); IcebergScanConfig scanConfig = IcebergScanConfig.builder() diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java index 45ecc7cf71c3..a942c9804c93 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java @@ -68,8 +68,7 @@ public enum ScanType { @Pure public Table getTable() { if (cachedTable == null) { - cachedTable = - getCatalogConfig().catalog().loadTable(TableIdentifier.parse(getTableIdentifier())); + cachedTable = TableCache.get(getCatalogConfig(), TableIdentifier.parse(getTableIdentifier())); } return cachedTable; } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java index 4df3eecb18e5..58cc8f50e0bc 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java @@ -53,10 +53,8 @@ class IncrementalScanSource extends PTransform> { @Override public PCollection expand(PBegin input) { Table table = - scanConfig - .getCatalogConfig() - .catalog() - .loadTable(TableIdentifier.parse(scanConfig.getTableIdentifier())); + TableCache.get( + scanConfig.getCatalogConfig(), TableIdentifier.parse(scanConfig.getTableIdentifier())); PCollection>> snapshots = MoreObjects.firstNonNull(scanConfig.getStreaming(), false) diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java index 528b89c203bf..5eeeacda48e3 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java @@ -51,11 +51,6 @@ class ReadFromTasks extends DoFn, Row> { this.scanConfig = scanConfig; } - @Setup - public void setup() { - TableCache.setup(scanConfig); - } - @ProcessElement public void process( @Element KV element, @@ -63,7 +58,7 @@ public void process( OutputReceiver out) throws IOException, ExecutionException, InterruptedException { ReadTask readTask = element.getValue(); - Table table = TableCache.get(scanConfig.getTableIdentifier()); + Table table = TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); List fileScanTasks = readTask.getFileScanTasks(); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java index 2f532a08754c..1e25e6b9c234 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java @@ -21,8 +21,6 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.io.IOException; -import java.time.Duration; -import java.time.Instant; import java.time.LocalDateTime; import java.time.YearMonth; import java.time.ZoneOffset; @@ -248,7 +246,7 @@ static String getPartitionDataPath( DateTimeFormatter.ofPattern("yyyy-MM-dd-HH"); private static final LocalDateTime EPOCH = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); - private final Catalog catalog; + private final IcebergCatalogConfig catalogConfig; private final String filePrefix; private final long maxFileSize; private final int maxNumWriters; @@ -260,46 +258,11 @@ static String getPartitionDataPath( private final Map, List> totalSerializableDataFiles = Maps.newHashMap(); - static final class LastRefreshedTable { - final Table table; - volatile Instant lastRefreshTime; - static final Duration STALENESS_THRESHOLD = Duration.ofMinutes(2); - - LastRefreshedTable(Table table, Instant lastRefreshTime) { - this.table = table; - this.lastRefreshTime = lastRefreshTime; - } - - /** - * Refreshes the table metadata if it is considered stale (older than 2 minutes). - * - *

      This method first performs a non-synchronized check on the table's freshness. This - * provides a lock-free fast path that avoids synchronization overhead in the common case where - * the table does not need to be refreshed. If the table might be stale, it then enters a - * synchronized block to ensure that only one thread performs the refresh operation. - */ - void refreshIfStale() { - // Fast path: Avoid entering the synchronized block if the table is not stale. - if (lastRefreshTime.isAfter(Instant.now().minus(STALENESS_THRESHOLD))) { - return; - } - synchronized (this) { - if (lastRefreshTime.isBefore(Instant.now().minus(STALENESS_THRESHOLD))) { - table.refresh(); - lastRefreshTime = Instant.now(); - } - } - } - } - - @VisibleForTesting - static final Cache LAST_REFRESHED_TABLE_CACHE = - CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(); - private boolean isClosed = false; - RecordWriterManager(Catalog catalog, String filePrefix, long maxFileSize, int maxNumWriters) { - this.catalog = catalog; + RecordWriterManager( + IcebergCatalogConfig catalogConfig, String filePrefix, long maxFileSize, int maxNumWriters) { + this.catalogConfig = catalogConfig; this.filePrefix = filePrefix; this.maxFileSize = maxFileSize; this.maxNumWriters = maxNumWriters; @@ -308,9 +271,9 @@ void refreshIfStale() { /** * Returns an Iceberg {@link Table}. * - *

      First attempts to fetch the table from the {@link #LAST_REFRESHED_TABLE_CACHE}. If it's not - * there, we attempt to load it using the Iceberg API. If the table doesn't exist at all, we - * attempt to create it, inferring the table schema from the record schema. + *

      First attempts to fetch the table from the shared {@link TableCache}. If it's not there, we + * attempt to load it using the Iceberg API. If the table doesn't exist at all, we attempt to + * create it, inferring the table schema from the record schema. * *

      Note that this is a best-effort operation that depends on the {@link Catalog} * implementation. Although it is expected, some implementations may not support creating a table @@ -319,13 +282,13 @@ void refreshIfStale() { @VisibleForTesting Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) { TableIdentifier identifier = destination.getTableIdentifier(); - @Nullable - LastRefreshedTable lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier); - if (lastRefreshedTable != null && lastRefreshedTable.table != null) { - lastRefreshedTable.refreshIfStale(); - return lastRefreshedTable.table; - } + return TableCache.getAndRefreshIfStale( + catalogConfig, identifier, () -> loadOrCreateTable(destination, dataSchema)); + } + private Table loadOrCreateTable(IcebergDestination destination, Schema dataSchema) { + Catalog catalog = catalogConfig.catalog(); + TableIdentifier identifier = destination.getTableIdentifier(); Namespace namespace = identifier.namespace(); @Nullable IcebergTableCreateConfig createConfig = destination.getTableCreateConfig(); PartitionSpec partitionSpec = @@ -336,53 +299,48 @@ Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) { ? createConfig.getTableProperties() : Maps.newHashMap(); - @Nullable Table table = null; - synchronized (LAST_REFRESHED_TABLE_CACHE) { - // Create namespace if it does not exist yet - if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { - SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; - if (!supportsNamespaces.namespaceExists(namespace)) { - try { - supportsNamespaces.createNamespace(namespace); - LOG.info("Created new namespace '{}'.", namespace); - } catch (AlreadyExistsException ignored) { - // race condition: another worker already created this namespace - } + // Create namespace if it does not exist yet + if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { + SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; + if (!supportsNamespaces.namespaceExists(namespace)) { + try { + supportsNamespaces.createNamespace(namespace); + LOG.info("Created new namespace '{}'.", namespace); + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this namespace } } + } - // If table exists, just load it - // Note: the implementation of catalog.tableExists() will load the table to check its - // existence. We don't use it here to avoid double loadTable() calls. + // If table exists, just load it + // Note: the implementation of catalog.tableExists() will load the table to check its + // existence. We don't use it here to avoid double loadTable() calls. + try { + return catalog.loadTable(identifier); + } catch (NoSuchTableException e) { // Otherwise, create the table + org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema); try { - table = catalog.loadTable(identifier); - } catch (NoSuchTableException e) { // Otherwise, create the table - org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema); - try { - table = - catalog - .buildTable(identifier, tableSchema) - .withPartitionSpec(partitionSpec) - .withSortOrder(sortOrder) - .withProperties(tableProperties) - .create(); - LOG.info( - "Created Iceberg table '{}' with schema: {}\n" - + ", partition spec: {}, sort order: {}, table properties: {}", - identifier, - tableSchema, - partitionSpec, - sortOrder, - tableProperties); - } catch (AlreadyExistsException ignored) { - // race condition: another worker already created this table - table = catalog.loadTable(identifier); - } + Table table = + catalog + .buildTable(identifier, tableSchema) + .withPartitionSpec(partitionSpec) + .withSortOrder(sortOrder) + .withProperties(tableProperties) + .create(); + LOG.info( + "Created Iceberg table '{}' with schema: {}\n" + + ", partition spec: {}, sort order: {}, table properties: {}", + identifier, + tableSchema, + partitionSpec, + sortOrder, + tableProperties); + return table; + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this table + return catalog.loadTable(identifier); } } - lastRefreshedTable = new LastRefreshedTable(table, Instant.now()); - LAST_REFRESHED_TABLE_CACHE.put(identifier, lastRefreshedTable); - return table; } /** diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java index 19218b85b63c..c407ef8d3e2d 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java @@ -47,7 +47,8 @@ public ScanSource(IcebergScanConfig scanConfig) { } private TableScan getTableScan() { - Table table = scanConfig.getTable(); + Table table = + TableCache.getRefreshed(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); TableScan tableScan = table.newScan().project(scanConfig.getProjectedSchema()); if (scanConfig.getFilter() != null) { diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java index cb00d90f7fb3..e95a6a5f66fd 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java @@ -17,70 +17,170 @@ */ package org.apache.beam.sdk.io.iceberg; -import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheLoader; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.LoadingCache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Futures; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ListenableFuture; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.UncheckedExecutionException; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; +import org.checkerframework.checker.nullness.qual.Nullable; -/** Utility to fetch and cache Iceberg {@link Table}s. */ +/** + * Process-wide cache for Iceberg {@link Table}s. + * + *

      Entries are keyed by catalog configuration and table identifier, so one machine can share + * table metadata across source and sink threads without colliding when different catalogs contain + * the same identifier. The underlying catalog is only resolved from {@link IcebergCatalogConfig} + * when the table has to be loaded. Refreshes are synchronized per table entry: if another thread + * refreshed after a caller started its request, the caller reuses that refresh instead of making + * another catalog call. + */ class TableCache { - private static final Map CATALOG_CACHE = new ConcurrentHashMap<>(); - private static final LoadingCache INTERNAL_CACHE = - CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.HOURS) - .refreshAfterWrite(3, TimeUnit.MINUTES) - .build( - new CacheLoader() { - @Override - public Table load(String identifier) { - return checkStateNotNull(CATALOG_CACHE.get(identifier)) - .catalog() - .loadTable(TableIdentifier.parse(identifier)); - } - - @Override - public ListenableFuture reload(String unusedIdentifier, Table table) { - table.refresh(); - return Futures.immediateFuture(table); - } - });; - - static Table get(String identifier) { + static final Duration DEFAULT_REFRESH_INTERVAL = Duration.ofMinutes(2); + + private static final Cache TABLES = + CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).build(); + + /** Returns the cached table, loading it from the catalog on a cache miss. */ + static Table get(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + return get(catalogConfig, identifier, () -> catalogConfig.catalog().loadTable(identifier)); + } + + /** Returns the cached table for a string identifier, loading it on a cache miss. */ + static Table get(IcebergCatalogConfig catalogConfig, String identifier) { + return get(catalogConfig, TableIdentifier.parse(identifier)); + } + + /** Returns the cached table, using the given loader only on a cache miss. */ + static Table get( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier, Callable
      loader) { + return getEntry(catalogConfig, identifier, loader).table; + } + + /** Returns the cached table after forcing a refresh of any pre-existing cache entry. */ + static Table getRefreshed(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + Instant refreshRequestTime = Instant.now(); + CachedTable cachedTable = + getEntry(catalogConfig, identifier, () -> catalogConfig.catalog().loadTable(identifier)); + cachedTable.refreshIfOlderThan(refreshRequestTime); + return cachedTable.table; + } + + /** Returns the cached table for a string identifier after refreshing any pre-existing entry. */ + static Table getRefreshed(IcebergCatalogConfig catalogConfig, String identifier) { + return getRefreshed(catalogConfig, TableIdentifier.parse(identifier)); + } + + /** + * Returns the cached table, refreshing it only if it is older than {@link + * #DEFAULT_REFRESH_INTERVAL}. + */ + static Table getAndRefreshIfStale( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + return getAndRefreshIfStale( + catalogConfig, identifier, () -> catalogConfig.catalog().loadTable(identifier)); + } + + /** Returns the cached table, using the loader on a miss and refreshing stale entries. */ + static Table getAndRefreshIfStale( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier, Callable
      loader) { + CachedTable cachedTable = getEntry(catalogConfig, identifier, loader); + cachedTable.refreshIfOlderThan(Instant.now().minus(DEFAULT_REFRESH_INTERVAL)); + return cachedTable.table; + } + + private static CachedTable getEntry( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier, Callable
      loader) { + CacheKey key = new CacheKey(catalogConfig, identifier); try { - return INTERNAL_CACHE.get(identifier); - } catch (ExecutionException e) { + return TABLES.get(key, () -> new CachedTable(loader.call(), Instant.now())); + } catch (ExecutionException | UncheckedExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } throw new RuntimeException( "Encountered a problem fetching table " + identifier + " from cache.", e); } } - /** Forces a table refresh and returns. */ - static Table getRefreshed(String identifier) { - INTERNAL_CACHE.refresh(identifier); - return get(identifier); + @VisibleForTesting + static long size() { + return TABLES.size(); } - static void setup(IcebergScanConfig scanConfig) { - String tableIdentifier = scanConfig.getTableIdentifier(); - IcebergCatalogConfig catalogConfig = scanConfig.getCatalogConfig(); - if (CATALOG_CACHE.containsKey(tableIdentifier)) { - checkState( - catalogConfig.equals(CATALOG_CACHE.get(tableIdentifier)), - "TableCache is already set up with a different catalog. " + "Existing: %s, new: %s.", - CATALOG_CACHE.get(tableIdentifier), - catalogConfig); - } else { - CATALOG_CACHE.put(scanConfig.getTableIdentifier(), scanConfig.getCatalogConfig()); + @VisibleForTesting + static void invalidateAll() { + TABLES.invalidateAll(); + } + + @VisibleForTesting + static void put( + IcebergCatalogConfig catalogConfig, + TableIdentifier identifier, + Table table, + Instant lastRefreshTime) { + TABLES.put(new CacheKey(catalogConfig, identifier), new CachedTable(table, lastRefreshTime)); + } + + @VisibleForTesting + static void markStale(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + CachedTable cachedTable = TABLES.getIfPresent(new CacheKey(catalogConfig, identifier)); + if (cachedTable != null) { + cachedTable.lastRefreshTime = Instant.EPOCH; + } + } + + private static class CachedTable { + private final Table table; + private volatile Instant lastRefreshTime; + + private CachedTable(Table table, Instant lastRefreshTime) { + this.table = table; + this.lastRefreshTime = lastRefreshTime; + } + + private void refreshIfOlderThan(Instant refreshRequestTime) { + if (lastRefreshTime.isAfter(refreshRequestTime)) { + return; + } + synchronized (this) { + if (lastRefreshTime.isBefore(refreshRequestTime)) { + table.refresh(); + lastRefreshTime = Instant.now(); + } + } + } + } + + private static class CacheKey { + private final IcebergCatalogConfig catalogConfig; + private final TableIdentifier identifier; + + private CacheKey(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + this.catalogConfig = catalogConfig; + this.identifier = identifier; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CacheKey)) { + return false; + } + CacheKey other = (CacheKey) obj; + return catalogConfig.equals(other.catalogConfig) && identifier.equals(other.identifier); + } + + @Override + public int hashCode() { + return 31 * catalogConfig.hashCode() + identifier.hashCode(); } } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java index 1af5588c2519..8bd436c55700 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java @@ -87,7 +87,6 @@ public PCollection>> expand(PBegin input) { private static class SnapshotPollFn extends Watch.Growth.PollFn> { private final IcebergScanConfig scanConfig; private @Nullable Long fromSnapshotId; - boolean isCacheSetup = false; SnapshotPollFn(IcebergScanConfig scanConfig) { this.scanConfig = scanConfig; @@ -95,11 +94,7 @@ private static class SnapshotPollFn extends Watch.Growth.PollFn> apply(String tableIdentifier, Context c) { - if (!isCacheSetup) { - TableCache.setup(scanConfig); - isCacheSetup = true; - } - Table table = TableCache.getRefreshed(tableIdentifier); + Table table = TableCache.getRefreshed(scanConfig.getCatalogConfig(), tableIdentifier); @Nullable Long userSpecifiedToSnapshot = ReadUtils.getToSnapshot(table, scanConfig); boolean isComplete = userSpecifiedToSnapshot != null; diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java index fbd6c15095e9..5cf095dc3c30 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java @@ -31,8 +31,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.iceberg.catalog.Catalog; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; class WriteDirectRowsToFiles @@ -66,7 +64,6 @@ private static class WriteDirectRowsToFilesDoFn extends DoFn, Fi private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private final String filePrefix; private final long maxFileSize; private transient @Nullable RecordWriterManager recordWriterManager; @@ -83,17 +80,10 @@ private static class WriteDirectRowsToFilesDoFn extends DoFn, Fi this.recordWriterManager = null; } - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; - } - @StartBundle public void startBundle() { recordWriterManager = - new RecordWriterManager(getCatalog(), filePrefix, maxFileSize, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, filePrefix, maxFileSize, Integer.MAX_VALUE); } @ProcessElement diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java index 12d9570d4a38..b16496240d18 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java @@ -30,8 +30,6 @@ import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; -import org.apache.iceberg.catalog.Catalog; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; class WriteGroupedRowsToFiles extends PTransform< @@ -67,7 +65,6 @@ private static class WriteGroupedRowsToFilesDoFn private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private final String filePrefix; private final long maxFileSize; @@ -82,13 +79,6 @@ private static class WriteGroupedRowsToFilesDoFn this.maxFileSize = maxFileSize; } - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; - } - @ProcessElement public void processElement( ProcessContext c, @@ -103,7 +93,7 @@ public void processElement( WindowedValues.of(destination, window.maxTimestamp(), window, paneInfo); RecordWriterManager writer; try (RecordWriterManager openWriter = - new RecordWriterManager(getCatalog(), filePrefix, maxFileSize, Integer.MAX_VALUE)) { + new RecordWriterManager(catalogConfig, filePrefix, maxFileSize, Integer.MAX_VALUE)) { writer = openWriter; for (Row e : element.getValue()) { writer.write(windowedDestination, e); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java index 54ad120f1aca..8b4ae0863f72 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java @@ -22,11 +22,8 @@ import static org.apache.beam.sdk.io.iceberg.RecordWriterManager.getPartitionDataPath; import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; -import java.time.Duration; -import java.time.Instant; import java.util.Map; import java.util.UUID; -import java.util.concurrent.TimeUnit; import org.apache.beam.sdk.coders.IterableCoder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.RowCoder; @@ -37,8 +34,6 @@ import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.apache.iceberg.DataFiles; import org.apache.iceberg.PartitionField; @@ -53,6 +48,7 @@ import org.apache.iceberg.data.Record; import org.apache.iceberg.exceptions.AlreadyExistsException; import org.apache.iceberg.exceptions.NoSuchTableException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,8 +87,9 @@ private static class WriteDoFn extends DoFn>, FileWriteRes private final IcebergCatalogConfig catalogConfig; private final String filePrefix; private final Schema dataSchema; - static final Cache LAST_REFRESHED_TABLE_CACHE = - CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(); + private transient @MonotonicNonNull Map specIds; + private transient @MonotonicNonNull Map> + partitionFieldMaps; WriteDoFn( IcebergCatalogConfig catalogConfig, @@ -105,6 +102,12 @@ private static class WriteDoFn extends DoFn>, FileWriteRes this.dataSchema = dataSchema; } + @Setup + public void setup() { + partitionFieldMaps = Maps.newHashMap(); + specIds = Maps.newHashMap(); + } + @ProcessElement public void processElement( @Element KV> element, OutputReceiver out) @@ -113,9 +116,10 @@ public void processElement( String partitionPath = checkStateNotNull(element.getKey().getString(PARTITION)); IcebergDestination destination = dynamicDestinations.instantiateDestination(tableIdentifier); - LastRefreshedTable lastRefreshedTable = getOrCreateTable(destination, dataSchema); - Table table = lastRefreshedTable.table; - partitionPath = getPartitionDataPath(partitionPath, lastRefreshedTable.partitionFieldMap); + Table table = getOrCreateTable(destination, dataSchema); + partitionPath = + getPartitionDataPath( + partitionPath, getPartitionFieldMap(destination.getTableIdentifier(), table)); StructLike partitionData = table.spec().isPartitioned() @@ -146,60 +150,32 @@ public void processElement( .build()); } - static final class LastRefreshedTable { - final Table table; - volatile Instant lastRefreshTime; - static final Duration STALENESS_THRESHOLD = Duration.ofMinutes(2); - private int specId; - volatile Map partitionFieldMap = Maps.newHashMap(); - - LastRefreshedTable(Table table, Instant lastRefreshTime) { - this.table = table; - this.specId = table.spec().specId(); - this.lastRefreshTime = lastRefreshTime; - for (PartitionField partitionField : table.spec().fields()) { - partitionFieldMap.put(partitionField.name(), partitionField); - } + private Map getPartitionFieldMap( + TableIdentifier identifier, Table table) { + @Nullable Integer specId = checkStateNotNull(specIds).get(identifier); + if (specId != null && specId == table.spec().specId()) { + return checkStateNotNull(checkStateNotNull(partitionFieldMaps).get(identifier)); } - - /** - * Refreshes the table metadata if it is considered stale (older than 2 minutes). - * - *

      This method first performs a non-synchronized check on the table's freshness. This - * provides a lock-free fast path that avoids synchronization overhead in the common case - * where the table does not need to be refreshed. If the table might be stale, it then enters - * a synchronized block to ensure that only one thread performs the refresh operation. - */ - void refreshIfStale() { - // Fast path: Avoid entering the synchronized block if the table is not stale. - if (lastRefreshTime.isAfter(Instant.now().minus(STALENESS_THRESHOLD))) { - return; - } - synchronized (this) { - if (lastRefreshTime.isBefore(Instant.now().minus(STALENESS_THRESHOLD))) { - table.refresh(); - lastRefreshTime = Instant.now(); - if (table.spec().specId() != this.specId) { - partitionFieldMap = Maps.newHashMap(); - for (PartitionField partitionField : table.spec().fields()) { - partitionFieldMap.put(partitionField.name(), partitionField); - } - this.specId = table.spec().specId(); - } - } - } + Map partitionFieldMap = Maps.newHashMap(); + for (PartitionField partitionField : table.spec().fields()) { + partitionFieldMap.put(partitionField.name(), partitionField); } + checkStateNotNull(specIds).put(identifier, table.spec().specId()); + checkStateNotNull(partitionFieldMaps).put(identifier, partitionFieldMap); + return partitionFieldMap; } - LastRefreshedTable getOrCreateTable(IcebergDestination destination, Schema dataSchema) { + Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) { TableIdentifier identifier = destination.getTableIdentifier(); - @Nullable - LastRefreshedTable lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier); - if (lastRefreshedTable != null) { - lastRefreshedTable.refreshIfStale(); - return lastRefreshedTable; - } + return TableCache.getAndRefreshIfStale( + catalogConfig, + identifier, + () -> loadOrCreateTable(catalogConfig.catalog(), destination, dataSchema)); + } + private Table loadOrCreateTable( + Catalog catalog, IcebergDestination destination, Schema dataSchema) { + TableIdentifier identifier = destination.getTableIdentifier(); Namespace namespace = identifier.namespace(); @Nullable IcebergTableCreateConfig createConfig = destination.getTableCreateConfig(); PartitionSpec partitionSpec = @@ -209,55 +185,43 @@ LastRefreshedTable getOrCreateTable(IcebergDestination destination, Schema dataS ? createConfig.getTableProperties() : Maps.newHashMap(); - @Nullable Table table = null; - synchronized (LAST_REFRESHED_TABLE_CACHE) { - lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier); - if (lastRefreshedTable != null) { - lastRefreshedTable.refreshIfStale(); - return lastRefreshedTable; - } - - Catalog catalog = catalogConfig.catalog(); - // Create namespace if it does not exist yet - if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { - SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; - if (!supportsNamespaces.namespaceExists(namespace)) { - try { - supportsNamespaces.createNamespace(namespace); - LOG.info("Created new namespace '{}'.", namespace); - } catch (AlreadyExistsException ignored) { - // race condition: another worker already created this namespace - LOG.info("Namespace `{}` already exists.", namespace); - } + // Create namespace if it does not exist yet + if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { + SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; + if (!supportsNamespaces.namespaceExists(namespace)) { + try { + supportsNamespaces.createNamespace(namespace); + LOG.info("Created new namespace '{}'.", namespace); + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this namespace + LOG.info("Namespace `{}` already exists.", namespace); } } + } - // If table exists, just load it - // Note: the implementation of catalog.tableExists() will load the table to check its - // existence. We don't use it here to avoid double loadTable() calls. + // If table exists, just load it + // Note: the implementation of catalog.tableExists() will load the table to check its + // existence. We don't use it here to avoid double loadTable() calls. + try { + return catalog.loadTable(identifier); + } catch (NoSuchTableException e) { // Otherwise, create the table + org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema); try { - table = catalog.loadTable(identifier); - } catch (NoSuchTableException e) { // Otherwise, create the table - org.apache.iceberg.Schema tableSchema = - IcebergUtils.beamSchemaToIcebergSchema(dataSchema); - try { - table = catalog.createTable(identifier, tableSchema, partitionSpec, tableProperties); - LOG.info( - "Created Iceberg table '{}' with schema: {}\n" - + ", partition spec: {}, table properties: {}", - identifier, - tableSchema, - partitionSpec, - tableProperties); - } catch (AlreadyExistsException ignored) { - // race condition: another worker already created this table - table = catalog.loadTable(identifier); - } + Table table = + catalog.createTable(identifier, tableSchema, partitionSpec, tableProperties); + LOG.info( + "Created Iceberg table '{}' with schema: {}\n" + + ", partition spec: {}, table properties: {}", + identifier, + tableSchema, + partitionSpec, + tableProperties); + return table; + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this table + return catalog.loadTable(identifier); } } - lastRefreshedTable = new LastRefreshedTable(table, Instant.now()); - LAST_REFRESHED_TABLE_CACHE.put(identifier, lastRefreshedTable); - return lastRefreshedTable; } } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java index 1db6ede30165..ff9a98c62005 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java @@ -47,8 +47,6 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.iceberg.catalog.Catalog; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -193,7 +191,6 @@ private static class WriteUngroupedRowsToFilesDoFn private final long maxFileSize; private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private transient @Nullable RecordWriterManager recordWriterManager; private int spilledShardNumber; @@ -210,17 +207,10 @@ public WriteUngroupedRowsToFilesDoFn( this.maxFileSize = maxFileSize; } - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; - } - @StartBundle public void startBundle() { recordWriterManager = - new RecordWriterManager(getCatalog(), filename, maxFileSize, maxWritersPerBundle); + new RecordWriterManager(catalogConfig, filename, maxFileSize, maxWritersPerBundle); this.spilledShardNumber = ThreadLocalRandom.current().nextInt(SPILLED_RECORD_SHARDING_FACTOR); } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java index f27a86cc72a2..390e8d87af28 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java @@ -53,7 +53,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.hadoop.conf.Configuration; import org.apache.iceberg.AppendFiles; import org.apache.iceberg.DataFile; import org.apache.iceberg.FileFormat; @@ -69,7 +68,6 @@ import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.data.Record; -import org.apache.iceberg.hadoop.HadoopCatalog; import org.apache.iceberg.io.FileIO; import org.apache.iceberg.io.InputFile; import org.apache.iceberg.io.OutputFile; @@ -109,16 +107,26 @@ public class RecordWriterManagerTest { IcebergUtils.beamSchemaToIcebergSchema(BEAM_SCHEMA); private static final PartitionSpec PARTITION_SPEC = PartitionSpec.builderFor(ICEBERG_SCHEMA).truncate("name", 3).identity("bool").build(); + private IcebergCatalogConfig catalogConfig; private WindowedValue windowedDestination; - private HadoopCatalog catalog; @Before public void setUp() { windowedDestination = getWindowedDestination("table_" + testName.getMethodName(), PARTITION_SPEC); - catalog = new HadoopCatalog(new Configuration(), warehouse.location); - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.invalidateAll(); + catalogConfig = + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build(); + TableCache.invalidateAll(); + } + + private static IcebergCatalogConfig mockCatalogConfigFor(Catalog mockCatalog) { + IcebergCatalogConfig catalogConfig = mock(IcebergCatalogConfig.class); + Mockito.doReturn(mockCatalog).when(catalogConfig).catalog(); + return catalogConfig; } private WindowedValue getWindowedDestination( @@ -145,7 +153,8 @@ private WindowedValue getWindowedDestination( @Test public void testCreateNamespaceAndTable() { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); Namespace newNamespace = Namespace.of("new_namespace"); TableIdentifier identifier = TableIdentifier.of(newNamespace, testName.getMethodName()); WindowedValue dest = @@ -157,15 +166,16 @@ public void testCreateNamespaceAndTable() { Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); - assertFalse(catalog.namespaceExists(newNamespace)); + assertFalse(catalogConfig.namespaceExists(newNamespace.toString())); boolean writeSuccess = writerManager.write(dest, row); assertTrue(writeSuccess); - assertTrue(catalog.namespaceExists(newNamespace)); + assertTrue(catalogConfig.namespaceExists(newNamespace.toString())); } @Test public void testCreateTableWithSortOrder() throws IOException { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); TableIdentifier identifier = TableIdentifier.of("default", testName.getMethodName()); WindowedValue dest = WindowedValues.valueInGlobalWindow( @@ -184,7 +194,7 @@ public void testCreateTableWithSortOrder() throws IOException { assertTrue(writerManager.write(dest, row)); writerManager.close(); - Table created = catalog.loadTable(identifier); + Table created = catalogConfig.catalog().loadTable(identifier); SortOrder order = created.sortOrder(); assertEquals(2, order.fields().size()); assertEquals(SortDirection.DESC, order.fields().get(0).direction()); @@ -196,7 +206,8 @@ public void testCreateTableWithSortOrder() throws IOException { @Test public void testCreateNewWriterForEachDestination() throws IOException { // Writer manager with a maximum limit of 3 writers - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); assertEquals(0, writerManager.openWriters); boolean writeSuccess; @@ -257,7 +268,8 @@ public void testCreateNewWriterForEachDestination() throws IOException { @Test public void testCreateNewWriterForEachPartition() throws IOException { // Writer manager with a maximum limit of 3 writers - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); assertEquals(0, writerManager.openWriters); boolean writeSuccess; @@ -318,7 +330,8 @@ public void testCreateNewWriterForEachPartition() throws IOException { @Test public void testRespectMaxFileSize() throws IOException { // Writer manager with a maximum file size of 100 bytes - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 100, 2); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 100, 2); assertEquals(0, writerManager.openWriters); boolean writeSuccess; @@ -364,7 +377,8 @@ public void testRespectMaxFileSize() throws IOException { @Test public void testRequireClosingBeforeFetchingDataFiles() { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 100, 2); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 100, 2); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); writerManager.write(windowedDestination, row); assertEquals(1, writerManager.openWriters); @@ -401,7 +415,11 @@ public void testSerializableDataFileRoundTripEquality() throws IOException { partitionKey.partition(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); RecordWriter writer = - new RecordWriter(catalog, windowedDestination.getValue(), "test_file_name", partitionKey); + new RecordWriter( + catalogConfig.catalog(), + windowedDestination.getValue(), + "test_file_name", + partitionKey); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row2)); @@ -443,7 +461,11 @@ public void testRecreateSerializableDataAfterUpdatingPartitionSpec() throws IOEx // write some rows RecordWriter writer = - new RecordWriter(catalog, windowedDestination.getValue(), "test_file_name", partitionKey); + new RecordWriter( + catalogConfig.catalog(), + windowedDestination.getValue(), + "test_file_name", + partitionKey); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row2)); writer.close(); @@ -462,7 +484,8 @@ public void testRecreateSerializableDataAfterUpdatingPartitionSpec() throws IOEx assertEquals(serializableDataFile.getPartitionSpecId(), datafile.specId()); // update spec - Table table = catalog.loadTable(windowedDestination.getValue().getTableIdentifier()); + Table table = + catalogConfig.catalog().loadTable(windowedDestination.getValue().getTableIdentifier()); table.updateSpec().addField("id").removeField("bool").commit(); Map updatedSpecs = table.specs(); @@ -473,13 +496,14 @@ public void testRecreateSerializableDataAfterUpdatingPartitionSpec() throws IOEx @Test public void testWriterKeepsUpWithUpdatingPartitionSpec() throws IOException { - Table table = catalog.loadTable(windowedDestination.getValue().getTableIdentifier()); + Table table = + catalogConfig.catalog().loadTable(windowedDestination.getValue().getTableIdentifier()); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "abcdef", true).build(); Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "abcxyz", true).build(); // write some rows RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(windowedDestination, row); writer.write(windowedDestination, row2); writer.close(); @@ -497,20 +521,17 @@ public void testWriterKeepsUpWithUpdatingPartitionSpec() throws IOException { assertThat(dataFile.path().toString(), containsString("bool=true")); // table is cached - assertEquals(1, RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.size()); + assertEquals(1, TableCache.size()); // update spec table.updateSpec().addField("id").removeField("bool").commit(); - // Make the cached table stale to force reloading its metadata. - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.getIfPresent( - windowedDestination.getValue().getTableIdentifier()) - .lastRefreshTime = - Instant.EPOCH; + // Make the cached table stale to force refreshing its metadata. + TableCache.markStale(catalogConfig, windowedDestination.getValue().getTableIdentifier()); // write a second data file // should refresh the table and use the new partition spec RecordWriterManager writer2 = - new RecordWriterManager(catalog, "test_prefix_2", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix_2", Long.MAX_VALUE, Integer.MAX_VALUE); writer2.write(windowedDestination, row); writer2.write(windowedDestination, row2); writer2.close(); @@ -578,7 +599,7 @@ public void testIdentityPartitioning() throws IOException { getWindowedDestination("identity_partitioning", icebergSchema, spec); RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(dest, row); writer.close(); List files = writer.getSerializableDataFiles().get(dest); @@ -664,7 +685,7 @@ public void testBucketPartitioning() throws IOException { getWindowedDestination("bucket_partitioning", icebergSchema, spec); RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(dest, row); writer.close(); List files = writer.getSerializableDataFiles().get(dest); @@ -730,7 +751,7 @@ public void testTimePartitioning() throws IOException { // write some rows RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(dest, row); writer.close(); List files = writer.getSerializableDataFiles().get(dest); @@ -763,7 +784,7 @@ public void testTimePartitioning() throws IOException { String expectedPartition = String.join("/", expectedPartitions); DataFile dataFile = serializableDataFile.createDataFile( - catalog.loadTable(dest.getValue().getTableIdentifier()).specs()); + catalogConfig.catalog().loadTable(dest.getValue().getTableIdentifier()).specs()); assertThat(dataFile.path().toString(), containsString(expectedPartition)); } @@ -771,7 +792,8 @@ public void testTimePartitioning() throws IOException { @Test public void testWriterExceptionGetsCaught() throws IOException { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 100, 2); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 100, 2); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "abcdef", true).build(); PartitionKey partitionKey = new PartitionKey(PARTITION_SPEC, ICEBERG_SCHEMA); partitionKey.partition(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); @@ -783,7 +805,10 @@ public void testWriterExceptionGetsCaught() throws IOException { // replace with a failing record writer FailingRecordWriter failingWriter = new FailingRecordWriter( - catalog, windowedDestination.getValue(), "test_failing_writer", partitionKey); + catalogConfig.catalog(), + windowedDestination.getValue(), + "test_failing_writer", + partitionKey); state.writers.put(partitionKey, failingWriter); writerManager.write(windowedDestination, row); @@ -843,7 +868,8 @@ public void testColumnSpecificMetricsCollection() throws IOException { WindowedValue singleDestination = WindowedValues.valueInGlobalWindow(destination); - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bbb", false).build(); Row row3 = Row.withSchema(BEAM_SCHEMA).addValues(3, "ccc", true).build(); @@ -905,7 +931,8 @@ public void testDefaultMetrics() throws IOException { WindowedValue singleDestination = WindowedValues.valueInGlobalWindow(destination); - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bbb", false).build(); Row row3 = Row.withSchema(BEAM_SCHEMA).addValues(3, "ccc", true).build(); @@ -1090,15 +1117,16 @@ public void testRecordWriterManagerDoesNotCloseSharedFileIO() throws IOException Mockito.doReturn(sharedTrackingIO).when(spyTable1).io(); Mockito.doReturn(sharedTrackingIO).when(spyTable2).io(); - Catalog spyCatalog = Mockito.spy(catalog); + Catalog spyCatalog = Mockito.spy(catalogConfig.catalog()); Mockito.doReturn(spyTable1).when(spyCatalog).loadTable(tableId1); Mockito.doReturn(spyTable2).when(spyCatalog).loadTable(tableId2); WindowedValue dest1 = getWindowedDestination(tableName1, null); WindowedValue dest2 = getWindowedDestination(tableName2, null); + IcebergCatalogConfig spyCatalogConfig = mockCatalogConfigFor(spyCatalog); RecordWriterManager writerManager = - new RecordWriterManager(spyCatalog, "test_file_name", 1000, 3); + new RecordWriterManager(spyCatalogConfig, "test_file_name", 1000, 3); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); assertTrue(writerManager.write(dest1, row)); @@ -1205,17 +1233,15 @@ public void testGetOrCreateTable_refreshLogic() { // test. Schema beamSchema = null; - // Instantiate a RecordWriterManager with a dummy catalog. - RecordWriterManager writer = new RecordWriterManager(null, "p", 1L, 1); + IcebergCatalogConfig mockCatalogConfig = mock(IcebergCatalogConfig.class); + RecordWriterManager writer = new RecordWriterManager(mockCatalogConfig, "p", 1L, 1); // Clean up cache before test - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.invalidateAll(); + TableCache.invalidateAll(); // --- 1. Test the fast path (entry is not stale) --- Instant freshTimestamp = Instant.now().minus(Duration.ofMinutes(1)); - RecordWriterManager.LastRefreshedTable freshEntry = - new RecordWriterManager.LastRefreshedTable(mockTable, freshTimestamp); - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.put(identifier, freshEntry); + TableCache.put(mockCatalogConfig, identifier, mockTable, freshTimestamp); // Access the table writer.getOrCreateTable(destination, beamSchema); @@ -1225,9 +1251,7 @@ public void testGetOrCreateTable_refreshLogic() { // --- 2. Test the stale path (entry is stale) --- Instant staleTimestamp = Instant.now().minus(Duration.ofMinutes(5)); - RecordWriterManager.LastRefreshedTable staleEntry = - new RecordWriterManager.LastRefreshedTable(mockTable, staleTimestamp); - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.put(identifier, staleEntry); + TableCache.put(mockCatalogConfig, identifier, mockTable, staleTimestamp); // Access the table again writer.getOrCreateTable(destination, beamSchema); @@ -1253,14 +1277,15 @@ public void testFileIOSurvivesAcrossBundles() throws IOException { Table spyTable = Mockito.spy(realTable); Mockito.doReturn(sharedIO).when(spyTable).io(); - Catalog spyCatalog = Mockito.spy(catalog); + Catalog spyCatalog = Mockito.spy(catalogConfig.catalog()); Mockito.doReturn(spyTable).when(spyCatalog).loadTable(tableId); WindowedValue dest = getWindowedDestination(tableName, null); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); + IcebergCatalogConfig spyCatalogConfig = mockCatalogConfigFor(spyCatalog); // Bundle 1: write and close - RecordWriterManager bundle1 = new RecordWriterManager(spyCatalog, "file_b1", 1000, 3); + RecordWriterManager bundle1 = new RecordWriterManager(spyCatalogConfig, "file_b1", 1000, 3); assertTrue(bundle1.write(dest, row)); bundle1.close(); assertFalse("FileIO must survive after bundle 1 close", sharedIO.closed); @@ -1268,7 +1293,7 @@ public void testFileIOSurvivesAcrossBundles() throws IOException { "Bundle 1 should produce data files", bundle1.getSerializableDataFiles().containsKey(dest)); // Bundle 2: write and close using the same catalog (simulates DoFn reuse) - RecordWriterManager bundle2 = new RecordWriterManager(spyCatalog, "file_b2", 1000, 3); + RecordWriterManager bundle2 = new RecordWriterManager(spyCatalogConfig, "file_b2", 1000, 3); assertTrue(bundle2.write(dest, row)); bundle2.close(); assertFalse("FileIO must survive after bundle 2 close", sharedIO.closed); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TableCacheTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TableCacheTest.java new file mode 100644 index 000000000000..6b3d7614ba4b --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TableCacheTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.TableIdentifier; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link TableCache}. */ +public class TableCacheTest { + private static final TableIdentifier IDENTIFIER = TableIdentifier.of("db", "table"); + + @Before + public void setUp() { + TableCache.invalidateAll(); + } + + @After + public void tearDown() { + TableCache.invalidateAll(); + } + + @Test + public void getLoadsTableOnceForSameCatalogAndIdentifier() { + Catalog catalog = mock(Catalog.class); + IcebergCatalogConfig catalogConfig = mock(IcebergCatalogConfig.class); + Table table = mock(Table.class); + when(catalogConfig.catalog()).thenReturn(catalog); + when(catalog.loadTable(IDENTIFIER)).thenReturn(table); + + assertSame(table, TableCache.get(catalogConfig, IDENTIFIER)); + assertSame(table, TableCache.get(catalogConfig, IDENTIFIER)); + assertSame(table, TableCache.get(catalogConfig, IDENTIFIER)); + + verify(catalog, times(1)).loadTable(IDENTIFIER); + } + + @Test + public void getKeysByCatalogConfigAndIdentifier() throws Exception { + IcebergCatalogConfig catalogConfig1 = + IcebergCatalogConfig.builder().setCatalogName("catalog").build(); + IcebergCatalogConfig catalogConfig2 = + IcebergCatalogConfig.builder().setCatalogName("catalog").build(); + Table table = mock(Table.class); + AtomicInteger loadCount = new AtomicInteger(); + + assertSame( + table, + TableCache.get( + catalogConfig1, + IDENTIFIER, + () -> { + loadCount.incrementAndGet(); + return table; + })); + assertSame( + table, + TableCache.get( + catalogConfig2, + IDENTIFIER, + () -> { + loadCount.incrementAndGet(); + return null; + })); + + org.junit.Assert.assertEquals(1, loadCount.get()); + } + + @Test + public void getRefreshedDoesNotRefreshNewlyLoadedTable() { + Catalog catalog = mock(Catalog.class); + IcebergCatalogConfig catalogConfig = mock(IcebergCatalogConfig.class); + Table table = mock(Table.class); + when(catalogConfig.catalog()).thenReturn(catalog); + when(catalog.loadTable(IDENTIFIER)).thenReturn(table); + + assertSame(table, TableCache.getRefreshed(catalogConfig, IDENTIFIER)); + + verify(catalog, times(1)).loadTable(IDENTIFIER); + verify(table, never()).refresh(); + } + + @Test + public void getRefreshedPropagatesRefreshFailure() { + IcebergCatalogConfig catalogConfig = mock(IcebergCatalogConfig.class); + Table table = mock(Table.class); + RuntimeException refreshFailure = new RuntimeException("refresh failed"); + doThrow(refreshFailure).when(table).refresh(); + TableCache.put(catalogConfig, IDENTIFIER, table, Instant.EPOCH); + + RuntimeException thrown = + assertThrows( + RuntimeException.class, () -> TableCache.getRefreshed(catalogConfig, IDENTIFIER)); + + assertSame(refreshFailure, thrown); + } +} From 036ed69f9a97781489aef915fd9b845c981eefa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:44:29 -0400 Subject: [PATCH 362/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38912) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.26 to 1.22.27. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.26...feature/s3/manager/v1.22.27) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.27 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 8817a26cd1f6..1d6c461baee4 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.25 github.com/aws/aws-sdk-go-v2/credentials v1.19.24 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 github.com/aws/smithy-go v1.27.2 github.com/docker/go-connections v0.7.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 7ed4b94c6337..49edbd2680d1 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEt github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26 h1:YNUEPr7Yiako/MzR/h3woMREbdwj0hGiBsZc5ZM90yE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.26/go.mod h1:KswdJ4xh+tUgW5CWx7sarhQuD+3iKgg46wojfmCA8q4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27 h1:gb+HtIZdwcIoLxv/xwGumQr1DmGmGGCQnjKKVVSMYsU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27/go.mod h1:2b/8jZl/qwUMBZpSAcxX+IdM3zj6RUyfnB2IdLt9I+I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= From 19c166c9f4f9e2ac2f1a78d87027083fd933dd6f Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 11 Jun 2026 16:00:17 +0200 Subject: [PATCH 363/490] Use p310_ml_test (#38890) --- .github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json | 2 +- .github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json index 8ed972c9f579..b5704c67ef1c 100644 --- a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 3 + "revision": 6 } diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index 4f30d1f9f081..9598b19ebbe0 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -80,7 +80,7 @@ jobs: - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=p310_ml_test,yaml - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() From b6f55d4e0003380141530e92702debda8a98ed3f Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 11 Jun 2026 16:11:32 +0200 Subject: [PATCH 364/490] Replace ClassLoadingStrategy with custom loading strategy (#38906) * Replace ClassLoadingStrategy with custom loading strategy * Update findClassLoader calls to match * spotless * Generate subclasses in beam's package and loade them in the same classloader as ProtoByteBuddyUtils * Resolve classloader visibility issues in ProtoByteBuddyUtils --- .../protobuf/ProtoByteBuddyUtils.java | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/sdks/java/extensions/protobuf/src/main/java/org/apache/beam/sdk/extensions/protobuf/ProtoByteBuddyUtils.java b/sdks/java/extensions/protobuf/src/main/java/org/apache/beam/sdk/extensions/protobuf/ProtoByteBuddyUtils.java index 98f80f6786c8..a1e45868e87d 100644 --- a/sdks/java/extensions/protobuf/src/main/java/org/apache/beam/sdk/extensions/protobuf/ProtoByteBuddyUtils.java +++ b/sdks/java/extensions/protobuf/src/main/java/org/apache/beam/sdk/extensions/protobuf/ProtoByteBuddyUtils.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.extensions.protobuf; import static org.apache.beam.sdk.extensions.protobuf.ProtoSchemaTranslator.getFieldNumber; +import static org.apache.beam.sdk.util.ByteBuddyUtils.getClassLoadingStrategy; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; @@ -55,7 +56,6 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.description.type.TypeDescription.ForLoadedType; import net.bytebuddy.dynamic.DynamicType; -import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.dynamic.scaffold.InstrumentedType; import net.bytebuddy.implementation.FixedValue; import net.bytebuddy.implementation.Implementation; @@ -88,7 +88,6 @@ import org.apache.beam.sdk.schemas.SchemaUserTypeCreator; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; import org.apache.beam.sdk.schemas.logicaltypes.OneOfType; -import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.ConvertType; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.ConvertValueForGetter; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.ConvertValueForSetter; @@ -511,8 +510,16 @@ public TypeConversion createSetterConversions(StackManipulati int[] keys = getterMethodMap.keySet().stream().mapToInt(Integer::intValue).toArray(); + Class targetClass = getLoadingTarget(protoClass); + @SuppressWarnings("unchecked") DynamicType.Builder> builder = - ByteBuddyUtils.subclassGetterInterface(BYTE_BUDDY, protoClass, OneOfType.Value.class); + (DynamicType.Builder>) + BYTE_BUDDY + .with(new InjectPackageStrategy(targetClass)) + .subclass( + TypeDescription.Generic.Builder.parameterizedType( + FieldValueGetter.class, protoClass, OneOfType.Value.class) + .build()); builder = builder .method(ElementMatchers.named("name")) @@ -546,7 +553,7 @@ public TypeConversion createSetterConversions(StackManipulati return builder .visit(new AsmVisitorWrapper.ForDeclaredMethods().writerFlags(ClassWriter.COMPUTE_FRAMES)) .make() - .load(ReflectHelpers.findClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .load(targetClass.getClassLoader(), getClassLoadingStrategy(targetClass)) .getLoaded() .getDeclaredConstructor(List.class, OneOfType.class) .newInstance(getters, oneOfType); @@ -568,9 +575,16 @@ FieldValueSetter createOneOfSetter( boolean contiguous = isContiguous(indices); int[] keys = setterMethodMap.keySet().stream().mapToInt(Integer::intValue).toArray(); + Class targetClass = getLoadingTarget(protoBuilderClass); + @SuppressWarnings("unchecked") DynamicType.Builder> builder = - ByteBuddyUtils.subclassSetterInterface( - BYTE_BUDDY, protoBuilderClass, OneOfType.Value.class); + (DynamicType.Builder>) + BYTE_BUDDY + .with(new InjectPackageStrategy(targetClass)) + .subclass( + TypeDescription.Generic.Builder.parameterizedType( + FieldValueSetter.class, protoBuilderClass, OneOfType.Value.class) + .build()); builder = builder .method(ElementMatchers.named("name")) @@ -598,7 +612,7 @@ FieldValueSetter createOneOfSetter( return builder .visit(new AsmVisitorWrapper.ForDeclaredMethods().writerFlags(ClassWriter.COMPUTE_FRAMES)) .make() - .load(ReflectHelpers.findClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .load(targetClass.getClassLoader(), getClassLoadingStrategy(targetClass)) .getLoaded() .getDeclaredConstructor(List.class) .newInstance(setters); @@ -1103,11 +1117,16 @@ static SchemaUserTypeCreator createB List> setters, Schema schema) { try { + Class targetClass = getLoadingTarget(builderClass); + @SuppressWarnings("unchecked") DynamicType.Builder> builder = - (DynamicType.Builder) + (DynamicType.Builder>) BYTE_BUDDY - .with(new InjectPackageStrategy(builderClass)) - .subclass(Supplier.class) + .with(new InjectPackageStrategy(targetClass)) + .subclass( + TypeDescription.Generic.Builder.parameterizedType( + Supplier.class, builderClass) + .build()) .method(ElementMatchers.named("get")) .intercept(new BuilderSupplier(protoClass)); Supplier supplier = @@ -1116,7 +1135,7 @@ static SchemaUserTypeCreator createB new AsmVisitorWrapper.ForDeclaredMethods() .writerFlags(ClassWriter.COMPUTE_FRAMES)) .make() - .load(ReflectHelpers.findClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .load(targetClass.getClassLoader(), getClassLoadingStrategy(targetClass)) .getLoaded() .getDeclaredConstructor() .newInstance(); @@ -1190,4 +1209,12 @@ public ByteCodeAppender appender(final Target implementationTarget) { }; } } + + private static Class getLoadingTarget(Class protoClass) { + ClassLoader loader = ReflectHelpers.findClassLoader(ProtoByteBuddyUtils.class, protoClass); + if (loader == ProtoByteBuddyUtils.class.getClassLoader()) { + return ProtoByteBuddyUtils.class; + } + return protoClass; + } } From 4baf0fc128de3a6f6e0b8b059f216f7c157acb31 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 11 Jun 2026 10:46:50 -0400 Subject: [PATCH 365/490] Don't preempt snapshot runs (#38904) --- .github/workflows/beam_Publish_Beam_SDK_Snapshots.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index 06214a200123..526abfd4e30b 100644 --- a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml +++ b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml @@ -36,11 +36,6 @@ permissions: security-events: read statuses: read -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.issue.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.sender.login }}' - cancel-in-progress: true - env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} From 2e94eadffe4ee76bb2dae2950e7b669113f68b20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:18:04 -0400 Subject: [PATCH 366/490] Bump @grpc/grpc-js from 1.14.3 to 1.14.4 in /sdks/typescript (#38922) Bumps [@grpc/grpc-js](https://github.com/grpc/grpc-node) from 1.14.3 to 1.14.4. - [Release notes](https://github.com/grpc/grpc-node/releases) - [Commits](https://github.com/grpc/grpc-node/compare/@grpc/grpc-js@1.14.3...@grpc/grpc-js@1.14.4) --- updated-dependencies: - dependency-name: "@grpc/grpc-js" dependency-version: 1.14.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/typescript/package-lock.json | 15 +++++++-------- sdks/typescript/package.json | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index 7bdc88efaf90..c7e294836db1 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -9,7 +9,7 @@ "version": "2.75.0-SNAPSHOT", "dependencies": { "@google-cloud/pubsub": "^4.2.0", - "@grpc/grpc-js": "^1.14.3", + "@grpc/grpc-js": "^1.14.4", "@protobuf-ts/grpc-transport": "^2.1.0", "@protobuf-ts/plugin": "^2.1.0", "bson": "^4.6.0", @@ -323,10 +323,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "license": "Apache-2.0", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -4893,9 +4892,9 @@ } }, "@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", "requires": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 6d9ffdb4aaae..b6b16be9a82d 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@google-cloud/pubsub": "^4.2.0", - "@grpc/grpc-js": "^1.14.3", + "@grpc/grpc-js": "^1.14.4", "@protobuf-ts/grpc-transport": "^2.1.0", "@protobuf-ts/plugin": "^2.1.0", "bson": "^4.6.0", From 9b27de68a5b7e0ae6439dfea53bea77890963539 Mon Sep 17 00:00:00 2001 From: Chamikara Jayalath Date: Thu, 11 Jun 2026 08:42:10 -0700 Subject: [PATCH 367/490] Adds a new agent SKILL for developing new I/O connectors (#38910) --- .../developing-new-io-connectors/SKILL.md | 345 ++++++++++++++++++ .agent/skills/io-connectors/SKILL.md | 2 + 2 files changed, 347 insertions(+) create mode 100644 .agent/skills/developing-new-io-connectors/SKILL.md diff --git a/.agent/skills/developing-new-io-connectors/SKILL.md b/.agent/skills/developing-new-io-connectors/SKILL.md new file mode 100644 index 000000000000..dfda083085de --- /dev/null +++ b/.agent/skills/developing-new-io-connectors/SKILL.md @@ -0,0 +1,345 @@ +--- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: developing-new-io-connectors +description: End-to-end guide on developing new Apache Beam I/O connectors correctly, including core IO transforms, SchemaTransforms, URN proto definitions, Managed API integration, cross-language expansion service, and testing. +--- + +# Developing New Apache Beam I/O Connectors + +This guide outlines the modern best practices and mandatory steps for building new Apache Beam I/O connectors. Modern Beam connectors are expected to be schema-aware, available in cross-language (Python, Go) pipelines via the Expansion Service, and seamlessly integrable with Beam YAML and the `Managed` I/O API. + +## 1. Module Structure and Gradle Setup + +New Java I/O connectors should reside under `sdks/java/io/`. + +### Directory Layout +```text +sdks/java/io// +├── build.gradle +└── src/ + ├── main/java/org/apache/beam/sdk/io// + │ ├── IO.java + │ ├── ReadSchemaTransformProvider.java + │ └── WriteSchemaTransformProvider.java + └── test/java/org/apache/beam/sdk/io// + ├── IOTest.java + └── ReadSchemaTransformProviderTest.java +``` + +### Gradle Configuration (`build.gradle`) +Your `build.gradle` must use standard Beam Java module conventions and explicitly declare necessary dependencies. + +```groovy +plugins { id 'org.apache.beam.module' } +applyJavaNature( + // If contains hyphens, convert them to dots or underscores + automaticModuleName: 'org.apache.beam.sdk.io.', +) + +description = "Apache Beam :: SDKs :: Java :: IO :: " +ext.summary = "Integration with ." + +dependencies { + implementation project(path: ":sdks:java:core", configuration: "shadow") + implementation project(path: ":model:pipeline", configuration: "shadow") // For URN definitions + + // Add external client libraries here + implementation library.java. + + // Handle strict dependency checking if necessary + permitUnusedDeclared library.java. + + // Standard test dependencies + testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") + testImplementation library.java.junit +} +``` + +--- + +## 2. Core I/O Transform Implementation (`IO.java`) + +Follow Beam's canonical AutoValue builder pattern for user-facing API configuration. While core Java I/O connectors can be strongly typed using specific domain classes or Java generics (``) for idiomatic Java SDK usage, modern sources should also emphasize Beam `Row` and Schema support (e.g., via `.readRows()`). For excellent real-world implementations of this pattern, refer to `IcebergIO` and `DeltaIO`. + +### Bounded & Unbounded Sources +Instead of legacy `Source` classes, implement reading via Beam's **Splittable DoFns (SDF)** framework for advanced features such as dynamic rebalancing and watermark support. + +A primary read transform (such as `read()` or `readRows()`) typically extends `PTransform>` (or `PCollection`). Using `PCollection` as input is meant for "ReadAll" operations (such as reading a collection of file patterns or queries). + +An example SDF-based read transform is given below: + +```java +public class MyIO { + public static ReadRows readRows() { + return new AutoValue_MyIO_ReadRows.Builder().build(); + } + + @AutoValue + public abstract static class ReadRows extends PTransform> { + public abstract @Nullable String getConfigurationOption(); + public abstract @Nullable Schema getSchema(); + public abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConfigurationOption(String value); + public abstract Builder setSchema(Schema schema); + public abstract ReadRows build(); + } + + public ReadRows withConfigurationOption(String value) { + return toBuilder().setConfigurationOption(value).build(); + } + + public ReadRows withSchema(Schema schema) { + return toBuilder().setSchema(schema).build(); + } + + @Override + public PCollection expand(PBegin input) { + return input + // `ReaderDoFn` is an SDF or source implementation that reads records and outputs `Row` objects. + .apply(ParDo.of(new ReaderDoFn(getConfigurationOption()))) + .setRowSchema(getSchema()); + } + } + + public static WriteRows writeRows() { + return new AutoValue_MyIO_WriteRows.Builder().build(); + } + + @AutoValue + public abstract static class WriteRows extends PTransform, PDone> { + public abstract @Nullable String getConfigurationOption(); + public abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConfigurationOption(String value); + public abstract WriteRows build(); + } + + public WriteRows withConfigurationOption(String value) { + return toBuilder().setConfigurationOption(value).build(); + } + + @Override + public PDone expand(PCollection input) { + input.apply("WriteRecords", ParDo.of(new WriterDoFn(getConfigurationOption()))); + return PDone.in(input.getPipeline()); + } + } +} +``` +* Please make sure that any code you add adheres to the Beam coding standards. These standards are documented here: https://beam.apache.org/contribute/code-guidelines/ + +* Especially scrutinize any logic that involves splitting the data for parallel processing since it is a common source of errors that can lead to data loss or data duplication related issues. + +### Developing Sinks (Write Transforms) +When implementing data egress (Write transforms), avoid creating single-worker bottlenecks. Depending on your target system's transactional requirements, prefer one of the following canonical Beam sink patterns: + +1. **DoFn-Based / Batching Sinks:** For external APIs or messaging systems (e.g., Kafka, Pub/Sub, NoSQL databases), use a `DoFn` that manages connections per bundle (`@StartBundle`, `@FinishBundle`) or utilizes `GroupIntoBatches` to perform highly efficient, parallel batched requests. +2. **Two-Phase Commit / Exactly-Once Sinks:** For transactional sinks (e.g., Relational DBs, Apache Iceberg, Delta Lake), implement a multi-stage `PTransform`: + * **Write Shards:** Write records in parallel tasks to staging files or temporary transactions, emitting commit descriptors (`PCollection`). + * **Global Commit:** Group the commit messages and execute a single final transaction to commit all staged files/shards. +3. **File-Based Sinks:** If your connector purely writes files, leverage Beam's `FileIO` core infrastructure (`FileIO.write()` / `FileIO.Sink`) rather than implementing custom file rolling and sharding logic. +4. **Error Reporting (Dead-Letter Queues):** Instead of failing the entire pipeline when an invalid element or API error occurs, modern Write transforms should optionally output a `PCollection` (or custom `WriteResult` / `PCollectionRowTuple`) containing failed records and explicit error metadata. + +* Specially scrutinize any logic that can create duplicate data due to worker failures. Assume that any transform in the pipeline can fail and be retried multiple times by the Beam runner. If the sink does not handle this properly, it can lead to duplicate data in the target system. +--- + +## 3. Extending the Pipeline Model Proto (`external_transforms.proto`) + +To standardize your transform identifier across SDKs, define its URN in Beam's protobuf schema. + +1. Open `model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto`. +2. Add your read/write URNs under the appropriate enum (e.g., `ManagedTransforms.Urns`). + +```protobuf +message ManagedTransforms { + enum Urns { + // ... existing entries + MY_SYSTEM_READ = 15 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:schematransform:org.apache.beam:my_system_read:v1"]; + MY_SYSTEM_WRITE = 16 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:schematransform:org.apache.beam:my_system_write:v1"]; + } +} +``` + +3. Re-generate and compile the model protos: +```bash +./gradlew :model:pipeline:generateProto :model:pipeline:compileJava +``` + +--- + +## 4. Implementing `SchemaTransformProvider` + +To expose your connector to cross-language pipelines and Beam YAML, create a typed `SchemaTransformProvider`. + +```java +@AutoService(SchemaTransformProvider.class) +public class MyReadSchemaTransformProvider extends TypedSchemaTransformProvider { + + @Override + public String identifier() { + return getUrn(ExternalTransforms.ManagedTransforms.Urns.MY_SYSTEM_READ); + } + + @Override + public String description() { + return "Reads records from My System and outputs a PCollection of Beam Rows."; + } + + @Override + protected SchemaTransform from(Configuration configuration) { + return new MyReadSchemaTransform(configuration); + } + + @Override + public List outputCollectionNames() { + return Collections.singletonList("output"); + } + + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class Configuration { + @SchemaFieldDescription("Configuration option description.") + public abstract String getConfigurationOption(); + + public static Builder builder() { + return new AutoValue_MyReadSchemaTransformProvider_Configuration.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConfigurationOption(String value); + public abstract Configuration build(); + } + } + + static class MyReadSchemaTransform extends SchemaTransform { + private final Configuration configuration; + + MyReadSchemaTransform(Configuration configuration) { + this.configuration = Objects.requireNonNull(configuration, "configuration cannot be null"); + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + PCollection output = input.getPipeline().apply( + MyIO.readRows().withConfigurationOption(configuration.getConfigurationOption())); + return PCollectionRowTuple.of("output", output); + } + } +} +``` + +--- + +## 5. Integrating with Managed API (`Managed.java`) + +Beam's `Managed` I/O transform provides a unified interface for data ingest/egress. To support it: + +1. Open `sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java`. +2. Define a public constant identifier: +```java +public static final String MY_SYSTEM = "my_system"; +``` +3. Register your URNs in `READ_TRANSFORMS` or `WRITE_TRANSFORMS`: +```java +public static final Map READ_TRANSFORMS = + ImmutableMap.builder() + // ... existing transforms + .put(MY_SYSTEM, getUrn(ExternalTransforms.ManagedTransforms.Urns.MY_SYSTEM_READ)) + .build(); +``` +4. Update the Javadoc block in `Managed.java` to list your new connector. + +--- + +## 6. Expansion Service Registration + +To enable non-Java SDKs (Python, Go) to discover and expand your new connector, include it in the standard Java Expansion Service. + +1. Open `sdks/java/io/expansion-service/build.gradle`. +2. Add your module as a runtime dependency: +```groovy +dependencies { + // ... existing dependencies + runtimeOnly project(":sdks:java:io:") +} +``` + +--- + +## 7. Python & Beam YAML Integration + +Once registered in the expansion service, your `SchemaTransform` can be utilized in Python and YAML. E.g., for `Managed` support in Python: + +1. Open `sdks/python/apache_beam/transforms/managed.py`. +2. Export your identifier in `__all__` and map it to its URN in `Read._READ_TRANSFORMS` or `Write._WRITE_TRANSFORMS`: +```python +MY_SYSTEM = 'my_system' + +__all__ = [ + # ... existing + "MY_SYSTEM", +] + +class Read(PTransform): + _READ_TRANSFORMS = { + # ... existing + MY_SYSTEM: ManagedTransforms.Urns.MY_SYSTEM_READ.urn, + } +``` +3. In `sdks/python/apache_beam/transforms/external.py`, map the URN to the appropriate Expansion Service jar target in `MANAGED_TRANSFORM_URN_TO_JAR_TARGET_MAPPING`. + +--- + +## 8. Verification and Testing + +Verify your new connector thoroughly across multiple abstraction layers: + +### 1. Unit Tests +Test your core builder and `SchemaTransformProvider` translation. +```bash +./gradlew :sdks:java:io::test +``` + +### 2. Managed API Translation +In `ManagedSchemaTransformTranslationTest.java` (under `sdks/java/managed`), you can verify the translation structure of your managed transform. Note that `ManagedTest.java` generally uses dummy/test providers (`TestSchemaTransformProvider`) to keep dependencies lightweight. +```bash +./gradlew :sdks:java:managed:test +``` + +### 3. Integration Tests (IT) +Create integration tests to test end-to-end data processing against real system instances, including `Managed.read(Managed.MY_SYSTEM)` usage. + +* Test resources should be managed via `ResourceManager` classes under the `it/` directory. +* Add GitHub Actions to trigger your tests when changes are made to your connector code. Consider adding one for pre-commit and one for post-commit. + +### 4. Documentation +Add any necessary documentation for your connector under the `website/www/site/content/en/documentation/io/built-in/` directory. + +--- + +> [!TIP] +> **Canonical Reference Implementations:** When developing a new connector, we highly recommend studying **Apache Iceberg** ([IcebergIO.java](https://github.com/apache/beam/blob/master/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java)) and **Delta Lake** ([DeltaIO.java](https://github.com/apache/beam/blob/master/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java)) as state-of-the-art reference implementations. diff --git a/.agent/skills/io-connectors/SKILL.md b/.agent/skills/io-connectors/SKILL.md index 596b602add6c..e0f641c46e0e 100644 --- a/.agent/skills/io-connectors/SKILL.md +++ b/.agent/skills/io-connectors/SKILL.md @@ -195,3 +195,5 @@ Key components: 1. **Source** - Reads data (bounded or unbounded) 2. **Sink** - Writes data 3. **Read/Write transforms** - User-facing API + +For more detailed information see the [Developing I/O connectors](https://beam.apache.org/documentation/io/developing-io-overview) guide. \ No newline at end of file From cd76624742e223ed6479b78a362dbf568b72e738 Mon Sep 17 00:00:00 2001 From: Chamikara Jayalath Date: Thu, 11 Jun 2026 09:56:42 -0700 Subject: [PATCH 368/490] Add Delta Lake source to the Java Managed API (#38902) --- .../pipeline/v1/external_transforms.proto | 2 + sdks/java/io/delta/build.gradle | 1 + .../DeltaReadSchemaTransformProvider.java | 142 ++++++++++++++++++ .../apache/beam/sdk/io/delta/DeltaIOTest.java | 41 +++++ .../org/apache/beam/sdk/managed/Managed.java | 4 + 5 files changed, 190 insertions(+) create mode 100644 sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto index 043a72dd34f2..c73986eed48b 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto @@ -88,6 +88,8 @@ message ManagedTransforms { "beam:schematransform:org.apache.beam:sql_server_read:v1"]; SQL_SERVER_WRITE = 12 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:sql_server_write:v1"]; + DELTA_LAKE_READ = 13 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:schematransform:org.apache.beam:delta_lake_read:v1"]; } } diff --git a/sdks/java/io/delta/build.gradle b/sdks/java/io/delta/build.gradle index c07aef6981b9..57b1cd8ad876 100644 --- a/sdks/java/io/delta/build.gradle +++ b/sdks/java/io/delta/build.gradle @@ -33,6 +33,7 @@ def parquet_version = "1.16.0" dependencies { implementation project(path: ":sdks:java:core", configuration: "shadow") + implementation project(path: ":model:pipeline", configuration: "shadow") implementation library.java.delta_kernel_api implementation library.java.delta_kernel_defaults diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java new file mode 100644 index 000000000000..42ca3f24def9 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import static org.apache.beam.sdk.io.delta.DeltaReadSchemaTransformProvider.Configuration; +import static org.apache.beam.sdk.util.construction.BeamUrns.getUrn; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.beam.model.pipeline.v1.ExternalTransforms; +import org.apache.beam.sdk.io.delta.DeltaReadSchemaTransformProvider.Configuration; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * SchemaTransform implementation for {@link DeltaIO#readRows}. Reads records from Delta Lake and + * outputs a {@link org.apache.beam.sdk.values.PCollection} of Beam {@link + * org.apache.beam.sdk.values.Row}s. + */ +@AutoService(SchemaTransformProvider.class) +public class DeltaReadSchemaTransformProvider extends TypedSchemaTransformProvider { + static final String OUTPUT_TAG = "output"; + + @Override + protected SchemaTransform from(Configuration configuration) { + return new DeltaReadSchemaTransform(configuration); + } + + @Override + public List outputCollectionNames() { + return Collections.singletonList(OUTPUT_TAG); + } + + @Override + public String identifier() { + return getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_READ); + } + + static class DeltaReadSchemaTransform extends SchemaTransform { + private final Configuration configuration; + + DeltaReadSchemaTransform(Configuration configuration) { + this.configuration = + java.util.Objects.requireNonNull(configuration, "configuration cannot be null"); + } + + Row getConfigurationRow() { + try { + return SchemaRegistry.createDefault() + .getToRowFunction(Configuration.class) + .apply(configuration) + .sorted() + .toSnakeCase(); + } catch (NoSuchSchemaException e) { + throw new RuntimeException(e); + } + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + DeltaIO.ReadRows read = DeltaIO.readRows().from(configuration.getTable()); + if (configuration.getVersion() != null) { + read = read.withVersion(configuration.getVersion()); + } + if (configuration.getTimestamp() != null) { + read = read.withTimestamp(configuration.getTimestamp()); + } + Map hadoopConfig = configuration.getHadoopConfig(); + if (hadoopConfig != null) { + read = read.withConfig(hadoopConfig); + } + + PCollection output = input.getPipeline().apply(read); + + return PCollectionRowTuple.of(OUTPUT_TAG, output); + } + } + + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class Configuration { + static Builder builder() { + return new AutoValue_DeltaReadSchemaTransformProvider_Configuration.Builder(); + } + + @SchemaFieldDescription("Identifier of the Delta Lake table.") + abstract String getTable(); + + @SchemaFieldDescription("Version of the Delta Lake table to read.") + @Nullable + abstract Long getVersion(); + + @SchemaFieldDescription("Timestamp of the Delta Lake table to read.") + @Nullable + abstract String getTimestamp(); + + @SchemaFieldDescription("Properties passed to the Hadoop Configuration.") + @Nullable + abstract Map getHadoopConfig(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setTable(String table); + + abstract Builder setVersion(@Nullable Long version); + + abstract Builder setTimestamp(@Nullable String timestamp); + + abstract Builder setHadoopConfig(@Nullable Map hadoopConfig); + + abstract Configuration build(); + } + } +} diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java index ef6dd660c607..bd8bf8b3c8cc 100644 --- a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java @@ -42,6 +42,7 @@ import org.apache.beam.sdk.io.FileIO; import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows; import org.apache.beam.sdk.io.parquet.ParquetIO; +import org.apache.beam.sdk.managed.Managed; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; @@ -52,6 +53,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -387,6 +389,45 @@ private byte[] writeParquetFile(File file, Row row) throws Exception { return java.nio.file.Files.readAllBytes(file.toPath()); } + @Test + public void testManagedDeltaRead() throws Exception { + File tableDir = tempFolder.newFolder("managed-delta-table"); + + // 1. Write a Parquet file to simulate a Delta table + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + writeParquetFile(new File(tableDir, "part-00000.parquet"), row); + + // 2. Create the Delta log + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + File parquetFile = new File(tableDir, "part-00000.parquet"); + byte[] fileBytes = Files.readAllBytes(parquetFile.toPath()); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":" + + fileBytes.length + + ",\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + // 3. Read it using Managed + PCollection output = + readPipeline + .apply( + Managed.read(Managed.DELTA_LAKE) + .withConfig(ImmutableMap.of("table", tableDir.getAbsolutePath()))) + .getSinglePCollection(); + + PAssert.that(output).containsInAnyOrder(row); + + readPipeline.run().waitUntilFinish(); + } + @Test @org.junit.Ignore("Manual integration test with external local table") public void testReadingLocalTable() throws Exception { diff --git a/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java b/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java index a5e7d879b441..9589992e079a 100644 --- a/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java +++ b/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java @@ -93,6 +93,7 @@ public class Managed { // TODO: Dynamically generate a list of supported transforms public static final String ICEBERG = "iceberg"; + public static final String DELTA_LAKE = "delta"; public static final String ICEBERG_CDC = "iceberg_cdc"; public static final String KAFKA = "kafka"; public static final String BIGQUERY = "bigquery"; @@ -104,6 +105,7 @@ public class Managed { public static final Map READ_TRANSFORMS = ImmutableMap.builder() .put(ICEBERG, getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_READ)) + .put(DELTA_LAKE, getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_READ)) .put(ICEBERG_CDC, getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_CDC_READ)) .put(KAFKA, getUrn(ExternalTransforms.ManagedTransforms.Urns.KAFKA_READ)) .put(BIGQUERY, getUrn(ExternalTransforms.ManagedTransforms.Urns.BIGQUERY_READ)) @@ -128,6 +130,8 @@ public class Managed { *

      \n", + " \n", + " \n", + "
      \n", + " Run in Google Colab\n", + " \n", + " View source on GitHub\n", + "
      \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HrCtxslBGK8Z" + }, + "source": [ + "This notebook shows how to use the Apache Beam [RunInference](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.base.html#apache_beam.ml.inference.base.RunInference) transform for image classification with [Gemini](https://ai.google.dev/gemini-api/docs).\n", + "Apache Beam has built-in support for sending requests to a remotely deployed Gemini model by using the [`GeminiModelHandler`](https://github.com/apache/beam/blob/5307c7138af8b5daba7a5495aba87d53ae9b0ae7/sdks/python/apache_beam/ml/inference/gemini_inference.py#L103) class. This class allows for custom input functions, enabling the use of any Gemini model accessible through the Gemini API; this allows for custom handling of any input type and configuration information necessary.\n", + "\n", + "When you use remote inference with Vertex AI, consider the following factors:\n", + "1. Sending requests to Gemini incurs cost from Google Cloud. Consider using smaller, less expensive models for experimentation and testing code.\n", + "\n", + "This notebook demonstrates the following steps:\n", + "- Configure a request function for the desired Gemini model\n", + "- Set up example inputs\n", + "- Run those examples with the built-in GeminiModelHandler and get a prediction inside an Apache Beam pipeline\n", + "- Use custom file sinks to save multimodal outputs from image and audio models\n", + "\n", + "For more information about using RunInference, see [Get started with AI/ML pipelines](https://beam.apache.org/documentation/ml/overview/) in the Apache Beam documentation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gVCtGOKTHMm4" + }, + "source": [ + "## Before you begin\n", + "Set up your environment and download dependencies." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YDHPlMjZRuY0" + }, + "source": [ + "### Install Apache Beam\n", + "To use RunInference with the built-in Gemini model handler, install the Apache Beam SDK version 2.66.0 or later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jBakpNZnAhqk" + }, + "outputs": [], + "source": [ + "!pip install apache_beam[interactive,gcp]==2.74.0 --quiet\n", + "\n", + "# To use the newly installed versions, restart the runtime.\n", + "exit()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X80jy3FqHjK4" + }, + "source": [ + "### Create a Gemini API Key\n", + "This notebook relies on the Gemini API, which requires an existing API key to use. Follow the instructions in the [Gemini API Quickstart](https://ai.google.dev/gemini-api/docs/quickstart#before_you_begin) to generate an API key for your account.\n", + "\n", + "To run the following cell, your API key must be stored it in a Colab Secret named `GEMINI_API_KEY`.\n", + "\n", + "1. Open your Google Colab notebook and click on the 🔑 **Secrets** tab in the left panel.\n", + " \n", + " \"You\n", + "\n", + "2. Create a new secret with the name `GEMINI_API_KEY`.\n", + "3. Copy and paste your API key into the `Value` input box of `GEMINI_API_KEY`.\n", + "4. Toggle the button on the left to allow all notebooks access to the secret." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "Kz9sccyGBqz3" + }, + "outputs": [], + "source": [ + "from google.colab import userdata\n", + "\n", + "GEMINI_API_KEY=userdata.get('GEMINI_API_KEY')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Import dependencies" + ], + "metadata": { + "id": "wwEvxB2_JO3h" + } + }, + { + "cell_type": "code", + "source": [ + "import os\n", + "import wave\n", + "\n", + "from collections.abc import Iterable\n", + "from collections.abc import Sequence\n", + "from io import BytesIO\n", + "from typing import Any\n", + "from typing import cast\n", + "\n", + "import apache_beam as beam\n", + "from apache_beam.io.fileio import FileSink\n", + "from apache_beam.io.fileio import WriteToFiles\n", + "from apache_beam.io.fileio import default_file_naming\n", + "from apache_beam.ml.inference.base import PredictionResult\n", + "from apache_beam.ml.inference.base import RunInference\n", + "from apache_beam.ml.inference.gemini_inference import GeminiModelHandler\n", + "from apache_beam.ml.inference.gemini_inference import generate_from_string\n", + "from apache_beam.ml.inference.gemini_inference import generate_image_from_strings_and_images\n", + "from apache_beam.options.pipeline_options import PipelineOptions\n", + "\n", + "from google import genai\n", + "from google.genai import types\n", + "from PIL import Image" + ], + "metadata": { + "id": "muTP7ywXJVea" + }, + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0a1zerXycQ0z" + }, + "source": [ + "## Run the pipeline" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text Output\n", + "This pipeline code takes text as input and receives text as output from Gemini." + ], + "metadata": { + "id": "ERSXxZS-OwBc" + } + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 766 + }, + "id": "St07XoibcQSb", + "outputId": "1ba4ef99-fcbb-441e-f991-bd370ff6f6c1" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Input: What is 5+2?, Output: 5 + 2 = 7\n", + "Input: Who is the protagonist of Lord of the Rings?, Output: While **Frodo Baggins** is widely considered the main protagonist of *The Lord of the Rings*, J.R.R. Tolkien’s epic is an ensemble piece with multiple characters occupying the role of \"hero.\" \n", + "\n", + "Depending on how you analyze the story, there are three primary candidates for the protagonist:\n", + "\n", + "### 1. Frodo Baggins (The Narrative Protagonist)\n", + "Frodo is the literal protagonist of the central plot. As the Ring-bearer, he is tasked with the ultimate quest: carrying the One Ring to Mount Doom to destroy it. \n", + "* **Why he is the protagonist:** The main conflict of the entire story—the struggle against the corrupting influence of the Ring—takes place inside Frodo’s mind and soul. If Frodo fails, Middle-earth falls. He is the tragic hero who sacrifices his own peace and well-being so that others may live.\n", + "\n", + "### 2. Samwise Gamgee (The Moral and Thematic Hero)\n", + "While Frodo carries the Ring, his loyal servant and friend Samwise Gamgee is often considered the emotional heart of the story.\n", + "* **Why he is the protagonist:** Tolkien himself famously referred to Sam as the **\"chief hero\"** of the epic in one of his letters (Letter 131). Sam represents the ordinary, decent person who rises to extraordinary heights through sheer love and loyalty. Without Sam, Frodo would have died in the wilderness, and the quest would have failed. Sam is also the character who gets the final line of the book (\"Well, I'm back\"), signaling that he is the survivor who carries the story forward into the future.\n", + "\n", + "### 3. Aragorn (The Epic/Classic Protagonist)\n", + "If *The Lord of the Rings* were a traditional, classical fantasy, Aragorn would be the undisputed protagonist. \n", + "* **Why he is the protagonist:** He has the classic \"Hero’s Journey\" arc—he is the exiled king living in disguise as a Ranger (Strider) who must overcome his doubts, claim his birthright, lead the armies of men, and defeat the dark lord’s armies. The third volume, *The Return of the King*, is named after him. Aragorn dominates the military and political plotlines of the story.\n", + "\n", + "### Summary\n", + "* **Frodo** is the **plot protagonist** (the story is about his quest).\n", + "* **Sam** is the **thematic protagonist** (he represents the ultimate virtues of the book: loyalty, hope, and humility).\n", + "* **Aragorn** is the **epic protagonist** (he is the traditional hero of the physical battles and political struggle).\n", + "Input: What is the air-speed velocity of a laden swallow?, Output: What do you mean? An African or a European swallow?\n", + "\n", + "***\n", + "\n", + "If you happen to be the Bridgekeeper from *Monty Python and the Holy Grail*, and you don't want to be cast into the Gorge of Eternal Peril, here is the actual breakdown of the math:\n", + "\n", + "### 1. The European Swallow (*Hirundo rustica*)\n", + "In 2003, software updates and avian physics calculations estimated the airspeed velocity of an **unladen European swallow** to be roughly **11 meters per second** (about **24 miles per hour** or 39 kilometers per hour). \n", + "\n", + "### 2. Can it be \"Laden\"?\n", + "A standard European swallow weighs about 20 grams. A standard coconut weighs about 1 pound (454 grams). \n", + "\n", + "According to the laws of aerodynamics, a 20-gram bird cannot maintain lift while carrying an object 22 times its own weight. Therefore, a European swallow **cannot be laden with a coconut**—at least, not individually.\n", + "\n", + "### 3. The \"Two Swallows\" Theory\n", + "As suggested by the castle guards in the film, two swallows could theoretically carry a coconut between them using a strand of creeper. \n", + "\n", + "However, they would have to have it \"simple, under the dorsal guiding feather,\" which opens up a whole new set of aerodynamic complications regarding synchronization and drag. \n", + "\n", + "### 4. The African Swallow\n", + "While the African swallow is non-migratory (and thus unlikely to bring a coconut to England anyway), it is slightly larger. However, it still wouldn't be large enough to carry a coconut solo.\n" + ] + } + ], + "source": [ + "model_handler = GeminiModelHandler(\n", + " model_name = 'gemini-3.5-flash',\n", + " request_fn=generate_from_string,\n", + " api_key=GEMINI_API_KEY,\n", + ")\n", + "\n", + "inputs: list[str] = [\n", + " \"What is 5+2?\",\n", + " \"Who is the protagonist of Lord of the Rings?\",\n", + " \"What is the air-speed velocity of a laden swallow?\"\n", + "]\n", + "\n", + "class PostProcessor(beam.DoFn):\n", + " def process(self, element: PredictionResult) -> Iterable[str]:\n", + " for part in element.inference.parts:\n", + " try:\n", + " output_text = part.text\n", + " yield f\"Input: {element.example}, Output: {output_text}\"\n", + " except Exception as e:\n", + " print(f\"Can't decode inference for element: {element.example}, got {e}\")\n", + " raise e\n", + "\n", + "with beam.Pipeline() as p:\n", + " _ = (p | \"Get prompts\" >> beam.Create(inputs)\n", + " | \"Query Gemini\" >> RunInference(model_handler)\n", + " | \"Process Output\" >> beam.ParDo(PostProcessor())\n", + " | \"Print output\" >> beam.Map(print)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Image Output\n", + "This pipeline code takes text as input and produces generated images as output from Gemini." + ], + "metadata": { + "id": "k__Aja9FPLaf" + } + }, + { + "cell_type": "code", + "source": [ + "model_handler = GeminiModelHandler(\n", + " model_name = 'gemini-3.1-flash-image', # Nano Banana 2\n", + " request_fn=generate_from_string,\n", + " api_key=GEMINI_API_KEY,\n", + ")\n", + "\n", + "class PostProcessor(beam.DoFn):\n", + " def process(self, element: PredictionResult) -> Iterable[Image.Image]:\n", + " try:\n", + " response = element.inference\n", + " for part in response.parts:\n", + " if part.text is not None:\n", + " print(part.text)\n", + " elif part.inline_data is not None:\n", + " image = Image.open(BytesIO(part.inline_data.data))\n", + " yield image\n", + " except Exception as e:\n", + " print(f\"Can't decode inference for element: {element.example}, got {e}\")\n", + " raise e\n", + "\n", + "\n", + "class ImageSink(FileSink):\n", + " def open(self, fh) -> None:\n", + " self._fh = fh\n", + "\n", + " def write(self, record):\n", + " record.save(self._fh, format='PNG')\n", + "\n", + " def flush(self):\n", + " self._fh.flush()\n", + "\n", + "\n", + "inputs: list[str] = [\n", + " \"Create a picture of a pineapple in the sand on the beach.\",\n", + "]\n", + "\n", + "with beam.Pipeline() as p:\n", + " output = (p | \"Get prompts\" >> beam.Create(inputs)\n", + " | \"Query Gemini\" >> RunInference(model_handler)\n", + " | \"Process Output\" >> beam.ParDo(PostProcessor())\n", + " | \"WriteOutput\" >> WriteToFiles(\n", + " path='tmp/',\n", + " file_naming=default_file_naming(\"gemini-image\", \".png\"),\n", + " sink=ImageSink())\n", + " )\n", + " _ = output | \"Print output\" >> beam.Map(print)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "_C-VajNGOVYs", + "outputId": "f44dde53-117b-4d6a-85da-dafdca7839fb" + }, + "execution_count": 4, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "FileResult(file_name='gemini-image-00000-of-00001.png', shard_index=0, total_shards=1, window=GlobalWindow, pane=None, destination=None)\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "Image outputs generally need to be saved somewhere before being viewable, so we will load the file created by the pipeline to render it. Because we only generated one file and specified a naming convention, we know the static file path to our image." + ], + "metadata": { + "id": "RBACanDTSrtB" + } + }, + { + "cell_type": "code", + "source": [ + "with Image.open(\"tmp/gemini-image-00000-of-00001.png\") as image:\n", + " display(image)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 785 + }, + "id": "jsU10BL_QvNL", + "outputId": "be2247aa-96cf-4625-f14c-c668a40734c5" + }, + "execution_count": 5, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "image/png": "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ze09MgqHPnnyZHmxffXKws7O1oMHDwwNupaWrwBVlruj+ATIXrj1HFI5FNNygunQyLQrMUaz9nUyhnomUb4he9T1NH9hLxxkaONDCUDAZcmybLIS6sKMXGVtinOQajW0+exwyT7YSNsSakFpAjVlH5iRYMoQg3OzVhaw/5S0MsaIkeNTm//ffPdTE2inj/NHaNqZ9vDSwrxrFO+8+NLnXn19Zj7rAGm55DGJRi8SssjB5OgaqEg3QiBSmtoIeVAb+alJjAyA+a5ngChjBECijywrSl20o9dEENuElurCtGQkPYQqvDehJtI7lrZdFO3WxE++94FIzne/8912a9hNCT9492NcOdkad/or83B26drW9j4OHes7393p9PY7olkWIGyiEdKeElkcGxJlkD66vDB3IFax31l9/HhqfOwgdKn3M9kZmBWEHnQivavX7aFRNj5hRObLaGNWQYuNI0SvjAzDd5JbTkOw2+vkRL4Af8t88R979i1c9ItB7XVOfdrimA03hAG+GBrouClzVwAURs53jW5KxMHtFv2YlMk7cHLY2bT971x4eW97ffPJw5iMJ876Mk0jkl1eunVldICc7F25fd3o1t0AvNd98Ojx+GS7b2hfnsWaN7sOQO2szczeWFH4bHx48PHaM3eB4gE2zpPHjziKZOonn9796OHm6rq9l32tiaHnb99amp/Z3Fw3tNbo0M2VMTtd8KXGDwjAHOohb2ZocW7m5dvXpGtNTeD04xefu2b3yvz84pPVjRdfefl7P35TFkq73Xrx5vL6+ur81MTs7Ix40GbneG9/W5bE0WFnca5vz73Fb/5oZ+tRZ2f1K6+99OVvfHOvd/Zk9Rf+wT/4hz/96c+w4pe/+rVf+ve+KwPFtR02B7317keyil76wmt/9MMfffTB+99ZWTns2a1NeIopSKnL6Sg4IpTGXDi0bTjOc6iw9qGSeHalYQ5klniwJb6yWlhCChAshDj5b678qRTp2BgXkS1FFXHbSoBrJBledS4A+aUpiixsF3oP94GBDBH+SL2szSbGFLOsHDZ8bV0hrmpMz8a7i/vsV6yA0lgHmAtDKYQXAienwqIunVkw109R8A08vpgvfoLCAKhegEqWsg1i2MESNkswyRgrfKkdoPlTFXZOKB+Rkb2UejI1giHNEuVa85mB8CEFbgzSMJulqtpqroEkxp6Ike26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\n", + "image/jpeg": "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\n" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Use a custom request function\n", + "Writing a custom request function to send arbitrary input to a model is relatively straightforward, as Gemini's API shares commonalities across each model type. Let's try a text-to-speech model." + ], + "metadata": { + "id": "fGHwV5z_Umi6" + } + }, + { + "cell_type": "code", + "source": [ + "tts_config = types.GenerateContentConfig(\n", + " response_modalities=[\"AUDIO\"],\n", + " speech_config=types.SpeechConfig(\n", + " voice_config=types.VoiceConfig(\n", + " prebuilt_voice_config=types.PrebuiltVoiceConfig(\n", + " voice_name='Charon',\n", + " )\n", + " )\n", + " ),\n", + " )\n", + "\n", + "def generate_tts_from_string(\n", + " model_name: str,\n", + " batch: Sequence[str],\n", + " model: genai.Client,\n", + " inference_args: dict[str, any]\n", + "):\n", + " return model.models.generate_content(model=model_name,\n", + " contents=cast(Any, batch),\n", + " config = tts_config)\n", + "\n", + "model_handler = GeminiModelHandler(\n", + " model_name = 'gemini-3.1-flash-tts-preview',\n", + " request_fn=generate_tts_from_string,\n", + " api_key=GEMINI_API_KEY,\n", + ")\n", + "\n", + "class PostProcessor(beam.DoFn):\n", + " def process(self, element: PredictionResult) -> Iterable[Any]:\n", + " try:\n", + " response = element.inference\n", + " for part in response.parts:\n", + " if part.text is not None:\n", + " print(part.text)\n", + " elif part.inline_data is not None:\n", + " yield part.inline_data\n", + " except Exception as e:\n", + " print(f\"Can't decode inference for element: {element.example}, got {e}\")\n", + " raise e\n", + "\n", + "\n", + "class AudioSink(FileSink):\n", + " def open(self, fh) -> None:\n", + " self._fh = fh\n", + "\n", + " def write(self, record):\n", + " with wave.open(self._fh, 'wb') as f:\n", + " f.setnchannels(1)\n", + " f.setsampwidth(2)\n", + " f.setframerate(16000)\n", + " f.writeframes(record.data)\n", + "\n", + " def flush(self):\n", + " self._fh.flush()\n", + "\n", + "inputs: list[str] = [\n", + " \"Say 'Hello, World!'\",\n", + "]\n", + "\n", + "with beam.Pipeline() as p:\n", + " output = (p | \"Get prompts\" >> beam.Create(inputs)\n", + " | \"Query Gemini\" >> RunInference(model_handler)\n", + " | \"Process Output\" >> beam.ParDo(PostProcessor())\n", + " | \"WriteOutput\" >> WriteToFiles(\n", + " path='tmp/',\n", + " file_naming=default_file_naming(\"gemini-audio\", \".wav\"),\n", + " sink=AudioSink())\n", + " )\n", + " _ = output | \"Print output\" >> beam.Map(print)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "1PGdvF5FVEmI", + "outputId": "8c48f6e0-a68d-4f95-dc9f-816984f7d314" + }, + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "FileResult(file_name='gemini-audio-00000-of-00001.wav', shard_index=0, total_shards=1, window=GlobalWindow, pane=None, destination=None)\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "Similar to the image output, we can load the audio output once the pipeline is complete." + ], + "metadata": { + "id": "mvvImVFLdOts" + } + }, + { + "cell_type": "code", + "source": [ + "from IPython.display import Audio\n", + "Audio(filename='tmp/gemini-audio-00000-of-00001.wav', autoplay=False)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 75 + }, + "id": "yOMW_1mZbd-o", + "outputId": "9ad91686-3d01-4512-9d88-c09338530e81" + }, + "execution_count": 7, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {}, + "execution_count": 7 + } + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "include_colab_link": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file From c107070e123f6def96f3baba6039f9d0a03cb210 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Fri, 12 Jun 2026 13:33:52 -0400 Subject: [PATCH 380/490] update agent skills table (#38818) --- .agent/skills/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.agent/skills/README.md b/.agent/skills/README.md index b0388303714d..9b2533fe0695 100644 --- a/.agent/skills/README.md +++ b/.agent/skills/README.md @@ -25,7 +25,9 @@ This directory contains skills that help the agent perform specialized tasks in | Skill | Description | |-------|-------------| +| [adding-new-metadata](adding-new-metadata/SKILL.md) | Guide on how to add and propagate new metadata fields in WindowedValue to avoid metadata loss | | [beam-concepts](beam-concepts/SKILL.md) | Core Beam programming model (PCollections, PTransforms, windowing, triggers) | +| [beam-dofn-modernizer](beam-dofn-modernizer/SKILL.md) | Rewrite Apache Beam DoFn methods to remove legacy ProcessContext/OnTimerContext | | [ci-cd](ci-cd/SKILL.md) | GitHub Actions workflows, debugging CI failures, triggering tests | | [contributing](contributing/SKILL.md) | PR workflow, issue management, code review, release cycles | | [gradle-build](gradle-build/SKILL.md) | Build commands, flags, publishing, troubleshooting | @@ -35,6 +37,7 @@ This directory contains skills that help the agent perform specialized tasks in | [license-compliance](license-compliance/SKILL.md) | Apache 2.0 license headers for all new files | | [python-development](python-development/SKILL.md) | Python SDK environment setup, testing, building pipelines | | [runners](runners/SKILL.md) | Direct, Dataflow, Flink, Spark runner configuration | +| [yaml-development](yaml-development/SKILL.md) | YAML SDK development, environment setup, testing, and key concepts | ## How Skills Work From 2232e5a55b07cebbc6af7590a9a3852d94a4cc22 Mon Sep 17 00:00:00 2001 From: HansMarcus01 Date: Fri, 12 Jun 2026 11:53:00 -0600 Subject: [PATCH 381/490] [Infra] Add beam_viewer and beam_writer roles for GSoC 2026 participant (#38933) * Request read and write permissions on the Beam project for GSoC 2026 * Fix: Placing my username in the correct alphabetical order --- infra/iam/users.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/infra/iam/users.yml b/infra/iam/users.yml index 42b56a6de53d..618420b39b06 100644 --- a/infra/iam/users.yml +++ b/infra/iam/users.yml @@ -486,6 +486,12 @@ - role: roles/cloudfunctions.invoker - role: roles/iam.serviceAccountTokenCreator - role: roles/storage.objectViewer +- username: hansmarcus + email: hansmarcus14@gmail.com + member_type: user + permissions: + - role: projects/apache-beam-testing/roles/beam_viewer + - role: projects/apache-beam-testing/roles/beam_writer - username: harrisonlim email: harrisonlim@google.com member_type: user @@ -1241,4 +1247,4 @@ member_type: user permissions: - role: projects/apache-beam-testing/roles/beam_viewer - - role: projects/apache-beam-testing/roles/beam_writer + - role: projects/apache-beam-testing/roles/beam_writer \ No newline at end of file From ca44d49138f3af13c1b864a80dd251018e3758ec Mon Sep 17 00:00:00 2001 From: tvalentyn Date: Fri, 12 Jun 2026 11:56:16 -0700 Subject: [PATCH 382/490] Add instrumentation for memory profiling in Python SDK (#38853) * Introduce python memory profiling pipeline options. * Add memray and google-pprof dependencies * Parse profiler options in boot.go * Upload profiles to GCS. * Disable the profiler after a timeout reached. * Support --profiler_stop_after_crash * Add on-the-worker postprocessing support. * Generate also the --leaks flamegraph. * Profiler options: use seconds, update defaults. * Set a default profile location from temp location. * Refactor: move profiling pieces into a separate file. * Post-process profiles sequentially. * Simplify tcmalloc preloading to be compatible with ARM versions. * Use consistent mechanisms for periodic invocations of background tasks. * Formatting * Regenerate dependencies. * Update changes.md * Address comments. * Be more resilient to scenarios when postprocessing was interrupted. * Handle postprocessing decision for each report individually. * Remove none-handling since pipeline option validation overwrites empty value later anyway. Users can pass --profile_upload_interval_sec=0 to disable uploads. * Append Job ID and hostname if available in the local temp directory structure. * yapf --- CHANGES.md | 7 +- .../apache_beam/options/pipeline_options.py | 76 ++++ .../options/pipeline_options_test.py | 37 ++ .../options/pipeline_options_validator.py | 2 + sdks/python/container/Dockerfile | 3 + .../base_image_requirements_manual.txt | 2 + sdks/python/container/boot.go | 90 ++++- .../ml/py310/base_image_requirements.txt | 44 ++- .../ml/py310/gpu_image_requirements.txt | 60 ++-- .../ml/py311/base_image_requirements.txt | 44 ++- .../ml/py311/gpu_image_requirements.txt | 60 ++-- .../ml/py312/base_image_requirements.txt | 44 ++- .../ml/py312/gpu_image_requirements.txt | 60 ++-- .../ml/py313/base_image_requirements.txt | 44 ++- sdks/python/container/profiler.go | 336 ++++++++++++++++++ .../py310/base_image_requirements.txt | 41 ++- .../py311/base_image_requirements.txt | 41 ++- .../py312/base_image_requirements.txt | 41 ++- .../py313/base_image_requirements.txt | 41 ++- .../py314/base_image_requirements.txt | 41 ++- 20 files changed, 864 insertions(+), 250 deletions(-) create mode 100644 sdks/python/container/profiler.go diff --git a/CHANGES.md b/CHANGES.md index 3eefa7f43092..7e8e55e3d12c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -59,10 +59,10 @@ ## Highlights -* New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). -* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). +* Python SDK now supports memory profiling with Memray ([#38853](https://github.com/apache/beam/issues/38853)). * (Python) Added [Qdrant](https://qdrant.tech/) VectorDatabaseWriteConfig implementation ([#38141](https://github.com/apache/beam/issues/38141)). + ## I/Os * Support for reading from Delta Lake added (Java) ([#38551](https://github.com/apache/beam/issues/38551)). @@ -71,10 +71,10 @@ ## New Features / Improvements * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). +* (Python) Added instrumentation to support off-the-shelf profiling agents when launching Python SDK Harness ([#38853](https://github.com/apache/beam/issues/38853)). ## Breaking Changes -* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). * (Python) Typehints of dataclass fields are honored during type inferences. To restore the behavior of fallback-to-any, use pipeline option `--exclude_infer_dataclass_field_type` ([#38797](https://github.com/apache/beam/issues/38797)). However fixing forward is recommended. @@ -85,7 +85,6 @@ ## Bugfixes -* Fixed X (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). ## Security Fixes diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index a5b66ce28ac5..c813939d53f1 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -1658,6 +1658,82 @@ def _add_argparse_args(cls, parser): default=1.0, help='A number between 0 and 1 indicating the ratio ' 'of bundles that should be profiled.') + parser.add_argument( + '--profiler_agent', + default=None, + help=( + 'Specifies the profiling agent to launch the SDK worker harness ' + 'with (e.g., "memray", "tcmalloc", or a custom wrapper script/binary).' + )) + parser.add_argument( + '--profiler_extra_arg', + '--profiler_extra_args', + dest='profiler_extra_args', + action=_CommaSeparatedListAction, + default=None, + help= + 'Comma-separated list of extra arguments to pass to the profiler agent.' + ) + parser.add_argument( + '--profiler_extra_env_var', + '--profiler_extra_env_vars', + dest='profiler_extra_env_vars', + action=_CommaSeparatedListAction, + default=None, + help=( + 'Comma-separated list of environment variables required by the profiler agent ' + 'in format "KEY1=VAL1,KEY2=VAL2".')) + parser.add_argument( + '--profile_temp_location', + default=None, + help=( + 'Directory path on the worker where local profiles are saved. ' + 'Defaults to ${semi_persist_dir}/profiles if not specified.')) + parser.add_argument( + '--profile_upload_interval_sec', + type=int, + default=300, + help=( + 'Frequency (in seconds) at which the local profiles are uploaded to GCS. ' + 'Defaults to 300 (5 min).')) + parser.add_argument( + '--profiler_stop_after_sec', + type=int, + default=0, + help=( + 'Time limit (in seconds) for profiling a single process. When exceeded, ' + 'the worker process is restarted without the profiler.')) + parser.add_argument( + '--profiler_stop_after_crash', + action='store_true', + default=False, + help=( + 'If True, the profiling agent won\'t be re-enabled after a worker ' + 'process crash.')) + parser.add_argument( + '--profile_postprocess_interval_sec', + type=int, + default=600, + help=( + 'Frequency (in seconds) at which the local profiles are post-processed ' + 'on-the-fly. Defaults to 600 (10 minutes). Set to 0 to disable.')) + + def validate(self, validator): + errors = [] + if self.profiler_agent: + if self.profile_cpu or self.profile_memory: + errors.append( + '--profiler_agent is mutually exclusive with --profile_cpu ' + 'and --profile_memory.') + + if not self.profile_location: + temp_location = self.view_as(GoogleCloudOptions).temp_location + if temp_location: + self.profile_location = temp_location.rstrip('/') + '/profiles' + _LOGGER.info( + 'Setting --profile_location to %s since profiling is enabled.', + self.profile_location) + return errors class SetupOptions(PipelineOptions): diff --git a/sdks/python/apache_beam/options/pipeline_options_test.py b/sdks/python/apache_beam/options/pipeline_options_test.py index 901f56b99cb6..90ad27d0a39b 100644 --- a/sdks/python/apache_beam/options/pipeline_options_test.py +++ b/sdks/python/apache_beam/options/pipeline_options_test.py @@ -669,6 +669,43 @@ def test_unknown_option_prefix(self): options = PipelineOptions(['--type_check_strictness', 'blahblah']) options.view_as(TypeOptions) + def test_profiling_agent_is_exclusive_with_legacy_profiling_options(self): + options = PipelineOptions(['--profiler_agent=memray']) + validator = PipelineOptionsValidator(options, None) + self.assertEqual(validator.validate(), []) + + options = PipelineOptions(['--profiler_agent=memray', '--profile_cpu']) + validator = PipelineOptionsValidator(options, None) + errors = validator.validate() + self.assertTrue( + any('--profiler_agent is mutually exclusive' in err for err in errors)) + + options = PipelineOptions(['--profiler_agent=memray', '--profile_memory']) + validator = PipelineOptionsValidator(options, None) + errors = validator.validate() + self.assertTrue( + any('--profiler_agent is mutually exclusive' in err for err in errors)) + + def test_profile_location_defaulting_and_opt_out(self): + options = PipelineOptions( + ['--profiler_agent=memray', '--temp_location=gs://bucket/temp']) + validator = PipelineOptionsValidator(options, None) + self.assertEqual(validator.validate(), []) + self.assertEqual( + options.view_as(ProfilingOptions).profile_location, + 'gs://bucket/temp/profiles') + + options = PipelineOptions([ + '--profiler_agent=memray', + '--temp_location=gs://bucket/temp', + '--profile_location=gs://other-bucket/custom_profiles' + ]) + validator = PipelineOptionsValidator(options, None) + self.assertEqual(validator.validate(), []) + self.assertEqual( + options.view_as(ProfilingOptions).profile_location, + 'gs://other-bucket/custom_profiles') + def test_add_experiment(self): options = PipelineOptions([]) options.view_as(DebugOptions).add_experiment('new_experiment') diff --git a/sdks/python/apache_beam/options/pipeline_options_validator.py b/sdks/python/apache_beam/options/pipeline_options_validator.py index 0217363bc9b8..29253329908e 100644 --- a/sdks/python/apache_beam/options/pipeline_options_validator.py +++ b/sdks/python/apache_beam/options/pipeline_options_validator.py @@ -30,6 +30,7 @@ from apache_beam.options.pipeline_options import DebugOptions from apache_beam.options.pipeline_options import GoogleCloudOptions from apache_beam.options.pipeline_options import PortableOptions +from apache_beam.options.pipeline_options import ProfilingOptions from apache_beam.options.pipeline_options import SetupOptions from apache_beam.options.pipeline_options import StandardOptions from apache_beam.options.pipeline_options import TestOptions @@ -55,6 +56,7 @@ class PipelineOptionsValidator(object): DebugOptions, GoogleCloudOptions, PortableOptions, + ProfilingOptions, SetupOptions, StandardOptions, TestOptions, diff --git a/sdks/python/container/Dockerfile b/sdks/python/container/Dockerfile index b0e0b94e7086..fa3414cd332e 100644 --- a/sdks/python/container/Dockerfile +++ b/sdks/python/container/Dockerfile @@ -45,7 +45,10 @@ RUN \ ccache \ # Required for using Beam Python SDK on ARM machines. libgeos-dev \ + # Required for memory profiling with tcmalloc. + google-perftools \ && \ + rm -rf /var/lib/apt/lists/* && \ pip install --upgrade pip setuptools wheel && \ diff --git a/sdks/python/container/base_image_requirements_manual.txt b/sdks/python/container/base_image_requirements_manual.txt index 2a6a11415ee9..a78d993461c0 100644 --- a/sdks/python/container/base_image_requirements_manual.txt +++ b/sdks/python/container/base_image_requirements_manual.txt @@ -38,6 +38,8 @@ google-cloud-profiler;python_version<="3.12" # tests google-api-python-client;python_version<="3.13" guppy3 +# Explicitly pin memray to use a version compatible with postprocessing logic in Beam. +memray==1.19.3 mmh3 # Optimizes execution of some Beam codepaths. TODO: Make it Beam's dependency. nltk # Commonly used for natural language processing. google-crc32c diff --git a/sdks/python/container/boot.go b/sdks/python/container/boot.go index 82d11dc89cb2..958fd46904af 100644 --- a/sdks/python/container/boot.go +++ b/sdks/python/container/boot.go @@ -32,6 +32,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -133,7 +134,17 @@ type PipelineOptionsData struct { } type OptionsData struct { - Experiments []string `json:"experiments"` + Experiments []string `json:"experiments"` + ProfilerAgent string `json:"profiler_agent"` + ProfilerExtraArgs []string `json:"profiler_extra_args"` + ProfilerExtraEnvVars []string `json:"profiler_extra_env_vars"` + ProfileLocation string `json:"profile_location"` + ProfileTempLocation string `json:"profile_temp_location"` + ProfileUploadIntervalSec int `json:"profile_upload_interval_sec"` + ProfilerStopAfterSec int `json:"profiler_stop_after_sec"` + ProfilerStopAfterCrash bool `json:"profiler_stop_after_crash"` + ProfilePostprocessIntervalSec int `json:"profile_postprocess_interval_sec"` + JobId string `json:"jobId,omitempty"` } func getExperiments(options string) []string { @@ -198,6 +209,14 @@ func launchSDKProcess() error { logger.Printf(ctx, "Build isolation disabled when installing packages with pip") } + var opts PipelineOptionsData + if err := json.Unmarshal([]byte(options), &opts); err != nil { + logger.Warnf(ctx, "Failed to unmarshal pipeline options for profiling config: %v", err) + } + + ctx = setupProfilerConfig(ctx, logger, &opts) + startProfilerBackgroundTasks(ctx, logger) + // (2) Retrieve and install the staged packages. // // No log.Fatalf() from here on, otherwise deferred cleanups will not be called! @@ -314,11 +333,6 @@ func launchSDKProcess() error { childPids.mu.Unlock() }() - args := []string{ - "-m", - sdkHarnessEntrypoint, - } - var wg sync.WaitGroup wg.Add(len(workerIds)) for _, workerId := range workerIds { @@ -333,12 +347,68 @@ func launchSDKProcess() error { childPids.mu.Unlock() return } - logger.Printf(ctx, "Executing Python (worker %v): python %v", workerId, strings.Join(args, " ")) - cmd := StartCommandEnv(map[string]string{"WORKER_ID": workerId}, os.Stdin, bufLogger, bufLogger, "python", args...) + + currentProg := "python" + currentArgs := []string{"-m", sdkHarnessEntrypoint} + currentEnv := map[string]string{"WORKER_ID": workerId} + + profilingActive := false + currentProg, currentArgs, currentEnv, profilingActive = maybeWithProfiler( + ctx, logger, workerId, currentProg, currentArgs, currentEnv, + ) + + var envStr string + if len(currentEnv) > 0 { + var envStrings []string + for k, v := range currentEnv { + envStrings = append(envStrings, k+"="+v) + } + slices.Sort(envStrings) + envStr = strings.Join(envStrings, ", ") + } + + logger.Printf(ctx, "Executing Python (%v): %v %v", envStr, currentProg, strings.Join(currentArgs, " ")) + cmd := StartCommandEnv(currentEnv, os.Stdin, bufLogger, bufLogger, currentProg, currentArgs...) childPids.v = append(childPids.v, cmd.Process.Pid) childPids.mu.Unlock() - if err := cmd.Wait(); err != nil { + var timer *time.Timer + var profilingTimedOut atomic.Bool + + pcfg := getProfilerConfig(ctx) + if profilingActive && pcfg.StopAfterSec > 0 { + duration := time.Duration(pcfg.StopAfterSec) * time.Second + timer = time.AfterFunc(duration, func() { + childPids.mu.Lock() + defer childPids.mu.Unlock() + if cmd.Process != nil { + logger.Printf(ctx, "Profiling timeout of %d seconds reached. Sending SIGINT to worker %s", + pcfg.StopAfterSec, workerId) + profilingTimedOut.Store(true) + syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) + } + }) + } + + err := cmd.Wait() + if timer != nil { + timer.Stop() + } + + if err != nil { + if profilingTimedOut.Load() { + stopProfiling(ctx) + bufLogger.FlushAtDebug(ctx) + logger.Printf(ctx, "Python worker %v terminated after profiling timeout. Restarting without profiler.", workerId) + // Error is not counted toward error budget. + continue + } + + if profilingActive && pcfg.StopAfterCrash { + stopProfiling(ctx) + logger.Printf(ctx, "Python worker %v crashed. Disabling profiler on subsequent restarts because --profiler_stop_after_crash is enabled.", workerId) + } + // Retry on fatal errors, like OOMs and segfaults, not just // DoFns throwing exceptions. errorCount += 1 @@ -532,3 +602,5 @@ func logSubmissionEnvDependencies(ctx context.Context, bufLogger *tools.Buffered bufLogger.Printf(ctx, "%s", string(content)) return nil } + + diff --git a/sdks/python/container/ml/py310/base_image_requirements.txt b/sdks/python/container/ml/py310/base_image_requirements.txt index 33b5587dc86b..74419bff9cd9 100644 --- a/sdks/python/container/ml/py310/base_image_requirements.txt +++ b/sdks/python/container/ml/py310/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -34,7 +34,7 @@ async-timeout==5.0.1 attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b7 bs4==0.0.2 build==1.5.0 @@ -57,43 +57,43 @@ exceptiongroup==1.3.1 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -105,7 +105,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -129,9 +129,12 @@ keras==3.12.2 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 +linkify-it-py==2.1.0 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 +memray==1.19.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -158,6 +161,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -195,7 +199,7 @@ requests-mock==1.12.1 rich==15.0.0 rpds-py==0.30.0 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.15.3 scramp==1.4.8 @@ -214,15 +218,17 @@ tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 tomli==2.4.1 torch==2.8.0+cpu -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/ml/py310/gpu_image_requirements.txt b/sdks/python/container/ml/py310/gpu_image_requirements.txt index d909ca3142ee..249a14d8f791 100644 --- a/sdks/python/container/ml/py310/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py310/gpu_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 @@ -36,13 +36,13 @@ async-timeout==5.0.1 attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b7 blake3==1.0.8 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -cbor2==6.1.1 +cbor2==6.1.2 certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 @@ -76,7 +76,7 @@ fastapi-cloud-cli==0.19.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 @@ -84,36 +84,36 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 gguf==0.19.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -125,7 +125,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.16.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -153,14 +153,17 @@ keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 libclang==18.1.1 +linkify-it-py==2.1.0 llguidance==0.7.30 llvmlite==0.44.0 lm-format-enforcer==0.10.12 Markdown==3.10.2 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 -mistral_common==1.11.2 +memray==1.19.3 +mistral_common==1.11.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -225,6 +228,7 @@ partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 prometheus-fastapi-instrumentator==8.0.0 @@ -259,7 +263,7 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-json-logger==4.1.0 -python-multipart==0.0.30 +python-multipart==0.0.32 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 @@ -271,24 +275,24 @@ regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 rich==15.0.0 -rich-toolkit==0.19.10 +rich-toolkit==0.20.1 rignore==0.7.6 rpds-py==0.30.0 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.15.3 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.61.1 +sentry-sdk==2.62.0 setproctitle==1.3.7 setuptools==81.0.0 shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soundfile==0.13.1 +soundfile==0.14.0 soupsieve==2.8.4 soxr==1.1.0 SQLAlchemy==2.0.50 @@ -303,6 +307,7 @@ tensorflow==2.20.0 tensorflow-cpu-aws==2.20.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tiktoken==0.13.0 tokenizers==0.21.4 @@ -310,16 +315,17 @@ tomli==2.4.1 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 triton==3.3.1 -typer==0.26.6 +typer==0.26.7 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 -uvicorn==0.48.0 +uvicorn==0.49.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 diff --git a/sdks/python/container/ml/py311/base_image_requirements.txt b/sdks/python/container/ml/py311/base_image_requirements.txt index a72c2814fd2e..85ae711b2324 100644 --- a/sdks/python/container/ml/py311/base_image_requirements.txt +++ b/sdks/python/container/ml/py311/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -33,7 +33,7 @@ astunparse==1.6.3 attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -55,43 +55,43 @@ envoy_data_plane==1.0.3 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -104,7 +104,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -128,9 +128,12 @@ keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 +linkify-it-py==2.1.0 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 +memray==1.19.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -157,6 +160,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -194,7 +198,7 @@ requests-mock==1.12.1 rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 scramp==1.4.8 @@ -213,14 +217,16 @@ tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 torch==2.8.0+cpu -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/ml/py311/gpu_image_requirements.txt b/sdks/python/container/ml/py311/gpu_image_requirements.txt index 49e997701548..2aa43f9c8fd3 100644 --- a/sdks/python/container/ml/py311/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py311/gpu_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 @@ -35,13 +35,13 @@ astunparse==1.6.3 attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 blake3==1.0.8 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -cbor2==6.1.1 +cbor2==6.1.2 certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 @@ -74,7 +74,7 @@ fastapi-cloud-cli==0.19.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 @@ -82,36 +82,36 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 gguf==0.19.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -124,7 +124,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.16.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -152,14 +152,17 @@ keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 libclang==18.1.1 +linkify-it-py==2.1.0 llguidance==0.7.30 llvmlite==0.44.0 lm-format-enforcer==0.10.12 Markdown==3.10.2 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 -mistral_common==1.11.2 +memray==1.19.3 +mistral_common==1.11.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -224,6 +227,7 @@ partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 prometheus-fastapi-instrumentator==8.0.0 @@ -258,7 +262,7 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-json-logger==4.1.0 -python-multipart==0.0.30 +python-multipart==0.0.32 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 @@ -270,24 +274,24 @@ regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 rich==15.0.0 -rich-toolkit==0.19.10 +rich-toolkit==0.20.1 rignore==0.7.6 rpds-py==2026.5.1 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.61.1 +sentry-sdk==2.62.0 setproctitle==1.3.7 setuptools==81.0.0 shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soundfile==0.13.1 +soundfile==0.14.0 soupsieve==2.8.4 soxr==1.1.0 SQLAlchemy==2.0.50 @@ -302,22 +306,24 @@ tensorflow==2.20.0 tensorflow-cpu-aws==2.20.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tiktoken==0.13.0 tokenizers==0.21.4 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 triton==3.3.1 -typer==0.26.6 +typer==0.26.7 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 -uvicorn==0.48.0 +uvicorn==0.49.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index ad3c161a2c69..2b8283180794 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -32,7 +32,7 @@ asn1crypto==1.5.1 astunparse==1.6.3 attrs==26.1.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -54,43 +54,43 @@ envoy_data_plane==1.0.3 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -103,7 +103,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -126,9 +126,12 @@ keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 +linkify-it-py==2.1.0 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 +memray==1.19.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -155,6 +158,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -192,7 +196,7 @@ requests-mock==1.12.1 rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 scramp==1.4.8 @@ -211,14 +215,16 @@ tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 torch==2.8.0+cpu -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index d5cfda651ff4..c659def93a8c 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 @@ -34,13 +34,13 @@ astor==0.8.1 astunparse==1.6.3 attrs==26.1.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 blake3==1.0.8 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -cbor2==6.1.1 +cbor2==6.1.2 certifi==2026.5.20 cffi==2.0.0 charset-normalizer==3.4.7 @@ -73,7 +73,7 @@ fastapi-cloud-cli==0.19.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 @@ -81,36 +81,36 @@ fsspec==2026.4.0 future==1.0.0 gast==0.7.0 gguf==0.19.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -123,7 +123,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.16.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -150,14 +150,17 @@ keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 lark==1.2.2 libclang==18.1.1 +linkify-it-py==2.1.0 llguidance==0.7.30 llvmlite==0.44.0 lm-format-enforcer==0.10.12 Markdown==3.10.2 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 -mistral_common==1.11.2 +memray==1.19.3 +mistral_common==1.11.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -222,6 +225,7 @@ partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 prometheus-fastapi-instrumentator==8.0.0 @@ -256,7 +260,7 @@ pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.2 python-json-logger==4.1.0 -python-multipart==0.0.30 +python-multipart==0.0.32 python-tds==1.17.1 pytz==2026.2 PyYAML==6.0.3 @@ -268,24 +272,24 @@ regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 rich==15.0.0 -rich-toolkit==0.19.10 +rich-toolkit==0.20.1 rignore==0.7.6 rpds-py==2026.5.1 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 scramp==1.4.8 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.61.1 +sentry-sdk==2.62.0 setproctitle==1.3.7 setuptools==79.0.1 shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -soundfile==0.13.1 +soundfile==0.14.0 soupsieve==2.8.4 soxr==1.1.0 SQLAlchemy==2.0.50 @@ -300,22 +304,24 @@ tensorflow==2.20.0 tensorflow-cpu-aws==2.20.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tiktoken==0.13.0 tokenizers==0.21.4 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 triton==3.3.1 -typer==0.26.6 +typer==0.26.7 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 -uvicorn==0.48.0 +uvicorn==0.49.0 uvloop==0.22.1 virtualenv-clone==0.5.7 vllm==0.10.1.1 diff --git a/sdks/python/container/ml/py313/base_image_requirements.txt b/sdks/python/container/ml/py313/base_image_requirements.txt index 1d129a09fedf..b98bef893b89 100644 --- a/sdks/python/container/ml/py313/base_image_requirements.txt +++ b/sdks/python/container/ml/py313/base_image_requirements.txt @@ -24,7 +24,7 @@ absl-py==2.4.0 aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -32,7 +32,7 @@ asn1crypto==1.5.1 astunparse==1.6.3 attrs==26.1.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -54,42 +54,42 @@ envoy_data_plane==1.0.3 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.0 +filelock==3.29.1 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 fsspec==2026.4.0 future==1.0.0 gast==0.7.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.35 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 -google-cloud-pubsub==2.38.0 +google-cloud-monitoring==2.31.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 +google-genai==2.8.0 google-pasta==0.2.0 -google-resumable-media==2.9.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -102,7 +102,7 @@ guppy3==3.1.7 h11==0.16.0 h2==4.3.0 h5py==3.14.0 -hf-xet==1.5.0 +hf-xet==1.5.1 hpack==4.1.0 httpcore==1.0.9 httplib2==0.31.2 @@ -125,9 +125,12 @@ keras==3.14.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 libclang==18.1.1 +linkify-it-py==2.1.0 markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 mdurl==0.1.2 +memray==1.19.3 ml_dtypes==0.5.4 mmh3==5.2.1 mock==5.2.0 @@ -154,6 +157,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -191,7 +195,7 @@ requests-mock==1.12.1 rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 -safetensors==0.7.0 +safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 scramp==1.4.8 @@ -210,14 +214,16 @@ tensorflow==2.21.0 tensorflow-cpu-aws==2.21.0;platform_machine=="aarch64" termcolor==3.3.0 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 torch==2.8.0+cpu -tqdm==4.67.3 +tqdm==4.68.2 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/profiler.go b/sdks/python/container/profiler.go new file mode 100644 index 000000000000..64211e9fac25 --- /dev/null +++ b/sdks/python/container/profiler.go @@ -0,0 +1,336 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/apache/beam/sdks/v2/go/container/tools" +) + +type profilerConfigKeyType struct{} + +var profilerConfigKey profilerConfigKeyType + +// ProfilerConfig holds all pre-computed profiling parameters. +type ProfilerConfig struct { + Enabled bool + Agent string + ExtraArgs []string + ExtraEnvVars []string + Location string + TempLocation string + BaseTempDir string + StopSentinelPath string + GcsDestPath string + UploadIntervalSec int + StopAfterSec int + StopAfterCrash bool + PostprocessIntervalSec int +} + +// setupProfilerConfig parses PipelineOptionsData and stores a resolved ProfilerConfig in the context. +func setupProfilerConfig(ctx context.Context, logger *tools.Logger, opts *PipelineOptionsData) context.Context { + agent := opts.Options.ProfilerAgent + if agent == "" { + return ctx + } + + baseTempDir := opts.Options.ProfileTempLocation + if baseTempDir == "" { + baseTempDir = filepath.Join(*semiPersistDir, "profiles") + } + + jobId := opts.Options.JobId + if jobId == "" { + jobId = "BEAM_JOB" + } + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "default-worker" + } + + tempLocation := filepath.Join(baseTempDir, jobId, hostname) + sentinelPath := filepath.Join(tempLocation, fmt.Sprintf(".profiler_disengaged_%s_%s", jobId, hostname)) + + var gcsDestPath string + if strings.HasPrefix(opts.Options.ProfileLocation, "gs://") { + gcsDestPath = strings.TrimSuffix(opts.Options.ProfileLocation, "/") + } + + config := &ProfilerConfig{ + Enabled: true, + Agent: agent, + ExtraArgs: opts.Options.ProfilerExtraArgs, + ExtraEnvVars: opts.Options.ProfilerExtraEnvVars, + Location: opts.Options.ProfileLocation, + BaseTempDir: baseTempDir, + TempLocation: tempLocation, + StopSentinelPath: sentinelPath, + GcsDestPath: gcsDestPath, + UploadIntervalSec: opts.Options.ProfileUploadIntervalSec, + StopAfterSec: opts.Options.ProfilerStopAfterSec, + StopAfterCrash: opts.Options.ProfilerStopAfterCrash, + PostprocessIntervalSec: opts.Options.ProfilePostprocessIntervalSec, + } + + return context.WithValue(ctx, profilerConfigKey, config) +} + +// getProfilerConfig extracts the ProfilerConfig from the context. +func getProfilerConfig(ctx context.Context) *ProfilerConfig { + if cfg, ok := ctx.Value(profilerConfigKey).(*ProfilerConfig); ok { + return cfg + } + return nil +} + +// startProfilerBackgroundTasks initializes profiling locations and runs background tasks (GCS sync, post-processing loops) if profiling is enabled. +func startProfilerBackgroundTasks(ctx context.Context, logger *tools.Logger) { + pcfg := getProfilerConfig(ctx) + if pcfg == nil { + return + } + + logger.Printf(ctx, "Worker will be configured with profiler agent enabled.") + logger.Printf(ctx, "ProfilerAgent: %v", pcfg.Agent) + logger.Printf(ctx, "ProfilerExtraArgs: %v", pcfg.ExtraArgs) + logger.Printf(ctx, "ProfilerExtraEnvVars: %v", pcfg.ExtraEnvVars) + logger.Printf(ctx, "ProfileLocation: %v", pcfg.Location) + logger.Printf(ctx, "ProfileTempLocation: %v", pcfg.BaseTempDir) + logger.Printf(ctx, "ProfileUploadIntervalSec: %v", pcfg.UploadIntervalSec) + logger.Printf(ctx, "ProfilerStopAfterSec: %v", pcfg.StopAfterSec) + logger.Printf(ctx, "ProfilerStopAfterCrash: %v", pcfg.StopAfterCrash) + logger.Printf(ctx, "ProfilePostprocessIntervalSec: %v", pcfg.PostprocessIntervalSec) + if err := os.MkdirAll(pcfg.TempLocation, 0755); err != nil { + logger.Warnf(ctx, "Failed to create ProfileTempLocation: %v", err) + } + + if pcfg.GcsDestPath != "" { + if _, err := exec.LookPath("gcloud"); err != nil { + logger.Errorf(ctx, "gcloud is not available, profiles will not be uploaded.") + } else { + if pcfg.UploadIntervalSec > 0 { + go func() { + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(pcfg.UploadIntervalSec) * time.Second): + // TODO(tvalentyn): Consider a periodic cleanup as well to save local disk space. + syncProfilesToGCS(ctx, logger, pcfg.BaseTempDir, pcfg.GcsDestPath) + } + } + }() + } + } + } + + if pcfg.Agent == "memray" { + go postProcessProfilesLoop(ctx, logger, pcfg.TempLocation, pcfg.PostprocessIntervalSec) + } +} + +// maybeWithProfiler builds the execution arguments and environment variables if profiling is enabled and active. +func maybeWithProfiler( + ctx context.Context, + logger *tools.Logger, + workerId string, + currentProg string, + currentArgs []string, + currentEnv map[string]string, +) (string, []string, map[string]string, bool) { + pcfg := getProfilerConfig(ctx) + if pcfg == nil { + return currentProg, currentArgs, currentEnv, false + } + + if _, err := os.Stat(pcfg.StopSentinelPath); err == nil { + return currentProg, currentArgs, currentEnv, false + } + + prog := currentProg + var args []string + // Copy env + env := make(map[string]string) + for k, v := range currentEnv { + env[k] = v + } + + if pcfg.Agent == "memray" { + timeSuffix := time.Now().Format("20060102150405") + memrayFile := filepath.Join(pcfg.TempLocation, fmt.Sprintf("memray-%s-%s.bin", workerId, timeSuffix)) + args = []string{"-m", "memray", "run"} + args = append(args, pcfg.ExtraArgs...) + args = append(args, "-o", memrayFile, "-m", sdkHarnessEntrypoint) + } else if pcfg.Agent == "tcmalloc" { + tcmallocHeapPath := filepath.Join(pcfg.TempLocation, fmt.Sprintf("tcmalloc-%s", workerId)) + existingPreload := os.Getenv("LD_PRELOAD") + if existingPreload != "" { + env["LD_PRELOAD"] = existingPreload + ":libtcmalloc.so.4" + } else { + env["LD_PRELOAD"] = "libtcmalloc.so.4" + } + env["HEAPPROFILE"] = tcmallocHeapPath + args = currentArgs + } else { + prog = pcfg.Agent + args = append(append([]string{}, pcfg.ExtraArgs...), currentProg) + args = append(args, currentArgs...) + } + + for _, envVar := range pcfg.ExtraEnvVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) == 2 { + env[parts[0]] = parts[1] + } else { + logger.Errorf(ctx, "Failed to parse profiler extra environment variable: %v. Expected format KEY=VALUE", envVar) + } + } + + return prog, args, env, true +} + +// stopProfiling creates a dummy file at StopSentinelPath to signal that profiling should stop. +func stopProfiling(ctx context.Context) error { + pcfg := getProfilerConfig(ctx) + if pcfg == nil { + return nil + } + f, err := os.Create(pcfg.StopSentinelPath) + if err == nil { + f.Close() + } + return err +} + +// syncProfilesToGCS uploads newly created local memory profiles to the designated GCS target path using gcloud storage. +func syncProfilesToGCS(ctx context.Context, logger *tools.Logger, localDir, gcsDest string) { + entries, err := os.ReadDir(localDir) + if err != nil || len(entries) == 0 { + return + } + + logger.Printf(ctx, "Syncing profiles from %s to %s", localDir, gcsDest) + + cmd := exec.CommandContext(ctx, "gcloud", "storage", "rsync", "-r", localDir, gcsDest) + if err := cmd.Run(); err != nil { + logger.Warnf(ctx, "Failed to sync profiles to GCS: %v", err) + } else { + logger.Printf(ctx, "Successfully synced profiles to GCS.") + } +} + +// postProcessProfilesLoop runs a background loop that periodically triggers profile post-processing if enabled. +func postProcessProfilesLoop(ctx context.Context, logger *tools.Logger, profilesDir string, intervalSec int) { + if intervalSec <= 0 { + return + } + + for { + runPostProcessingSweep(ctx, logger, profilesDir, intervalSec) + + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(intervalSec) * time.Second): + // Block until the sleep completes before starting the next sweep + } + } +} + +// runPostProcessingSweep scans the profiles directory and launches sequential postprocessing for newly updated profiles. +func runPostProcessingSweep(ctx context.Context, logger *tools.Logger, profilesDir string, intervalSec int) { + files, err := os.ReadDir(profilesDir) + if err != nil { + return + } + + for _, file := range files { + name := file.Name() + if !strings.HasSuffix(name, ".bin") || strings.HasPrefix(name, ".") { + continue + } + + binPath := filepath.Join(profilesDir, name) + binInfo, err := os.Stat(binPath) + if err != nil || binInfo.Size() == 0 { + continue + } + + peakHtml := strings.TrimSuffix(binPath, ".bin") + ".html" + leaksHtml := strings.TrimSuffix(binPath, ".bin") + "_leaks.html" + + filename := filepath.Base(binPath) + peakReportStale := needsProcessing(binInfo, peakHtml) + leakReportStale := needsProcessing(binInfo, leaksHtml) + + if peakReportStale || leakReportStale { + binSizeMb := float64(binInfo.Size()) / (1024 * 1024) + logger.Printf(ctx, "Post-processing profile %s of size %.2f MB", filename, binSizeMb) + } + + // 1. Peak Flamegraph + if peakReportStale { + tmpPath := peakHtml + ".tmp" + cmd1 := exec.CommandContext(ctx, "python", "-m", "memray", "flamegraph", "-f", "-o", tmpPath, binPath) + if err := cmd1.Run(); err != nil { + logger.Warnf(ctx, "Failed to generate peak flamegraph for %s: %v", filename, err) + } else { + if err := os.Rename(tmpPath, peakHtml); err != nil { + logger.Warnf(ctx, "Failed to rename peak flamegraph for %s: %v", filename, err) + } else { + logger.Printf(ctx, "Successfully updated peak flamegraph for %s", filename) + _ = os.Chtimes(peakHtml, binInfo.ModTime(), binInfo.ModTime()) + } + } + } + + // 2. Leaks Flamegraph + if leakReportStale { + tmpPath := leaksHtml + ".tmp" + cmd2 := exec.CommandContext(ctx, "python", "-m", "memray", "flamegraph", "-f", "--leaks", "-o", tmpPath, binPath) + if err := cmd2.Run(); err != nil { + logger.Warnf(ctx, "Failed to generate leaks flamegraph for %s: %v", filename, err) + } else { + if err := os.Rename(tmpPath, leaksHtml); err != nil { + logger.Warnf(ctx, "Failed to rename leaks flamegraph for %s: %v", filename, err) + } else { + logger.Printf(ctx, "Successfully updated leaks flamegraph for %s", filename) + _ = os.Chtimes(leaksHtml, binInfo.ModTime(), binInfo.ModTime()) + } + } + } + } +} + +func needsProcessing(binInfo os.FileInfo, path string) bool { + info, err := os.Stat(path) + if os.IsNotExist(err) { + return true + } + if err != nil { + return true + } + // Don't regenerate when there were no updates to the profile. + return binInfo.ModTime().After(info.ModTime()) +} diff --git a/sdks/python/container/py310/base_image_requirements.txt b/sdks/python/container/py310/base_image_requirements.txt index c44d8ef0bf5c..f3a840c1809e 100644 --- a/sdks/python/container/py310/base_image_requirements.txt +++ b/sdks/python/container/py310/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -32,7 +32,7 @@ async-timeout==5.0.1 attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b7 bs4==0.0.2 build==1.5.0 @@ -58,35 +58,35 @@ fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 -google-resumable-media==2.9.0 +google-genai==2.8.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -117,7 +117,12 @@ jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 +linkify-it-py==2.1.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +memray==1.19.3 mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 @@ -138,6 +143,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -172,6 +178,7 @@ referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 +rich==15.0.0 rpds-py==0.30.0 rsa==4.9.1 scikit-learn==1.7.2 @@ -188,12 +195,14 @@ sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 tomli==2.4.1 -tqdm==4.67.3 +tqdm==4.68.2 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/py311/base_image_requirements.txt b/sdks/python/container/py311/base_image_requirements.txt index 790825a8b2af..68f5e5469390 100644 --- a/sdks/python/container/py311/base_image_requirements.txt +++ b/sdks/python/container/py311/base_image_requirements.txt @@ -23,7 +23,7 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 @@ -31,7 +31,7 @@ asn1crypto==1.5.1 attrs==26.1.0 backports.tarfile==1.2.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -56,35 +56,35 @@ fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 -google-resumable-media==2.9.0 +google-genai==2.8.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -116,7 +116,12 @@ jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 +linkify-it-py==2.1.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +memray==1.19.3 mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 @@ -137,6 +142,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -171,6 +177,7 @@ referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 +rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 @@ -187,11 +194,13 @@ sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.67.3 +tqdm==4.68.2 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/py312/base_image_requirements.txt b/sdks/python/container/py312/base_image_requirements.txt index 151705811fdd..3f4194f20ea4 100644 --- a/sdks/python/container/py312/base_image_requirements.txt +++ b/sdks/python/container/py312/base_image_requirements.txt @@ -23,14 +23,14 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 asn1crypto==1.5.1 attrs==26.1.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -55,35 +55,35 @@ fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 +google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 -google-cloud-pubsub==2.38.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 -google-resumable-media==2.9.0 +google-genai==2.8.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -114,7 +114,12 @@ jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 +linkify-it-py==2.1.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +memray==1.19.3 mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 @@ -135,6 +140,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -169,6 +175,7 @@ referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 +rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 @@ -185,11 +192,13 @@ sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.67.3 +tqdm==4.68.2 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/py313/base_image_requirements.txt b/sdks/python/container/py313/base_image_requirements.txt index 68ad2a9530c2..31ef940c1e20 100644 --- a/sdks/python/container/py313/base_image_requirements.txt +++ b/sdks/python/container/py313/base_image_requirements.txt @@ -23,14 +23,14 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 asn1crypto==1.5.1 attrs==26.1.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -55,34 +55,34 @@ fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.35 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 -google-cloud-pubsub==2.38.0 +google-cloud-monitoring==2.31.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 -google-resumable-media==2.9.0 +google-genai==2.8.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -113,7 +113,12 @@ jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 +linkify-it-py==2.1.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +memray==1.19.3 mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 @@ -134,6 +139,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -168,6 +174,7 @@ referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 +rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 @@ -184,11 +191,13 @@ sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.67.3 +tqdm==4.68.2 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 uritemplate==4.2.0 urllib3==2.7.0 virtualenv-clone==0.5.7 diff --git a/sdks/python/container/py314/base_image_requirements.txt b/sdks/python/container/py314/base_image_requirements.txt index 7b9225c5e68d..2c7fd33996c2 100644 --- a/sdks/python/container/py314/base_image_requirements.txt +++ b/sdks/python/container/py314/base_image_requirements.txt @@ -23,14 +23,14 @@ aiofiles==25.1.0 aiohappyeyeballs==2.6.2 -aiohttp==3.14.0 +aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.13.0 asn1crypto==1.5.1 attrs==26.1.0 beartype==0.22.9 -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 @@ -55,33 +55,33 @@ fasteners==0.20 freezegun==1.5.5 frozenlist==1.8.0 future==1.0.0 -google-api-core==2.30.3 +google-api-core==2.31.0 google-apitools==0.5.35 google-auth==2.53.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.154.0 +google-cloud-aiplatform==1.157.0 google-cloud-bigquery==3.41.0 -google-cloud-bigquery-storage==2.38.0 +google-cloud-bigquery-storage==2.39.0 google-cloud-bigtable==2.38.0 -google-cloud-build==3.36.0 +google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 -google-cloud-datastore==2.24.0 -google-cloud-dlp==3.36.0 +google-cloud-datastore==2.25.0 +google-cloud-dlp==3.37.0 google-cloud-kms==3.13.0 google-cloud-language==2.20.0 -google-cloud-monitoring==2.30.0 -google-cloud-pubsub==2.38.0 +google-cloud-monitoring==2.31.0 +google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 google-cloud-resource-manager==1.17.0 -google-cloud-secret-manager==2.28.0 -google-cloud-spanner==3.66.0 -google-cloud-storage==3.10.1 +google-cloud-secret-manager==2.29.0 +google-cloud-spanner==3.67.0 +google-cloud-storage==3.11.0 google-cloud-videointelligence==2.19.0 google-cloud-vision==3.14.0 google-crc32c==1.8.0 -google-genai==2.7.0 -google-resumable-media==2.9.0 +google-genai==2.8.0 +google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 greenlet==3.5.1 grpc-google-iam-v1==0.14.4 @@ -112,7 +112,12 @@ jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 keyrings.google-artifactregistry-auth==1.1.2 +linkify-it-py==2.1.0 +markdown-it-py==4.2.0 MarkupSafe==3.0.3 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +memray==1.19.3 mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 @@ -133,6 +138,7 @@ parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 pip==26.1.2 +platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 propcache==0.5.2 @@ -167,6 +173,7 @@ referencing==0.37.0 regex==2026.5.9 requests==2.34.2 requests-mock==1.12.1 +rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 @@ -183,11 +190,13 @@ sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 +textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.67.3 +tqdm==4.68.2 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 +uc-micro-py==2.0.0 urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 From 986d1f2c8e171e4e29d654cc4eaded451f8e957f Mon Sep 17 00:00:00 2001 From: innuendo <1178453+hilaryRope@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:14:39 +0200 Subject: [PATCH 383/490] [#38059] Fix GCS glob matching to support ** and / in object names (#38099) * fix GCS filesystem glob matching to handle / in object names and support ** * update CHANGES.md * update CHANGES.md * address comments * Update sdks/go/pkg/beam/io/filesystem/gcs/gcs.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * modify beam_PostCommit_Go.json as required --------- Co-authored-by: corda.ilaria@gmail.com Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> --- .github/trigger_files/beam_PostCommit_Go.json | 2 +- CHANGES.md | 2 + sdks/go/pkg/beam/io/filesystem/gcs/gcs.go | 88 ++++++++++- .../go/pkg/beam/io/filesystem/gcs/gcs_test.go | 146 ++++++++++++++++++ 4 files changed, 231 insertions(+), 7 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Go.json b/.github/trigger_files/beam_PostCommit_Go.json index b73af5e61a43..7ab7bcd9a9c6 100644 --- a/.github/trigger_files/beam_PostCommit_Go.json +++ b/.github/trigger_files/beam_PostCommit_Go.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 2 } diff --git a/CHANGES.md b/CHANGES.md index 7e8e55e3d12c..c70fef0cf8e8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -85,6 +85,8 @@ ## Bugfixes +* Fixed GCS filesystem glob matching to correctly handle `/` in object names and support `**` for recursive matching (Go) ([#38059](https://github.com/apache/beam/issues/38059)). +* Fixed BigQueryEnrichmentHandler batch mode dropping earlier requests when multiple requests share the same enrichment key (Python) ([#38035](https://github.com/apache/beam/issues/38035)). * Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). ## Security Fixes diff --git a/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go b/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go index 73e686381053..8d6ce28559f7 100644 --- a/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go +++ b/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go @@ -21,7 +21,8 @@ import ( "context" "fmt" "io" - "path/filepath" + "regexp" + "strings" "time" "cloud.google.com/go/storage" @@ -38,6 +39,76 @@ const ( projectBillingHook = "beam:go:hook:filesystem:billingproject" ) +// globToRegex translates a glob pattern to a regular expression. +// It differs from filepath.Match in that: +// - / is treated as a regular character (not a separator), since GCS object +// names are flat with / being just another character +// - ** matches any sequence of characters including / (zero or more) +// - **/ matches zero or more path segments (e.g., "" or "dir/" or "dir/subdir/") +// - * matches any sequence of characters except / (zero or more) +// - ? matches any single character except / +// +// This matches the behavior of the Python and Java SDKs. +func globToRegex(pattern string) (*regexp.Regexp, error) { + var result strings.Builder + result.WriteString("^") + + for i := 0; i < len(pattern); i++ { + c := pattern[i] + switch c { + case '*': + // Check for ** (double asterisk) + if i+1 < len(pattern) && pattern[i+1] == '*' { + // Check if followed by / (e.g., "**/" matches zero or more path segments) + if i+2 < len(pattern) && pattern[i+2] == '/' { + // **/ matches "" or "something/" or "a/b/c/" + result.WriteString("(?:.*/)?") + i += 2 // Skip the second * and the / + } else { + // ** at end or before non-slash matches any characters + result.WriteString(".*") + i++ // Skip the second * + } + } else { + result.WriteString("[^/]*") + } + case '?': + result.WriteString("[^/]") + case '[': + // Character class - find the closing bracket + j := i + 1 + if j < len(pattern) && pattern[j] == '!' { + j++ + } + if j < len(pattern) && pattern[j] == ']' { + j++ + } + for j < len(pattern) && pattern[j] != ']' { + j++ + } + if j >= len(pattern) { + return nil, fmt.Errorf("syntax error: unclosed '[' in pattern %q", pattern) + } else { + // Copy the character class, converting ! to ^ for negation + result.WriteByte('[') + content := pattern[i+1 : j] + if len(content) > 0 && content[0] == '!' { + result.WriteByte('^') + content = content[1:] + } + result.WriteString(content) + result.WriteByte(']') + i = j + } + default: + result.WriteString(regexp.QuoteMeta(string(c))) + } + } + + result.WriteString("$") // match end + return regexp.Compile(result.String()) +} + var billingProject string = "" func init() { @@ -107,6 +178,15 @@ func (f *fs) List(ctx context.Context, glob string) ([]string, error) { return nil, err } + // Compile the glob pattern to a regex. We use a custom glob-to-regex + // translation that treats / as a regular character (not a separator), + // since GCS object names are flat. This also supports ** for recursive + // matching, similar to the Java and Python SDKs. + re, err := globToRegex(object) + if err != nil { + return nil, fmt.Errorf("invalid glob pattern %q: %w", object, err) + } + var candidates []string // We handle globs by list all candidates and matching them here. @@ -125,11 +205,7 @@ func (f *fs) List(ctx context.Context, glob string) ([]string, error) { return nil, err } - match, err := filepath.Match(object, obj.Name) - if err != nil { - return nil, err - } - if match { + if re.MatchString(obj.Name) { candidates = append(candidates, obj.Name) } } diff --git a/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go b/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go index 66dee6bb23f6..cd6aab2a2364 100644 --- a/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go +++ b/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go @@ -19,6 +19,7 @@ import ( "context" "io" "sort" + "strings" "testing" "time" @@ -271,6 +272,151 @@ func TestGCS_copy(t *testing.T) { } } +func TestGlobToRegex(t *testing.T) { + tests := []struct { + pattern string + name string + want bool + }{ + // Single * should NOT match / in object names + {"*.txt", "file.txt", true}, + {"*.txt", "dir/file.txt", false}, + {"prefix*", "prefix123", true}, + {"prefix*", "prefix/subdir", false}, + + // ** should match any characters including / + {"**", "file.txt", true}, + {"**", "dir/file.txt", true}, + {"**", "dir/subdir/file.txt", true}, + {"prefix/**", "prefix/file.txt", true}, + {"prefix/**", "prefix/subdir/file.txt", true}, + {"**/file.txt", "file.txt", true}, + {"**/file.txt", "dir/file.txt", true}, + {"**/file.txt", "dir/subdir/file.txt", true}, + + // Mixed patterns + {"dir/*.txt", "dir/file.txt", true}, + {"dir/*.txt", "dir/subdir/file.txt", false}, + {"dir/**/*.txt", "dir/file.txt", true}, + {"dir/**/*.txt", "dir/subdir/file.txt", true}, + {"dir/**/file.txt", "dir/file.txt", true}, + {"dir/**/file.txt", "dir/a/b/c/file.txt", true}, + + // ? should match any single character except / + {"file?.txt", "file1.txt", true}, + {"file?.txt", "file12.txt", false}, + {"file?.txt", "file/.txt", false}, // ? should not cross / + {"dir?file.txt", "dir/file.txt", false}, + + // Character classes + {"file[0-9].txt", "file1.txt", true}, + {"file[0-9].txt", "filea.txt", false}, + {"file[!0-9].txt", "filea.txt", true}, + {"file[!0-9].txt", "file1.txt", false}, + + // Exact match (no wildcards) + {"exact.txt", "exact.txt", true}, + {"exact.txt", "notexact.txt", false}, + + // Regex special characters should be escaped + {"file.txt", "file.txt", true}, + {"file.txt", "fileXtxt", false}, + {"file(1).txt", "file(1).txt", true}, + } + + for _, tt := range tests { + t.Run(tt.pattern+"_"+tt.name, func(t *testing.T) { + re, err := globToRegex(tt.pattern) + if err != nil { + t.Fatalf("globToRegex(%q) error = %v", tt.pattern, err) + } + got := re.MatchString(tt.name) + if got != tt.want { + t.Errorf("globToRegex(%q).MatchString(%q) = %v, want %v", tt.pattern, tt.name, got, tt.want) + } + }) + } +} + +func TestGlobToRegex_errors(t *testing.T) { + tests := []struct { + pattern string + wantErr string + }{ + {"file[abc.txt", "unclosed '['"}, + {"[invalid", "unclosed '['"}, + } + + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + _, err := globToRegex(tt.pattern) + if err == nil { + t.Errorf("globToRegex(%q) expected error containing %q, got nil", tt.pattern, tt.wantErr) + } else if !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("globToRegex(%q) error = %v, want error containing %q", tt.pattern, err, tt.wantErr) + } + }) + } +} + +func TestGCS_listWithSlashesInObjectNames(t *testing.T) { + ctx := context.Background() + bucket := "beamgogcsfilesystemtest" + dirPath := "gs://" + bucket + + // Create server with objects that have / in their names + server := fakestorage.NewServer([]fakestorage.Object{ + {ObjectAttrs: fakestorage.ObjectAttrs{BucketName: bucket, Name: "file.txt"}, Content: []byte("")}, + {ObjectAttrs: fakestorage.ObjectAttrs{BucketName: bucket, Name: "dir/file.txt"}, Content: []byte("")}, + {ObjectAttrs: fakestorage.ObjectAttrs{BucketName: bucket, Name: "dir/subdir/file.txt"}, Content: []byte("")}, + {ObjectAttrs: fakestorage.ObjectAttrs{BucketName: bucket, Name: "other.txt"}, Content: []byte("")}, + }) + t.Cleanup(server.Stop) + c := &fs{client: server.Client()} + + tests := []struct { + glob string + want []string + }{ + // Single * should only match top-level files + {dirPath + "/*.txt", []string{dirPath + "/file.txt", dirPath + "/other.txt"}}, + // ** should match all files recursively + {dirPath + "/**", []string{ + dirPath + "/file.txt", + dirPath + "/dir/file.txt", + dirPath + "/dir/subdir/file.txt", + dirPath + "/other.txt", + }}, + // dir/* should only match immediate children + {dirPath + "/dir/*", []string{dirPath + "/dir/file.txt"}}, + // dir/** should match all descendants + {dirPath + "/dir/**", []string{ + dirPath + "/dir/file.txt", + dirPath + "/dir/subdir/file.txt", + }}, + // Deeply nested ** matching (core scenario from issue #38059) + {dirPath + "/dir/subdir/**", []string{ + dirPath + "/dir/subdir/file.txt", + }}, + } + + for _, tt := range tests { + t.Run(tt.glob, func(t *testing.T) { + got, err := c.List(ctx, tt.glob) + if err != nil { + t.Fatalf("List(%q) error = %v", tt.glob, err) + } + + sort.Strings(got) + sort.Strings(tt.want) + + if !cmp.Equal(got, tt.want) { + t.Errorf("List(%q) = %v, want %v", tt.glob, got, tt.want) + } + }) + } +} + func createFakeGCSServer(tb testing.TB) *fakestorage.Server { tb.Helper() From 619c0635f6d384b9a87df6be67edc0754447b949 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Fri, 12 Jun 2026 17:02:12 -0400 Subject: [PATCH 384/490] SQL Database DDL & Usability Improvements (#38952) * SQL Database DDL & Usability Improvements: Support CREATE/DROP DATABASE and USE short syntax * Address code review feedback: add guard in InMemoryCatalog.dropDatabase and add tests for DROP DATABASE IF EXISTS * Address new code review feedback: optimize InMemoryCatalog, add null check in SqlDdlNodes, and make tests more precise --- .../src/main/codegen/includes/parserImpls.ftl | 4 +- .../sql/impl/parser/SqlDdlNodes.java | 7 ++- .../sql/meta/catalog/InMemoryCatalog.java | 12 +++-- .../sql/BeamSqlCliDatabaseTest.java | 52 +++++++++++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/sdks/java/extensions/sql/src/main/codegen/includes/parserImpls.ftl b/sdks/java/extensions/sql/src/main/codegen/includes/parserImpls.ftl index 94c0161c492c..cb8eec438728 100644 --- a/sdks/java/extensions/sql/src/main/codegen/includes/parserImpls.ftl +++ b/sdks/java/extensions/sql/src/main/codegen/includes/parserImpls.ftl @@ -381,7 +381,7 @@ SqlCreate SqlCreateDatabase(Span s, boolean replace) : } /** - * USE DATABASE ( catalog_name '.' )? database_name + * USE [ DATABASE ] ( catalog_name '.' )? database_name */ SqlCall SqlUseDatabase(Span s, String scope) : { @@ -391,7 +391,7 @@ SqlCall SqlUseDatabase(Span s, String scope) : { s.add(this); } - + [ ] databaseName = CompoundIdentifier() { return new SqlUseDatabase( diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/parser/SqlDdlNodes.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/parser/SqlDdlNodes.java index 6d6be5d5a127..f8d7e6f73851 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/parser/SqlDdlNodes.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/parser/SqlDdlNodes.java @@ -55,7 +55,7 @@ public static SqlNode column( } /** Returns the schema in which to create an object. */ - static Pair schema( + public static Pair schema( CalcitePrepare.Context context, boolean mutable, SqlIdentifier id) { CalciteSchema rootSchema = mutable ? context.getMutableRootSchema() : context.getRootSchema(); @Nullable CalciteSchema schema = null; @@ -72,7 +72,10 @@ static Pair schema( return Pair.of(checkStateNotNull(schema, "Got null sub-schema for path '%s'", path), name(id)); } - private static @Nullable CalciteSchema childSchema(CalciteSchema rootSchema, List path) { + public static @Nullable CalciteSchema childSchema(CalciteSchema rootSchema, List path) { + if (path == null) { + return null; + } @Nullable CalciteSchema schema = rootSchema; for (String p : path) { if (schema == null) { diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java index cdee6c930224..68e80c2340fa 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java @@ -18,7 +18,6 @@ package org.apache.beam.sdk.extensions.sql.meta.catalog; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.util.Collection; import java.util.Collections; @@ -111,13 +110,20 @@ public Collection databases() { @Override public boolean dropDatabase(String database, boolean cascade) { - checkState(!cascade, "%s does not support CASCADE.", getClass().getSimpleName()); + MetaStore metaStore = metaStores.get(database); + if (!cascade && metaStore != null && !metaStore.getTables().isEmpty()) { + throw new IllegalStateException("Database '" + database + "' is not empty."); + } boolean removed = databases.remove(database); + if (!removed) { + return false; + } if (database.equals(currentDatabase)) { currentDatabase = null; } - return removed; + metaStores.remove(database); + return true; } @Override diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlCliDatabaseTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlCliDatabaseTest.java index 588caa78a2b7..54682911fe11 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlCliDatabaseTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlCliDatabaseTest.java @@ -83,6 +83,15 @@ public void testUseDatabase() { assertEquals("my_database2", catalogManager.currentCatalog().currentDatabase()); } + @Test + public void testUseDatabaseWithoutDatabaseKeyword() { + assertEquals(DEFAULT, catalogManager.currentCatalog().currentDatabase()); + cli.execute("CREATE DATABASE my_database"); + assertEquals(DEFAULT, catalogManager.currentCatalog().currentDatabase()); + cli.execute("USE my_database"); + assertEquals("my_database", catalogManager.currentCatalog().currentDatabase()); + } + @Test public void testUseDatabase_doesNotExist() { assertEquals(DEFAULT, catalogManager.currentCatalog().currentDatabase()); @@ -126,6 +135,49 @@ public void testDropDatabase_nonexistent() { cli.execute("DROP DATABASE my_database"); } + @Test + public void testDropDatabase_ifExists_nonexistent() { + assertFalse(catalogManager.currentCatalog().databaseExists("my_database")); + // Should not throw exception + cli.execute("DROP DATABASE IF EXISTS my_database"); + assertFalse(catalogManager.currentCatalog().databaseExists("my_database")); + } + + @Test + public void testDropDatabase_ifExists_exists() { + cli.execute("CREATE DATABASE my_database"); + assertTrue(catalogManager.currentCatalog().databaseExists("my_database")); + cli.execute("DROP DATABASE IF EXISTS my_database"); + assertFalse(catalogManager.currentCatalog().databaseExists("my_database")); + } + + @Test + public void testDropDatabase_notEmpty_restrict() { + cli.execute("CREATE DATABASE db_1"); + cli.execute("USE db_1"); + + TestTableProvider testTableProvider = new TestTableProvider(); + catalogManager.registerTableProvider(testTableProvider); + cli.execute("CREATE EXTERNAL TABLE person(id int, name varchar, age int) TYPE 'test'"); + + thrown.expect(CalciteContextException.class); + thrown.expectMessage("Database 'db_1' is not empty."); + cli.execute("DROP DATABASE db_1"); + } + + @Test + public void testDropDatabase_notEmpty_cascade() { + cli.execute("CREATE DATABASE db_1"); + cli.execute("USE db_1"); + + TestTableProvider testTableProvider = new TestTableProvider(); + catalogManager.registerTableProvider(testTableProvider); + cli.execute("CREATE EXTERNAL TABLE person(id int, name varchar, age int) TYPE 'test'"); + + cli.execute("DROP DATABASE db_1 CASCADE"); + assertFalse(catalogManager.currentCatalog().databaseExists("db_1")); + } + @Test public void testCreateInsertDropTableUsingDefaultDatabase() { Catalog catalog = catalogManager.currentCatalog(); From b81fe12a93ae8044098fa52da438b4243e3be16f Mon Sep 17 00:00:00 2001 From: Aakash Baskaran Date: Sat, 18 Apr 2026 16:22:21 -0400 Subject: [PATCH 385/490] Use add_experiment() instead of experiments.append() in Python Replaces manual list manipulation pattern with DebugOptions.add_experiment() which handles null-init and deduplication internally. Resolves https://github.com/apache/beam/issues/19347 --- .../apache_beam/io/external/xlang_parquetio_test.py | 3 +-- sdks/python/apache_beam/io/iobase_test.py | 9 +-------- sdks/python/apache_beam/pipeline.py | 5 +---- .../apache_beam/runners/dataflow/internal/apiclient.py | 8 ++------ .../runners/portability/fn_api_runner/fn_runner.py | 6 +----- 5 files changed, 6 insertions(+), 25 deletions(-) diff --git a/sdks/python/apache_beam/io/external/xlang_parquetio_test.py b/sdks/python/apache_beam/io/external/xlang_parquetio_test.py index b4074d156ce7..9b70db21571e 100644 --- a/sdks/python/apache_beam/io/external/xlang_parquetio_test.py +++ b/sdks/python/apache_beam/io/external/xlang_parquetio_test.py @@ -51,8 +51,7 @@ def test_xlang_parquetio_write(self): address = 'localhost:%s' % port try: with TestPipeline() as p: - p.get_pipeline_options().view_as(DebugOptions).experiments.append( - 'jar_packages=' + expansion_jar) + p.get_pipeline_options().view_as(DebugOptions).add_experiment('jar_packages=' + expansion_jar) p.not_use_test_runner_api = True _ = p \ | beam.Create([ diff --git a/sdks/python/apache_beam/io/iobase_test.py b/sdks/python/apache_beam/io/iobase_test.py index eb9617cfae34..c3b0be8e781d 100644 --- a/sdks/python/apache_beam/io/iobase_test.py +++ b/sdks/python/apache_beam/io/iobase_test.py @@ -196,14 +196,7 @@ def test_try_split_with_any_exception(self): class UseSdfBoundedSourcesTests(unittest.TestCase): def _run_sdf_wrapper_pipeline(self, source, expected_values): with beam.Pipeline() as p: - experiments = (p._options.view_as(DebugOptions).experiments or []) - - # Setup experiment option to enable using SDFBoundedSourceWrapper - if 'beam_fn_api' not in experiments: - # Required so mocking below doesn't mock Create used in assert_that. - experiments.append('beam_fn_api') - - p._options.view_as(DebugOptions).experiments = experiments + p._options.view_as(DebugOptions).add_experiment('beam_fn_api') actual = p | beam.io.Read(source) assert_that(actual, equal_to(expected_values)) diff --git a/sdks/python/apache_beam/pipeline.py b/sdks/python/apache_beam/pipeline.py index 750868f7443a..5d92f9ac2b30 100644 --- a/sdks/python/apache_beam/pipeline.py +++ b/sdks/python/apache_beam/pipeline.py @@ -232,10 +232,7 @@ def __init__( # set default experiments for portable runners # (needs to occur prior to pipeline construction) if runner.is_fnapi_compatible(): - experiments = (self._options.view_as(DebugOptions).experiments or []) - if not 'beam_fn_api' in experiments: - experiments.append('beam_fn_api') - self._options.view_as(DebugOptions).experiments = experiments + self._options.view_as(DebugOptions).add_experiment('beam_fn_api') self.local_tempdir = tempfile.mkdtemp(prefix='beam-pipeline-temp') diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 4755aea0bc2c..92d00dd17707 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -149,16 +149,12 @@ def __init__( self.proto.version = version # TODO: Use enumerated type instead of strings for job types. if job_type.startswith('FNAPI_'): - self.debug_options.experiments = self.debug_options.experiments or [] - - debug_options_experiments = self.debug_options.experiments # Add use_multiple_sdk_containers flag if it's not already present. Do not # add the flag if 'no_use_multiple_sdk_containers' is present. # TODO: Cleanup use_multiple_sdk_containers once we deprecate Python SDK # till version 2.4. - if ('use_multiple_sdk_containers' not in debug_options_experiments and - 'no_use_multiple_sdk_containers' not in debug_options_experiments): - debug_options_experiments.append('use_multiple_sdk_containers') + if ('no_use_multiple_sdk_containers' not in (self.debug_options.experiments or [])): + self.debug_options.add_experiment('use_multiple_sdk_containers') # FlexRS if self.google_cloud_options.flexrs_goal == 'COST_OPTIMIZED': self.proto.flex_resource_scheduling_goal = ( diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py index 32997441a48e..37138e40f8b4 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner.py @@ -141,11 +141,7 @@ def run_pipeline( RuntimeValueProvider.set_runtime_options({}) # Setup "beam_fn_api" experiment options if lacked. - experiments = ( - options.view_as(pipeline_options.DebugOptions).experiments or []) - if not 'beam_fn_api' in experiments: - experiments.append('beam_fn_api') - options.view_as(pipeline_options.DebugOptions).experiments = experiments + options.view_as(pipeline_options.DebugOptions).add_experiment('beam_fn_api') # This is sometimes needed if type checking is disabled # to enforce that the inputs (and outputs) of GroupByKey operations From 0e3087dc79958b0ab3c1812b75a70db88ea62a53 Mon Sep 17 00:00:00 2001 From: Aakash Baskaran Date: Tue, 21 Apr 2026 01:07:34 -0400 Subject: [PATCH 386/490] Use ExperimentalOptions.addExperiment() instead of get/set pattern in Java Replaces getExperiments()/modify/setExperiments() boilerplate with ExperimentalOptions.addExperiment() which handles null-init and deduplication. Resolves https://github.com/apache/beam/issues/19347 --- .../dataflow/DataflowPipelineTranslator.java | 16 +---- .../beam/runners/dataflow/DataflowRunner.java | 59 +++++-------------- .../client/grpc/GrpcWindmillServer.java | 11 ++-- 3 files changed, 23 insertions(+), 63 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index 4016f31a5475..f66185c47941 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -69,6 +69,7 @@ import org.apache.beam.sdk.coders.IterableCoder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.runners.AppliedPTransform; @@ -414,19 +415,8 @@ public Job translate(List packages) { // back end as well. If streaming engine is not enabled make sure the experiments are also // not enabled. if (options.isEnableStreamingEngine()) { - List experiments = options.getExperiments(); - if (experiments == null) { - experiments = new ArrayList(); - } else { - experiments = new ArrayList(experiments); - } - if (!experiments.contains(GcpOptions.STREAMING_ENGINE_EXPERIMENT)) { - experiments.add(GcpOptions.STREAMING_ENGINE_EXPERIMENT); - } - if (!experiments.contains(GcpOptions.WINDMILL_SERVICE_EXPERIMENT)) { - experiments.add(GcpOptions.WINDMILL_SERVICE_EXPERIMENT); - } - options.setExperiments(experiments); + ExperimentalOptions.addExperiment(options, GcpOptions.STREAMING_ENGINE_EXPERIMENT); + ExperimentalOptions.addExperiment(options, GcpOptions.WINDMILL_SERVICE_EXPERIMENT); } else { List experiments = options.getExperiments(); if (experiments != null) { diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 011f60f4fd14..d37285f117c4 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1246,13 +1246,11 @@ public DataflowPipelineJob run(Pipeline pipeline) { // Multi-language pipelines and pipelines that include upgrades should automatically be upgraded // to Dataflow Portable Runner. if (DataflowRunner.isMultiLanguagePipeline(pipeline) || includesTransformUpgrades(pipeline)) { - if (!useUnifiedWorker(options)) { - List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); + if (!firstNonNull(options.getExperiments(), Collections.emptyList()).contains("use_runner_v2")) { LOG.info( "Automatically enabling Dataflow Portable Runner since the pipeline used cross-language" + " transforms or pipeline needed a transform upgrade."); - options.setExperiments( - ImmutableList.builder().addAll(experiments).add("use_runner_v2").build()); + ExperimentalOptions.addExperiment(options, "use_runner_v2"); } } if (useUnifiedWorker(options)) { @@ -1264,21 +1262,10 @@ public DataflowPipelineJob run(Pipeline pipeline) { throw new IllegalArgumentException( "Dataflow Portable Runner both disabled and enabled: at least one of ['enable_portable_runner', 'beam_fn_api', 'use_unified_worker', 'use_runner_v2', 'use_portable_job_submission'] is set and also one of ['enable_streaming_java_runner', 'disable_portable_runner', 'disable_runner_v2', 'disable_runner_v2_until_2023', 'disable_prime_runner_v2'] is set."); } - List experiments = - new ArrayList<>(options.getExperiments()); // non-null if useUnifiedWorker is true - if (!experiments.contains("use_runner_v2")) { - experiments.add("use_runner_v2"); - } - if (!experiments.contains("use_unified_worker")) { - experiments.add("use_unified_worker"); - } - if (!experiments.contains("beam_fn_api")) { - experiments.add("beam_fn_api"); - } - if (!experiments.contains("use_portable_job_submission")) { - experiments.add("use_portable_job_submission"); - } - options.setExperiments(ImmutableList.copyOf(experiments)); + ExperimentalOptions.addExperiment(options, "use_runner_v2"); + ExperimentalOptions.addExperiment(options, "use_unified_worker"); + ExperimentalOptions.addExperiment(options, "beam_fn_api"); + ExperimentalOptions.addExperiment(options, "use_portable_job_submission"); // Ensure that logging via the FnApi is enabled options.as(SdkHarnessOptions.class).setEnableLogViaFnApi(true); } @@ -1305,19 +1292,16 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.setStreaming(true); { - List experiments = - options.getExperiments() == null - ? new ArrayList<>() - : new ArrayList<>(options.getExperiments()); // Experiment marking that the harness supports tag encoding v2 // Backend will enable tag encoding v2 only if the harness supports it. - experiments.add("streaming_engine_state_tag_encoding_v2_supported"); + ExperimentalOptions.addExperiment( + options, "streaming_engine_state_tag_encoding_v2_supported"); // Experiment requesting tag encoding v2 on new jobs starting with 2.75.0. During job // updates old job's tag encoding version is carried over by the backend. if (!StreamingOptions.updateCompatibilityVersionLessThan(options, "2.75.0")) { - experiments.add("enable_streaming_engine_state_tag_encoding_v2"); + ExperimentalOptions.addExperiment( + options, "enable_streaming_engine_state_tag_encoding_v2"); } - options.setExperiments(ImmutableList.copyOf(experiments)); } if (useUnifiedWorker(options)) { @@ -1443,15 +1427,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { pipeline, dataflowV1PipelineProto, dataflowV1Components, this, packages); if (!isNullOrEmpty(dataflowOptions.getDataflowWorkerJar()) && !useUnifiedWorker(options)) { - List experiments = - firstNonNull(dataflowOptions.getExperiments(), Collections.emptyList()); - if (!experiments.contains("use_staged_dataflow_worker_jar")) { - dataflowOptions.setExperiments( - ImmutableList.builder() - .addAll(experiments) - .add("use_staged_dataflow_worker_jar") - .build()); - } + ExperimentalOptions.addExperiment(options, "use_staged_dataflow_worker_jar"); } Job newJob = jobSpecification.getJob(); @@ -1510,11 +1486,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { .collect(Collectors.toList()); if (minCpuFlags.isEmpty()) { - dataflowOptions.setExperiments( - ImmutableList.builder() - .addAll(experiments) - .add("min_cpu_platform=" + dataflowOptions.getMinCpuPlatform()) - .build()); + ExperimentalOptions.addExperiment(dataflowOptions, "min_cpu_platform=" + dataflowOptions.getMinCpuPlatform()); } else { LOG.warn( "Flag min_cpu_platform is defined in both top level PipelineOption, " @@ -1551,11 +1523,8 @@ public DataflowPipelineJob run(Pipeline pipeline) { byte[] jobGraphBytes = DataflowPipelineTranslator.jobToString(newJob).getBytes(UTF_8); int jobGraphByteSize = jobGraphBytes.length; if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES - && !hasExperiment(options, "upload_graph") && !useUnifiedWorker(options)) { - List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); - options.setExperiments( - ImmutableList.builder().addAll(experiments).add("upload_graph").build()); + ExperimentalOptions.addExperiment(options, "upload_graph"); LOG.info( "The job graph size ({} in bytes) is larger than {}. Automatically add " + "the upload_graph option to experiments.", @@ -1563,7 +1532,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { CREATE_JOB_REQUEST_LIMIT_BYTES); } - if (hasExperiment(options, "upload_graph") && useUnifiedWorker(options)) { + if (useUnifiedWorker(options)) { ArrayList experiments = new ArrayList<>(options.getExperiments()); while (experiments.remove("upload_graph")) {} options.setExperiments(experiments); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java index 4d5acdc8071e..2a7823e69091 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java @@ -55,6 +55,7 @@ import org.apache.beam.runners.dataflow.worker.windmill.client.grpc.stubs.WindmillStubFactoryFactory; import org.apache.beam.runners.dataflow.worker.windmill.work.WorkItemReceiver; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.util.BackOff; import org.apache.beam.sdk.util.BackOffUtils; @@ -113,13 +114,13 @@ private static DataflowWorkerHarnessOptions testOptions( options.setProject("project"); options.setJobId("job"); options.setWorkerId("worker"); - List experiments = - options.getExperiments() == null ? new ArrayList<>() : options.getExperiments(); + if (enableStreamingEngine) { - experiments.add(GcpOptions.STREAMING_ENGINE_EXPERIMENT); + ExperimentalOptions.addExperiment(options, GcpOptions.STREAMING_ENGINE_EXPERIMENT); + } + for (String experiment : additionalExperiments) { + ExperimentalOptions.addExperiment(options, experiment); } - experiments.addAll(additionalExperiments); - options.setExperiments(experiments); options.setWindmillServiceStreamingRpcBatchLimit(Integer.MAX_VALUE); options.setWindmillServiceStreamingRpcHealthCheckPeriodMs(NO_HEALTH_CHECK); From 008483599d73fadb83d35bdc23140d784a38f643 Mon Sep 17 00:00:00 2001 From: Aakash Baskaran Date: Tue, 21 Apr 2026 23:12:50 -0400 Subject: [PATCH 387/490] Fix upload_graph condition regression and formatting issues --- .../apache/beam/runners/dataflow/DataflowRunner.java | 11 ++++++----- .../apache_beam/io/external/xlang_parquetio_test.py | 3 ++- .../runners/dataflow/internal/apiclient.py | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index d37285f117c4..8213f0a987c2 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1246,7 +1246,8 @@ public DataflowPipelineJob run(Pipeline pipeline) { // Multi-language pipelines and pipelines that include upgrades should automatically be upgraded // to Dataflow Portable Runner. if (DataflowRunner.isMultiLanguagePipeline(pipeline) || includesTransformUpgrades(pipeline)) { - if (!firstNonNull(options.getExperiments(), Collections.emptyList()).contains("use_runner_v2")) { + if (!firstNonNull(options.getExperiments(), Collections.emptyList()) + .contains("use_runner_v2")) { LOG.info( "Automatically enabling Dataflow Portable Runner since the pipeline used cross-language" + " transforms or pipeline needed a transform upgrade."); @@ -1486,7 +1487,8 @@ public DataflowPipelineJob run(Pipeline pipeline) { .collect(Collectors.toList()); if (minCpuFlags.isEmpty()) { - ExperimentalOptions.addExperiment(dataflowOptions, "min_cpu_platform=" + dataflowOptions.getMinCpuPlatform()); + ExperimentalOptions.addExperiment( + dataflowOptions, "min_cpu_platform=" + dataflowOptions.getMinCpuPlatform()); } else { LOG.warn( "Flag min_cpu_platform is defined in both top level PipelineOption, " @@ -1522,8 +1524,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { // enable upload_graph when the graph is too large byte[] jobGraphBytes = DataflowPipelineTranslator.jobToString(newJob).getBytes(UTF_8); int jobGraphByteSize = jobGraphBytes.length; - if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES - && !useUnifiedWorker(options)) { + if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES && !useUnifiedWorker(options)) { ExperimentalOptions.addExperiment(options, "upload_graph"); LOG.info( "The job graph size ({} in bytes) is larger than {}. Automatically add " @@ -1532,7 +1533,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { CREATE_JOB_REQUEST_LIMIT_BYTES); } - if (useUnifiedWorker(options)) { + if (hasExperiment(options, "upload_graph") && useUnifiedWorker(options)) { ArrayList experiments = new ArrayList<>(options.getExperiments()); while (experiments.remove("upload_graph")) {} options.setExperiments(experiments); diff --git a/sdks/python/apache_beam/io/external/xlang_parquetio_test.py b/sdks/python/apache_beam/io/external/xlang_parquetio_test.py index 9b70db21571e..9ae8f2e90f99 100644 --- a/sdks/python/apache_beam/io/external/xlang_parquetio_test.py +++ b/sdks/python/apache_beam/io/external/xlang_parquetio_test.py @@ -51,7 +51,8 @@ def test_xlang_parquetio_write(self): address = 'localhost:%s' % port try: with TestPipeline() as p: - p.get_pipeline_options().view_as(DebugOptions).add_experiment('jar_packages=' + expansion_jar) + p.get_pipeline_options().view_as(DebugOptions).add_experiment( + 'jar_packages=' + expansion_jar) p.not_use_test_runner_api = True _ = p \ | beam.Create([ diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py index 92d00dd17707..5f6b37d4a16b 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py @@ -153,7 +153,8 @@ def __init__( # add the flag if 'no_use_multiple_sdk_containers' is present. # TODO: Cleanup use_multiple_sdk_containers once we deprecate Python SDK # till version 2.4. - if ('no_use_multiple_sdk_containers' not in (self.debug_options.experiments or [])): + if ('no_use_multiple_sdk_containers' + not in (self.debug_options.experiments or [])): self.debug_options.add_experiment('use_multiple_sdk_containers') # FlexRS if self.google_cloud_options.flexrs_goal == 'COST_OPTIMIZED': From ae9b589bc8fa7111e447ac86595b6c6bf46105c5 Mon Sep 17 00:00:00 2001 From: Aakash Baskaran Date: Mon, 27 Apr 2026 19:16:38 -0400 Subject: [PATCH 388/490] Copy experiments list to mutable ArrayList before addExperiment() calls --- .../dataflow/DataflowPipelineTranslator.java | 4 ++++ .../beam/runners/dataflow/DataflowRunner.java | 18 ++++++++++++------ .../client/grpc/GrpcWindmillServer.java | 4 ++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index f66185c47941..c64bd4f535d6 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -403,6 +403,10 @@ public Translator(Pipeline pipeline, DataflowRunner runner, SdkComponents sdkCom * @return a Job definition filled in with the type of job, the environment, and the job steps. */ public Job translate(List packages) { + // Ensure the experiments list is mutable before any experiments are added. + if (options.getExperiments() != null) { + options.setExperiments(new ArrayList<>(options.getExperiments())); + } job.setName(options.getJobName().toLowerCase()); Environment environment = new Environment(); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 8213f0a987c2..659bc22ac06c 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1243,15 +1243,21 @@ private static boolean includesTransformUpgrades(Pipeline pipeline) { @SuppressWarnings("Slf4jFormatShouldBeConst") @Override public DataflowPipelineJob run(Pipeline pipeline) { + // Ensure the experiments list is mutable before any experiments are added. + if (options.getExperiments() != null) { + options.setExperiments(new ArrayList<>(options.getExperiments())); + } // Multi-language pipelines and pipelines that include upgrades should automatically be upgraded // to Dataflow Portable Runner. if (DataflowRunner.isMultiLanguagePipeline(pipeline) || includesTransformUpgrades(pipeline)) { - if (!firstNonNull(options.getExperiments(), Collections.emptyList()) - .contains("use_runner_v2")) { - LOG.info( - "Automatically enabling Dataflow Portable Runner since the pipeline used cross-language" - + " transforms or pipeline needed a transform upgrade."); - ExperimentalOptions.addExperiment(options, "use_runner_v2"); + if (!useUnifiedWorker(options)) { + if (!firstNonNull(options.getExperiments(), Collections.emptyList()) + .contains("use_runner_v2")) { + LOG.info( + "Automatically enabling Dataflow Portable Runner since the pipeline used cross-language" + + " transforms or pipeline needed a transform upgrade."); + ExperimentalOptions.addExperiment(options, "use_runner_v2"); + } } } if (useUnifiedWorker(options)) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java index 2a7823e69091..550011fa2a45 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java @@ -111,6 +111,10 @@ private static DataflowWorkerHarnessOptions testOptions( boolean enableStreamingEngine, List additionalExperiments) { DataflowWorkerHarnessOptions options = PipelineOptionsFactory.create().as(DataflowWorkerHarnessOptions.class); + // Ensure the experiments list is mutable before any experiments are added. + if (options.getExperiments() != null) { + options.setExperiments(new ArrayList<>(options.getExperiments())); + } options.setProject("project"); options.setJobId("job"); options.setWorkerId("worker"); From 45699f5e8e1d93dd9e4548355f1ca94cfbdd53fe Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Sat, 13 Jun 2026 20:55:05 +0200 Subject: [PATCH 389/490] [mqtt] Add portable MqttIO Read/Write transforms for batch and streaming (revives #32385) (#38493) * [mqtt] Add SchemaTransform providers for MqttIO Read/Write Adds MqttReadSchemaTransformProvider and MqttWriteSchemaTransformProvider so MqttIO can be used through the portable SchemaTransform API and exposed as cross-language transforms. Decorates MqttIO.ConnectionConfiguration with @DefaultSchema(AutoValueSchema.class) and @SchemaFieldDescription so the config round-trips through Beam Schemas. Both batch and streaming are supported on the read side: omitting maxNumRecords/maxReadTimeSeconds yields an unbounded (streaming) read, while setting either bounds it to a batch read. The provider descriptions document this and note that streaming requires a portable streaming runner (e.g. Prism, Flink, Dataflow); the legacy local Python DirectRunner does not execute portable streaming cross-language reads. Tests cover read-with-timeout-no-data, an unbounded streaming read (publish/collect/cancel), and a write-then-read round trip against an embedded ActiveMQ broker. Revives the approved diff from PR #32385 (ahmedabu98, twosom) and adapts it to the post-#32668 generic API (MqttIO.Read / MqttIO.Write). * [mqtt] Add messaging expansion service and wire MqttIO into Python xlang Adds a new :sdks:java:io:messaging-expansion-service module that serves messaging IOs (MQTT for now, with room for JMS/Solace later) instead of adding MqttIO to the shared :sdks:java:io:expansion-service, per review feedback from @Abacn and @chamikaramj. Registers MqttIO's SchemaTransforms in standard_expansion_services.yaml under the new service with kafka-style names (ReadFromMqtt / WriteToMqtt), skipping the core SchemaTransforms it bundles transitively (those are generated from the Java IO expansion service). Regenerates standard_external_transforms.yaml so the generated Python wrappers are served by the messaging expansion service, and registers the new target in the generateExternalTransformsConfig task and the xlang wrapper-validation list. The CHANGES.md announcement is deferred to the follow-up PR that sets up the Xlang Messaging PostCommit, per review feedback. * [expansion-service] Remove obsolete upToDateWhen workaround outputs.upToDateWhen { false } in the shadowJar block was a workaround for a corrupted gradle cache and is no longer needed (review feedback on PR #38493). --- sdks/java/io/expansion-service/build.gradle | 1 - .../messaging-expansion-service/build.gradle | 52 ++++ .../org/apache/beam/sdk/io/mqtt/MqttIO.java | 7 + .../mqtt/MqttReadSchemaTransformProvider.java | 150 +++++++++++ .../MqttWriteSchemaTransformProvider.java | 132 +++++++++ .../mqtt/MqttSchemaTransformProviderTest.java | 252 ++++++++++++++++++ sdks/python/build.gradle | 1 + sdks/python/test-suites/xlang/build.gradle | 3 +- sdks/standard_expansion_services.yaml | 15 ++ sdks/standard_external_transforms.yaml | 57 +++- settings.gradle.kts | 1 + 11 files changed, 668 insertions(+), 3 deletions(-) create mode 100644 sdks/java/io/messaging-expansion-service/build.gradle create mode 100644 sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java create mode 100644 sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java create mode 100644 sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 60ef89ed223b..70a3fce538b6 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -72,7 +72,6 @@ shadowJar { attributes(["Multi-Release": true]) } mergeServiceFiles() - outputs.upToDateWhen { false } } description = "Apache Beam :: SDKs :: Java :: IO :: Expansion Service" diff --git a/sdks/java/io/messaging-expansion-service/build.gradle b/sdks/java/io/messaging-expansion-service/build.gradle new file mode 100644 index 000000000000..6cdf67b86d6e --- /dev/null +++ b/sdks/java/io/messaging-expansion-service/build.gradle @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'org.apache.beam.module' +apply plugin: 'application' +mainClassName = "org.apache.beam.sdk.expansion.service.ExpansionService" + +applyJavaNature( + automaticModuleName: 'org.apache.beam.sdk.io.messaging.expansion.service', + exportJavadoc: false, + validateShadowJar: false, + shadowClosure: {}, +) + +shadowJar { + manifest { + attributes(["Multi-Release": true]) + } + mergeServiceFiles() +} + +description = "Apache Beam :: SDKs :: Java :: IO :: Messaging Expansion Service" +ext.summary = "Expansion service serving messaging IOs (e.g. MQTT)" + +dependencies { + implementation project(":sdks:java:expansion-service") + permitUnusedDeclared project(":sdks:java:expansion-service") // BEAM-11761 + implementation project(":sdks:java:io:mqtt") + permitUnusedDeclared project(":sdks:java:io:mqtt") // BEAM-11761 + runtimeOnly library.java.slf4j_jdk14 +} + +task runExpansionService (type: JavaExec) { + mainClass = "org.apache.beam.sdk.expansion.service.ExpansionService" + classpath = sourceSets.test.runtimeClasspath + args = [project.findProperty("constructionService.port") ?: "8097"] +} diff --git a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java index 78876eb6534d..72449c0697ae 100644 --- a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java +++ b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java @@ -37,7 +37,10 @@ import org.apache.beam.sdk.coders.SerializableCoder; import org.apache.beam.sdk.io.UnboundedSource; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.schemas.AutoValueSchema; import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -205,13 +208,17 @@ public static Write dynamicWrite() { private MqttIO() {} /** A POJO describing a MQTT connection. */ + @DefaultSchema(AutoValueSchema.class) @AutoValue public abstract static class ConnectionConfiguration implements Serializable { + @SchemaFieldDescription("The MQTT broker URI.") abstract String getServerUri(); + @SchemaFieldDescription("The MQTT topic pattern.") abstract @Nullable String getTopic(); + @SchemaFieldDescription("The client ID prefix, which is used to construct a unique client ID.") abstract @Nullable String getClientId(); abstract @Nullable String getUsername(); diff --git a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java new file mode 100644 index 000000000000..b83d9bba9f48 --- /dev/null +++ b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mqtt; + +import static org.apache.beam.sdk.io.mqtt.MqttIO.ConnectionConfiguration; +import static org.apache.beam.sdk.io.mqtt.MqttReadSchemaTransformProvider.ReadConfiguration; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import javax.annotation.Nullable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.joda.time.Duration; + +@AutoService(SchemaTransformProvider.class) +public class MqttReadSchemaTransformProvider + extends TypedSchemaTransformProvider { + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class ReadConfiguration implements Serializable { + public static Builder builder() { + return new AutoValue_MqttReadSchemaTransformProvider_ReadConfiguration.Builder(); + } + + @SchemaFieldDescription("Configuration options to set up the MQTT connection.") + public abstract ConnectionConfiguration getConnectionConfiguration(); + + @SchemaFieldDescription( + "The max number of records to receive. Setting this will result in a bounded PCollection.") + @Nullable + public abstract Long getMaxNumRecords(); + + @SchemaFieldDescription( + "The maximum time for this source to read messages. Setting this will result in a bounded PCollection.") + @Nullable + public abstract Long getMaxReadTimeSeconds(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConnectionConfiguration( + ConnectionConfiguration connectionConfiguration); + + public abstract Builder setMaxNumRecords(Long maxNumRecords); + + public abstract Builder setMaxReadTimeSeconds(Long maxReadTimeSeconds); + + public abstract ReadConfiguration build(); + } + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mqtt_read:v1"; + } + + @Override + public String description() { + return "Reads messages from an MQTT broker and outputs each payload as a single `bytes` " + + "field.\n" + + "\n" + + "By default the read is unbounded (streaming): it keeps consuming messages from the " + + "subscribed topic until the pipeline is stopped. Setting `maxNumRecords` and/or " + + "`maxReadTimeSeconds` bounds the read, producing a bounded (batch) PCollection.\n" + + "\n" + + "Note: streaming reads require a runner that supports portable streaming (e.g. Prism, " + + "Flink, or Dataflow). The legacy local Python DirectRunner does not execute portable " + + "streaming cross-language reads."; + } + + @Override + protected SchemaTransform from(ReadConfiguration configuration) { + return new MqttReadSchemaTransform(configuration); + } + + private static class MqttReadSchemaTransform extends SchemaTransform { + private final ReadConfiguration config; + + MqttReadSchemaTransform(ReadConfiguration configuration) { + this.config = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + Preconditions.checkState( + input.getAll().isEmpty(), + "Expected zero input PCollections for this source, but found: %s", + input.getAll().keySet()); + + MqttIO.Read readTransform = + MqttIO.read().withConnectionConfiguration(config.getConnectionConfiguration()); + + Long maxRecords = config.getMaxNumRecords(); + Long maxReadTime = config.getMaxReadTimeSeconds(); + if (maxRecords != null) { + readTransform = readTransform.withMaxNumRecords(maxRecords); + } + if (maxReadTime != null) { + readTransform = readTransform.withMaxReadTime(Duration.standardSeconds(maxReadTime)); + } + + Schema outputSchema = Schema.builder().addByteArrayField("bytes").build(); + + PCollection outputRows = + input + .getPipeline() + .apply(readTransform) + .apply( + "Wrap in Beam Rows", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element byte[] data, OutputReceiver outputReceiver) { + outputReceiver.output( + Row.withSchema(outputSchema).addValue(data).build()); + } + })) + .setRowSchema(outputSchema); + + return PCollectionRowTuple.of("output", outputRows); + } + } +} diff --git a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java new file mode 100644 index 000000000000..95ee00c8c3a2 --- /dev/null +++ b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mqtt; + +import static org.apache.beam.sdk.io.mqtt.MqttIO.ConnectionConfiguration; +import static org.apache.beam.sdk.io.mqtt.MqttWriteSchemaTransformProvider.WriteConfiguration; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import javax.annotation.Nullable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; + +@AutoService(SchemaTransformProvider.class) +public class MqttWriteSchemaTransformProvider + extends TypedSchemaTransformProvider { + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class WriteConfiguration implements Serializable { + public static Builder builder() { + return new AutoValue_MqttWriteSchemaTransformProvider_WriteConfiguration.Builder(); + } + + @SchemaFieldDescription("Configuration options to set up the MQTT connection.") + public abstract ConnectionConfiguration getConnectionConfiguration(); + + @SchemaFieldDescription( + "Whether or not the publish message should be retained by the messaging engine. " + + "When a subscriber connects, it gets the latest retained message. " + + "Defaults to `False`, which will clear the retained message from the server.") + @Nullable + public abstract Boolean getRetained(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConnectionConfiguration( + ConnectionConfiguration connectionConfiguration); + + public abstract Builder setRetained(Boolean retained); + + public abstract WriteConfiguration build(); + } + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mqtt_write:v1"; + } + + @Override + public String description() { + return "Publishes messages to an MQTT broker. Expects an input PCollection of rows with a " + + "single `bytes` field, each of which is published as one MQTT message.\n" + + "\n" + + "Works with both bounded (batch) and unbounded (streaming) input PCollections."; + } + + @Override + protected SchemaTransform from(WriteConfiguration configuration) { + return new MqttWriteSchemaTransform(configuration); + } + + private static class MqttWriteSchemaTransform extends SchemaTransform { + private final WriteConfiguration config; + + MqttWriteSchemaTransform(WriteConfiguration configuration) { + this.config = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + PCollection inputRows = input.getSinglePCollection(); + + Preconditions.checkState( + inputRows.getSchema().getFieldCount() == 1 + && inputRows.getSchema().getField(0).getType().equals(Schema.FieldType.BYTES), + "Expected only one Schema field containing bytes, but instead received: %s", + inputRows.getSchema()); + + MqttIO.Write writeTransform = + MqttIO.write().withConnectionConfiguration(config.getConnectionConfiguration()); + Boolean retained = config.getRetained(); + if (retained != null) { + writeTransform = writeTransform.withRetained(retained); + } + + inputRows + .apply( + "Extract bytes", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element Row row, OutputReceiver outputReceiver) { + outputReceiver.output( + org.apache.beam.sdk.util.Preconditions.checkStateNotNull( + row.getBytes(0))); + } + })) + .apply(writeTransform); + + return PCollectionRowTuple.empty(inputRows.getPipeline()); + } + } +} diff --git a/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java b/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java new file mode 100644 index 000000000000..60bdd1104dbe --- /dev/null +++ b/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mqtt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import org.apache.activemq.broker.BrokerService; +import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.PipelineResult; +import org.apache.beam.sdk.io.common.NetworkTestHelper; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptors; +import org.fusesource.hawtbuf.Buffer; +import org.fusesource.mqtt.client.BlockingConnection; +import org.fusesource.mqtt.client.MQTT; +import org.fusesource.mqtt.client.Message; +import org.fusesource.mqtt.client.QoS; +import org.fusesource.mqtt.client.Topic; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MqttSchemaTransformProviderTest { + private static final Logger LOG = LoggerFactory.getLogger(MqttSchemaTransformProviderTest.class); + + private BrokerService brokerService; + + private int port; + + @Rule public final transient TestPipeline pipeline = TestPipeline.create(); + + @Before + public void startBroker() throws Exception { + port = NetworkTestHelper.getAvailableLocalPort(); + LOG.info("Starting ActiveMQ brokerService on {}", port); + brokerService = new BrokerService(); + brokerService.setDeleteAllMessagesOnStartup(true); + // use memory persistence for the test: it's faster and don't pollute test folder with KahaDB + brokerService.setPersistent(false); + brokerService.addConnector("mqtt://localhost:" + port); + brokerService.start(); + brokerService.waitUntilStarted(); + } + + @Test(timeout = 30 * 1000) + public void testReceiveWithTimeoutAndNoData() { + MqttReadSchemaTransformProvider.ReadConfiguration readConfiguration = + MqttReadSchemaTransformProvider.ReadConfiguration.builder() + .setConnectionConfiguration( + MqttIO.ConnectionConfiguration.create("tcp://localhost:" + port, "READ_TOPIC") + .withClientId("READ_PIPELINE")) + .setMaxReadTimeSeconds(2L) + .build(); + + PCollectionRowTuple.empty(pipeline) + .apply(new MqttReadSchemaTransformProvider().from(readConfiguration)); + + // should stop before the test timeout + pipeline.run().waitUntilFinish(); + } + + /** Collects the bytes field of every output row into a shared queue (DirectRunner is in-JVM). */ + private static final ConcurrentLinkedQueue STREAMING_RECEIVED = + new ConcurrentLinkedQueue<>(); + + private static class CollectBytesFn extends DoFn { + @ProcessElement + public void processElement(@Element Row row) { + byte[] bytes = row.getBytes("bytes"); + if (bytes != null) { + STREAMING_RECEIVED.add(new String(bytes, StandardCharsets.UTF_8)); + } + } + } + + /** + * Reads in streaming mode: when neither {@code maxNumRecords} nor {@code maxReadTimeSeconds} is + * set the SchemaTransform produces an unbounded PCollection. Verifies that messages published + * after the reader subscribes flow through continuously, then cancels the running pipeline. + */ + @Test(timeout = 90 * 1000) + public void testReadUnboundedStreaming() throws Exception { + STREAMING_RECEIVED.clear(); + final String topicName = "STREAM_READ_TOPIC"; + + // No bound -> unbounded (streaming) read. + MqttReadSchemaTransformProvider.ReadConfiguration readConfiguration = + MqttReadSchemaTransformProvider.ReadConfiguration.builder() + .setConnectionConfiguration( + MqttIO.ConnectionConfiguration.create("tcp://localhost:" + port, topicName) + .withClientId("STREAM_READ_PIPELINE")) + .build(); + + // Use a local pipeline so run() does not block (the read never terminates on its own). + PipelineOptions options = + PipelineOptionsFactory.fromArgs("--blockOnRun=false").withoutStrictParsing().create(); + Pipeline p = Pipeline.create(options); + PCollectionRowTuple.empty(p) + .apply(new MqttReadSchemaTransformProvider().from(readConfiguration)) + .get("output") + .apply(ParDo.of(new CollectBytesFn())); + + // Publish a steady stream of messages until the reader has consumed enough. + final boolean[] keepPublishing = {true}; + MQTT client = new MQTT(); + client.setHost("tcp://localhost:" + port); + final BlockingConnection publishConnection = client.blockingConnection(); + publishConnection.connect(); + Thread publisher = + new Thread( + () -> { + int i = 0; + try { + while (keepPublishing[0]) { + publishConnection.publish( + topicName, + ("stream-" + i).getBytes(StandardCharsets.UTF_8), + QoS.AT_LEAST_ONCE, + false); + i++; + Thread.sleep(200); + } + } catch (Exception e) { + // ignore: connection closed on teardown + } + }); + + PipelineResult result = p.run(); + publisher.start(); + + // Wait until the unbounded read delivers a meaningful number of records. + int expected = 10; + long deadline = System.currentTimeMillis() + 60 * 1000; + while (STREAMING_RECEIVED.size() < expected && System.currentTimeMillis() < deadline) { + Thread.sleep(500); + } + + keepPublishing[0] = false; + publisher.join(); + publishConnection.disconnect(); + result.cancel(); + + assertTrue( + "expected at least " + expected + " streamed records, got " + STREAMING_RECEIVED.size(), + STREAMING_RECEIVED.size() >= expected); + for (String received : STREAMING_RECEIVED) { + assertTrue("unexpected payload: " + received, received.startsWith("stream-")); + } + } + + @Test + public void testWrite() throws Exception { + final int numberOfTestMessages = 200; + MQTT client = new MQTT(); + client.setHost("tcp://localhost:" + port); + final BlockingConnection connection = client.blockingConnection(); + connection.connect(); + connection.subscribe(new Topic[] {new Topic(Buffer.utf8("WRITE_TOPIC"), QoS.EXACTLY_ONCE)}); + + final Set messages = new ConcurrentSkipListSet<>(); + + Thread subscriber = + new Thread( + () -> { + try { + for (int i = 0; i < numberOfTestMessages; i++) { + Message message = connection.receive(); + messages.add(new String(message.getPayload(), StandardCharsets.UTF_8)); + message.ack(); + LOG.info("message: {}", new String(message.getPayload(), StandardCharsets.UTF_8)); + } + } catch (Exception e) { + LOG.error("Can't receive message", e); + } + }); + subscriber.start(); + + ArrayList data = new ArrayList<>(); + for (int i = 0; i < numberOfTestMessages; i++) { + data.add(("Test " + i).getBytes(StandardCharsets.UTF_8)); + } + + MqttWriteSchemaTransformProvider.WriteConfiguration writeConfiguration = + MqttWriteSchemaTransformProvider.WriteConfiguration.builder() + .setConnectionConfiguration( + MqttIO.ConnectionConfiguration.create("tcp://localhost:" + port, "WRITE_TOPIC")) + .build(); + Schema dataSchema = Schema.builder().addByteArrayField("bytes").build(); + + PCollection inputRows = + pipeline + .apply(Create.of(data)) + .apply( + MapElements.into(TypeDescriptors.rows()) + .via(d -> Row.withSchema(dataSchema).addValue(d).build())) + .setRowSchema(dataSchema); + PCollectionRowTuple.of("input", inputRows) + .apply(new MqttWriteSchemaTransformProvider().from(writeConfiguration)); + pipeline.run(); + subscriber.join(); + + connection.disconnect(); + + assertEquals(numberOfTestMessages, messages.size()); + for (int i = 0; i < numberOfTestMessages; i++) { + assertTrue(messages.contains("Test " + i)); + } + } + + @After + public void stopBroker() throws Exception { + if (brokerService != null) { + brokerService.stop(); + brokerService.waitUntilStopped(); + brokerService = null; + } + } +} diff --git a/sdks/python/build.gradle b/sdks/python/build.gradle index b39b12f198e9..9e2fe232c42c 100644 --- a/sdks/python/build.gradle +++ b/sdks/python/build.gradle @@ -73,6 +73,7 @@ tasks.register("generateExternalTransformsConfig") { // Need to build all expansion services listed in sdks/standard_expansion_services.yaml dependsOn ":sdks:java:io:google-cloud-platform:expansion-service:build" dependsOn ":sdks:java:io:expansion-service:build" + dependsOn ":sdks:java:io:messaging-expansion-service:build" // Keep this in-sync with pyproject.toml def PyYaml = "'pyyaml>=3.12,<7.0.0'" diff --git a/sdks/python/test-suites/xlang/build.gradle b/sdks/python/test-suites/xlang/build.gradle index 3065ad8377e3..1cbbaa0db534 100644 --- a/sdks/python/test-suites/xlang/build.gradle +++ b/sdks/python/test-suites/xlang/build.gradle @@ -25,6 +25,7 @@ project.evaluationDependsOn(":sdks:python") // relevant fields as done here, then add it to `xlangTasks`. def gcpExpansionPath = project.project(':sdks:java:io:google-cloud-platform:expansion-service').getPath() def ioExpansionPath = project.project(':sdks:java:io:expansion-service').getPath() +def messagingExpansionPath = project.project(':sdks:java:io:messaging-expansion-service').getPath() // Properties that are common across runners. // Used to launch the expansion service, collect the right tests, and cleanup afterwards def gcpXlang = new CrossLanguageTask().tap { @@ -42,7 +43,7 @@ def ioXlang = new CrossLanguageTask().tap { } // This list should include all expansion service targets in sdks/python/standard_expansion_services.yaml -def servicesToGenerateFrom = [ioExpansionPath, gcpExpansionPath] +def servicesToGenerateFrom = [ioExpansionPath, messagingExpansionPath, gcpExpansionPath] def xlangWrapperValidation = new CrossLanguageTask().tap { name = "xlangWrapperValidation" expansionProjectPaths = servicesToGenerateFrom diff --git a/sdks/standard_expansion_services.yaml b/sdks/standard_expansion_services.yaml index 531caca5a376..79c7e06280df 100644 --- a/sdks/standard_expansion_services.yaml +++ b/sdks/standard_expansion_services.yaml @@ -53,6 +53,21 @@ - 'beam:schematransform:org.apache.beam:iceberg_read:v1' - 'beam:schematransform:org.apache.beam:iceberg_cdc_read:v1' +- gradle_target: 'sdks:java:io:messaging-expansion-service:shadowJar' + destinations: + python: 'apache_beam/io' + transforms: + 'beam:schematransform:org.apache.beam:mqtt_write:v1': + name: 'WriteToMqtt' + 'beam:schematransform:org.apache.beam:mqtt_read:v1': + name: 'ReadFromMqtt' + skip_transforms: + # Core SchemaTransforms bundled via :sdks:java:expansion-service; already + # generated from the Java IO expansion service above. + - 'beam:schematransform:org.apache.beam:generate_sequence:v1' + - 'beam:schematransform:org.apache.beam:tfrecord_read:v1' + - 'beam:schematransform:org.apache.beam:tfrecord_write:v1' + # TODO(ahmedabu98): Enable this service in a future PR #- gradle_target: 'sdks:java:io:google-cloud-platform:expansion-service:shadowJar' # destinations: diff --git a/sdks/standard_external_transforms.yaml b/sdks/standard_external_transforms.yaml index b50402a64d54..b9802f11b6cf 100644 --- a/sdks/standard_external_transforms.yaml +++ b/sdks/standard_external_transforms.yaml @@ -19,7 +19,7 @@ # configuration in /sdks/standard_expansion_services.yaml. # Refer to gen_xlang_wrappers.py for more info. # -# Last updated on: 2026-05-06 +# Last updated on: 2026-06-11 - default_service: sdks:java:io:expansion-service:shadowJar description: '' @@ -180,3 +180,58 @@ type: str identifier: beam:schematransform:org.apache.beam:tfrecord_write:v1 name: TfrecordWrite +- default_service: sdks:java:io:messaging-expansion-service:shadowJar + description: 'Reads messages from an MQTT broker and outputs each payload as a single + `bytes` field. + + + By default the read is unbounded (streaming): it keeps consuming messages from + the subscribed topic until the pipeline is stopped. Setting `maxNumRecords` and/or + `maxReadTimeSeconds` bounds the read, producing a bounded (batch) PCollection. + + + Note: streaming reads require a runner that supports portable streaming (e.g. + Prism, Flink, or Dataflow). The legacy local Python DirectRunner does not execute + portable streaming cross-language reads.' + destinations: + python: apache_beam/io + fields: + - description: Configuration options to set up the MQTT connection. + name: connection_configuration + nullable: false + type: Row(client_id=typing.Optional[str], password=typing.Optional[str], server_uri=, topic=typing.Optional[str], username=typing.Optional[str]) + - description: The max number of records to receive. Setting this will result in + a bounded PCollection. + name: max_num_records + nullable: true + type: int64 + - description: The maximum time for this source to read messages. Setting this will + result in a bounded PCollection. + name: max_read_time_seconds + nullable: true + type: int64 + identifier: beam:schematransform:org.apache.beam:mqtt_read:v1 + name: ReadFromMqtt +- default_service: sdks:java:io:messaging-expansion-service:shadowJar + description: 'Publishes messages to an MQTT broker. Expects an input PCollection + of rows with a single `bytes` field, each of which is published as one MQTT message. + + + Works with both bounded (batch) and unbounded (streaming) input PCollections.' + destinations: + python: apache_beam/io + fields: + - description: Configuration options to set up the MQTT connection. + name: connection_configuration + nullable: false + type: Row(client_id=typing.Optional[str], password=typing.Optional[str], server_uri=, topic=typing.Optional[str], username=typing.Optional[str]) + - description: Whether or not the publish message should be retained by the messaging + engine. When a subscriber connects, it gets the latest retained message. Defaults + to `False`, which will clear the retained message from the server. + name: retained + nullable: true + type: boolean + identifier: beam:schematransform:org.apache.beam:mqtt_write:v1 + name: WriteToMqtt diff --git a/settings.gradle.kts b/settings.gradle.kts index 443d9c567752..3d4346661a47 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -236,6 +236,7 @@ include(":sdks:java:io:elasticsearch-tests:elasticsearch-tests-8") include(":sdks:java:io:elasticsearch-tests:elasticsearch-tests-9") include(":sdks:java:io:elasticsearch-tests:elasticsearch-tests-common") include(":sdks:java:io:expansion-service") +include(":sdks:java:io:messaging-expansion-service") include(":sdks:java:io:file-based-io-tests") include(":sdks:java:io:bigquery-io-perf-tests") include(":sdks:java:io:cdap") From a3dc1a2f0f8ff8bb7bd77fa8a9a72e20e493208e Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Sat, 13 Jun 2026 15:42:16 -0400 Subject: [PATCH 390/490] Upgrade expansion jar to java17 (#38931) * upgrade expansion sevice to Java 17 * update comment * add container and workflow change * try something * Add PreCommit_Java to run on Java 17 in setup-environment-action * add more coverage --- .github/actions/setup-environment-action/action.yml | 2 +- .../workflows/beam_PreCommit_Xlang_Generated_Transforms.yml | 2 +- sdks/java/expansion-service/container/Dockerfile | 2 +- sdks/java/io/expansion-service/build.gradle | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-environment-action/action.yml b/.github/actions/setup-environment-action/action.yml index 06633c6c7279..daa3daa1cd52 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -74,7 +74,7 @@ runs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} + java-version: ${{ inputs.java-version == 'default' && ((contains(github.job, 'Xlang') || contains(github.job, 'XVR') || contains(github.job, 'PreCommit_Java')) && '17' || '11') || inputs.java-version }} - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: diff --git a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml index 959f36234d70..1dfab40f552d 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -102,7 +102,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: default + java-version: '17' python-version: ${{ matrix.python_version }} - name: Set PY_VER_CLEAN id: set_py_ver_clean diff --git a/sdks/java/expansion-service/container/Dockerfile b/sdks/java/expansion-service/container/Dockerfile index 968f5cd2ac25..513dd6b75b88 100644 --- a/sdks/java/expansion-service/container/Dockerfile +++ b/sdks/java/expansion-service/container/Dockerfile @@ -16,7 +16,7 @@ # limitations under the License. ############################################################################### -FROM eclipse-temurin:11 +FROM eclipse-temurin:17 LABEL Author "Apache Beam " ARG TARGETOS ARG TARGETARCH diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 70a3fce538b6..f7b241a75944 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -25,8 +25,8 @@ applyJavaNature( exportJavadoc: false, validateShadowJar: false, shadowClosure: {}, - // iceberg requires Java11+ - requireJavaVersion: JavaVersion.VERSION_11 + // iceberg requires Java11+ and delta lake requires Java17+ + requireJavaVersion: JavaVersion.VERSION_17 ) // We don't want to use the latest version for the entire beam sdk since beam Java users can override it themselves. From 0de5ec2867d45b5235831e24c3dee8e9e7c6b390 Mon Sep 17 00:00:00 2001 From: Sagnik Ghosh Date: Mon, 15 Jun 2026 20:35:41 +0530 Subject: [PATCH 391/490] Spanner: Extend SpannerIO to support Spanner Omni mTLS setup (#38928) * spanner: extend SpannerIO to support Spanner OMNI mTLS setup * address gemini comments * fix typo in java doc * remove order dependancy from Builder pattern --- .../sdk/io/gcp/spanner/SpannerAccessor.java | 13 +- .../sdk/io/gcp/spanner/SpannerConfig.java | 37 ++++ .../beam/sdk/io/gcp/spanner/SpannerIO.java | 171 +++++++++++++++++- .../spanner/SpannerTransformRegistrar.java | 30 +++ .../MetadataSpannerConfigFactory.java | 6 + .../sdk/io/gcp/spanner/SpannerReadIT.java | 33 ++-- .../sdk/io/gcp/spanner/SpannerTestHelper.java | 110 +++++++++++ .../sdk/io/gcp/spanner/SpannerWriteIT.java | 110 ++++++----- .../changestreams/it/IntegrationTestEnv.java | 17 +- .../it/SpannerChangeStreamIT.java | 19 +- ...mOrderedByTimestampAndTransactionIdIT.java | 10 +- ...hangeStreamOrderedWithinKeyGloballyIT.java | 10 +- ...SpannerChangeStreamOrderedWithinKeyIT.java | 10 +- .../SpannerChangeStreamPlacementTableIT.java | 19 +- ...rChangeStreamPlacementTablePostgresIT.java | 10 +- .../it/SpannerChangeStreamPostgresIT.java | 10 +- ...erChangeStreamTransactionBoundariesIT.java | 10 +- ...SpannerChangeStreamsSchemaTransformIT.java | 4 + 18 files changed, 510 insertions(+), 119 deletions(-) create mode 100644 sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTestHelper.java diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java index eb1860902c6f..351425db67a3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java @@ -247,7 +247,16 @@ static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { } if (plainText != null && Boolean.TRUE.equals(plainText.get())) { builder.setChannelConfigurator(b -> b.usePlaintext()); - builder.setCredentials(NoCredentials.getInstance()); + } + ValueProvider clientCert = spannerConfig.getClientCertPath(); + ValueProvider clientKey = spannerConfig.getClientCertKeyPath(); + if (clientCert != null + && clientKey != null + && clientCert.isAccessible() + && clientKey.isAccessible() + && !Strings.isNullOrEmpty(clientCert.get()) + && !Strings.isNullOrEmpty(clientKey.get())) { + builder.useClientCert(clientCert.get(), clientKey.get()); } } @@ -273,6 +282,8 @@ static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { ValueProvider credentials = spannerConfig.getCredentials(); if (credentials != null && credentials.get() != null) { builder.setCredentials(credentials.get()); + } else if (experimentalHost != null && !Strings.isNullOrEmpty(experimentalHost.get())) { + builder.setCredentials(NoCredentials.getInstance()); } ValueProvider waitForSessionCreationDuration = diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java index a17c851f38a0..92eac9108283 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java @@ -97,6 +97,10 @@ public String getHostValue() { public abstract @Nullable ValueProvider getPlainText(); + public abstract @Nullable ValueProvider getClientCertPath(); + + public abstract @Nullable ValueProvider getClientCertKeyPath(); + @VisibleForTesting abstract @Nullable ServiceFactory getServiceFactory(); @@ -194,6 +198,10 @@ abstract Builder setExecuteStreamingSqlRetrySettings( abstract Builder setWaitForSessionCreationDuration( ValueProvider waitForSessionCreationDuration); + abstract Builder setClientCertPath(ValueProvider clientCertPath); + + abstract Builder setClientCertKeyPath(ValueProvider clientCertKeyPath); + public abstract SpannerConfig build(); } @@ -414,4 +422,33 @@ public SpannerConfig withWaitForSessionCreationDuration( return withWaitForSessionCreationDuration( ValueProvider.StaticValueProvider.of(waitForSessionCreationDuration)); } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using a Spanner Omni instance (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public SpannerConfig withClientCert( + ValueProvider certPath, ValueProvider keyPath) { + return toBuilder().setClientCertPath(certPath).setClientCertKeyPath(keyPath).build(); + } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using a Spanner Omni instance (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public SpannerConfig withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java index d271e763aac5..c326541818b3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java @@ -37,6 +37,7 @@ import com.google.api.gax.rpc.StatusCode.Code; import com.google.auth.Credentials; import com.google.auto.value.AutoValue; +import com.google.cloud.NoCredentials; import com.google.cloud.ServiceFactory; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbortedException; @@ -146,6 +147,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Stopwatch; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheLoader; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.LoadingCache; @@ -657,6 +659,35 @@ public ReadAll withUsingPlainTextChannel(boolean plainText) { return withUsingPlainTextChannel(ValueProvider.StaticValueProvider.of(plainText)); } + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public ReadAll withClientCert(ValueProvider certPath, ValueProvider keyPath) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withClientCert(certPath, keyPath)); + } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public ReadAll withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } + /** Specifies the Cloud Spanner database. */ public ReadAll withDatabaseId(ValueProvider databaseId) { SpannerConfig config = getSpannerConfig(); @@ -917,6 +948,35 @@ public Read withUsingPlainTextChannel(boolean plainText) { return withUsingPlainTextChannel(ValueProvider.StaticValueProvider.of(plainText)); } + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public Read withClientCert(ValueProvider certPath, ValueProvider keyPath) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withClientCert(certPath, keyPath)); + } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public Read withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } + /** If true the uses Cloud Spanner batch API. */ public Read withBatching(boolean batching) { return toBuilder().setBatching(batching).build(); @@ -1244,6 +1304,36 @@ public CreateTransaction withUsingPlainTextChannel(boolean plainText) { return withUsingPlainTextChannel(ValueProvider.StaticValueProvider.of(plainText)); } + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public CreateTransaction withClientCert( + ValueProvider certPath, ValueProvider keyPath) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withClientCert(certPath, keyPath)); + } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public CreateTransaction withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } + @VisibleForTesting CreateTransaction withServiceFactory(ServiceFactory serviceFactory) { SpannerConfig config = getSpannerConfig(); @@ -1412,6 +1502,35 @@ public Write withUsingPlainTextChannel(boolean plainText) { return withUsingPlainTextChannel(ValueProvider.StaticValueProvider.of(plainText)); } + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public Write withClientCert(ValueProvider certPath, ValueProvider keyPath) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withClientCert(certPath, keyPath)); + } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public Write withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } + public Write withDialectView(PCollectionView dialect) { return toBuilder().setDialectView(dialect).build(); } @@ -1770,6 +1889,10 @@ public abstract static class ReadChangeStream abstract @Nullable ValueProvider getPlainText(); + abstract @Nullable ValueProvider getClientCertPath(); + + abstract @Nullable ValueProvider getClientCertKeyPath(); + abstract Duration getRealTimeCheckpointInterval(); abstract int getHeartbeatMillis(); @@ -1807,6 +1930,10 @@ abstract static class Builder { abstract Builder setPlainText(ValueProvider plainText); + abstract Builder setClientCertPath(ValueProvider clientCertPath); + + abstract Builder setClientCertKeyPath(ValueProvider clientCertKeyPath); + /** * When caught up to real-time, checkpoint processing of change stream this often. This sets a * bound on latency of processing if a steady trickle of elements prevents the heartbeat @@ -1946,6 +2073,36 @@ public ReadChangeStream withUsingPlainTextChannel(boolean plainText) { return withUsingPlainTextChannel(ValueProvider.StaticValueProvider.of(plainText)); } + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public ReadChangeStream withClientCert( + ValueProvider certPath, ValueProvider keyPath) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withClientCert(certPath, keyPath)); + } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

      Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public ReadChangeStream withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } + /** * Configures low latency experiment for readChangeStream transform. Example usage: * @@ -2177,9 +2334,17 @@ SpannerConfig buildChangeStreamSpannerConfig() { static SpannerConfig buildSpannerConfigWithCredential( SpannerConfig spannerConfig, PipelineOptions pipelineOptions) { if (spannerConfig.getCredentials() == null && pipelineOptions != null) { - final Credentials credentials = pipelineOptions.as(GcpOptions.class).getGcpCredential(); - if (credentials != null) { - spannerConfig = spannerConfig.withCredentials(credentials); + boolean isExperimentalHostEmpty = + spannerConfig.getExperimentalHost() == null + || (spannerConfig.getExperimentalHost().isAccessible() + && Strings.isNullOrEmpty(spannerConfig.getExperimentalHost().get())); + if (isExperimentalHostEmpty) { + final Credentials credentials = pipelineOptions.as(GcpOptions.class).getGcpCredential(); + if (credentials != null) { + spannerConfig = spannerConfig.withCredentials(credentials); + } + } else { + spannerConfig = spannerConfig.withCredentials(NoCredentials.getInstance()); } } return spannerConfig; diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java index 70908f982721..544aa2938d51 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java @@ -81,6 +81,8 @@ public abstract static class CrossLanguageConfiguration { @Nullable String emulatorHost; @Nullable String experimentalHost; @Nullable Boolean plainText; + @Nullable String clientCertPath; + @Nullable String clientCertKeyPath; public void setInstanceId(String instanceId) { this.instanceId = instanceId; @@ -110,6 +112,14 @@ public void setPlainText(@Nullable Boolean plainText) { this.plainText = plainText; } + public void setClientCertPath(@Nullable String clientCertPath) { + this.clientCertPath = clientCertPath; + } + + public void setClientCertKeyPath(@Nullable String clientCertKeyPath) { + this.clientCertKeyPath = clientCertKeyPath; + } + void checkMandatoryFields() { if (projectId.isEmpty()) { throw new IllegalArgumentException("projectId can't be empty"); @@ -120,6 +130,10 @@ void checkMandatoryFields() { if (instanceId.isEmpty()) { throw new IllegalArgumentException("instanceId can't be empty"); } + if ((clientCertPath != null) != (clientCertKeyPath != null)) { + throw new IllegalArgumentException( + "Both clientCertPath and clientCertKeyPath must be specified together."); + } } } @@ -249,6 +263,11 @@ public PTransform> buildExternal( if (configuration.plainText != null) { readTransform = readTransform.withUsingPlainTextChannel(configuration.plainText); } + if (configuration.clientCertPath != null && configuration.clientCertKeyPath != null) { + readTransform = + readTransform.withClientCert( + configuration.clientCertPath, configuration.clientCertKeyPath); + } @Nullable TimestampBound timestampBound = configuration.getTimestampBound(); if (timestampBound != null) { readTransform = readTransform.withTimestampBound(timestampBound); @@ -393,6 +412,11 @@ public PTransform, PDone> buildExternal( if (configuration.plainText != null) { writeTransform = writeTransform.withUsingPlainTextChannel(configuration.plainText); } + if (configuration.clientCertPath != null && configuration.clientCertKeyPath != null) { + writeTransform = + writeTransform.withClientCert( + configuration.clientCertPath, configuration.clientCertKeyPath); + } if (configuration.commitDeadline != null) { writeTransform = writeTransform.withCommitDeadline(configuration.commitDeadline); } @@ -504,6 +528,12 @@ public PTransform> buildExternal( readChangeStream = readChangeStream.withMetadataTable(configuration.metadataTable); } + if (configuration.clientCertPath != null && configuration.clientCertKeyPath != null) { + readChangeStream = + readChangeStream.withClientCert( + configuration.clientCertPath, configuration.clientCertKeyPath); + } + if (configuration.rpcPriority != null) { readChangeStream = readChangeStream.withRpcPriority(configuration.rpcPriority); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java index 959582e9c35f..de81814c2110 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java @@ -87,6 +87,12 @@ public static SpannerConfig create( config = config.withUsingPlainTextChannel(plainText.get()); } + ValueProvider clientCert = primaryConfig.getClientCertPath(); + ValueProvider clientKey = primaryConfig.getClientCertKeyPath(); + if (clientCert != null && clientKey != null) { + config = config.withClientCert(clientCert, clientKey); + } + ValueProvider isLocalChannelProvider = primaryConfig.getIsLocalChannelProvider(); if (isLocalChannelProvider != null) { config = diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerReadIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerReadIT.java index 34c839d3e1e6..405d1e9b1e15 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerReadIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerReadIT.java @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; import org.apache.beam.sdk.options.Default; import org.apache.beam.sdk.options.Description; import org.apache.beam.sdk.options.PipelineOptionsFactory; @@ -110,21 +109,19 @@ public void setUp() throws Exception { PipelineOptionsFactory.register(SpannerTestPipelineOptions.class); options = TestPipeline.testingPipelineOptions().as(SpannerTestPipelineOptions.class); - project = options.getInstanceProjectId(); - if (project == null) { - project = options.as(GcpOptions.class).getProject(); - } + project = SpannerTestHelper.getProject(options, options.getInstanceProjectId()); + options.setInstanceId(SpannerTestHelper.getInstanceId(options.getInstanceId())); - spanner = + SpannerOptions.Builder spannerBuilder = SpannerOptions.newBuilder() .setProjectId(project) .disableGrpcGcpExtension() .setSessionPoolOption( SessionPoolOptions.newBuilder() .setWaitForMinSessionsDuration(java.time.Duration.ofMinutes(5)) - .build()) - .build() - .getService(); + .build()); + spannerBuilder = SpannerTestHelper.setUpSpannerOptions(spannerBuilder); + spanner = spannerBuilder.build().getService(); databaseName = generateDatabaseName(); pgDatabaseName = "pg-" + databaseName; @@ -485,17 +482,19 @@ private void makeTestData() { } private SpannerConfig createSpannerConfig() { - return SpannerConfig.create() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(databaseName); + return SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(databaseName)); } private SpannerConfig createPgSpannerConfig() { - return SpannerConfig.create() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(pgDatabaseName); + return SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(pgDatabaseName)); } private DatabaseClient getDatabaseClient() { diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTestHelper.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTestHelper.java new file mode 100644 index 000000000000..55ec74f19eb6 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTestHelper.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.spanner; + +import com.google.cloud.spanner.SpannerOptions; +import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; + +/** Helper methods for Spanner IT tests to support Spanner Omni natively. */ +public class SpannerTestHelper { + + public static String getOmniEndpoint() { + return System.getenv("SPANNER_OMNI_ENDPOINT"); + } + + public static boolean isOmni() { + return !Strings.isNullOrEmpty(getOmniEndpoint()); + } + + public static String getOmniClientCert() { + return System.getenv("SPANNER_OMNI_CLIENT_CERT"); + } + + public static String getOmniClientKey() { + return System.getenv("SPANNER_OMNI_CLIENT_KEY"); + } + + public static boolean isOmniUsePlainText() { + return Boolean.parseBoolean(System.getenv("SPANNER_OMNI_USE_PLAIN_TEXT")); + } + + public static String getProject(PipelineOptions options, String instanceProjectId) { + if (isOmni()) { + return "default"; + } + String project = instanceProjectId; + if (project == null) { + project = options.as(GcpOptions.class).getProject(); + } + return project; + } + + public static String getInstanceId(String instanceId) { + if (isOmni()) { + return "default"; + } + return instanceId; + } + + public static SpannerOptions.Builder setUpSpannerOptions(SpannerOptions.Builder builder) { + if (isOmni()) { + builder.setExperimentalHost(getOmniEndpoint()); + if (isOmniUsePlainText()) { + builder.usePlainText(); + } + String cert = getOmniClientCert(); + String key = getOmniClientKey(); + if (!Strings.isNullOrEmpty(cert) && !Strings.isNullOrEmpty(key)) { + builder.useClientCert(cert, key); + } + } + return builder; + } + + public static SpannerConfig setUpSpannerConfig(SpannerConfig config) { + if (isOmni()) { + config = config.withExperimentalHost(getOmniEndpoint()); + if (isOmniUsePlainText()) { + config = config.withUsingPlainTextChannel(true); + } + String cert = getOmniClientCert(); + String key = getOmniClientKey(); + if (!Strings.isNullOrEmpty(cert) && !Strings.isNullOrEmpty(key)) { + config = config.withClientCert(cert, key); + } + } + return config; + } + + public static SpannerIO.Write setUpSpannerIO(SpannerIO.Write write) { + if (isOmni()) { + write = write.withExperimentalHost(getOmniEndpoint()); + if (isOmniUsePlainText()) { + write = write.withUsingPlainTextChannel(true); + } + String cert = getOmniClientCert(); + String key = getOmniClientKey(); + if (!Strings.isNullOrEmpty(cert) && !Strings.isNullOrEmpty(key)) { + write = write.withClientCert(cert, key); + } + } + return write; + } +} diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerWriteIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerWriteIT.java index df23435d82ab..e4ad156ef885 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerWriteIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerWriteIT.java @@ -17,9 +17,11 @@ */ package org.apache.beam.sdk.io.gcp.spanner; +import static org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper.isOmni; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.Assume.assumeFalse; import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.spanner.Database; @@ -37,7 +39,6 @@ import java.util.Collections; import java.util.Objects; import org.apache.beam.sdk.PipelineResult; -import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; import org.apache.beam.sdk.io.GenerateSequence; import org.apache.beam.sdk.options.Default; import org.apache.beam.sdk.options.Description; @@ -117,21 +118,20 @@ public void setUp() throws Exception { PipelineOptionsFactory.register(SpannerTestPipelineOptions.class); options = TestPipeline.testingPipelineOptions().as(SpannerTestPipelineOptions.class); - project = options.getInstanceProjectId(); - if (project == null) { - project = options.as(GcpOptions.class).getProject(); - } + project = SpannerTestHelper.getProject(options, options.getInstanceProjectId()); + options.setInstanceId(SpannerTestHelper.getInstanceId(options.getInstanceId())); - spanner = + SpannerOptions.Builder spannerBuilder = SpannerOptions.newBuilder() .setProjectId(project) .disableGrpcGcpExtension() .setSessionPoolOption( SessionPoolOptions.newBuilder() .setWaitForMinSessionsDuration(java.time.Duration.ofMinutes(5)) - .build()) - .build() - .getService(); + .build()); + + spannerBuilder = SpannerTestHelper.setUpSpannerOptions(spannerBuilder); + spanner = spannerBuilder.build().getService(); databaseName = generateDatabaseName(); pgDatabaseName = "pg-" + databaseName; @@ -191,10 +191,11 @@ public void testWrite() throws Exception { .apply("Generate mu", ParDo.of(new GenerateMutations(options.getTable()))) .apply( "Write db", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(databaseName)); + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(databaseName))); PCollectionView dialectView = p.apply("PG Dialect", Create.of(Dialect.POSTGRESQL)) @@ -203,10 +204,11 @@ public void testWrite() throws Exception { .apply("Generate PG mu", ParDo.of(new GenerateMutations(options.getTable()))) .apply( "Write PG db", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(pgDatabaseName) + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(pgDatabaseName)) .withDialectView(dialectView)); PipelineResult result = p.run(); @@ -218,6 +220,8 @@ public void testWrite() throws Exception { @Test public void testWriteViaSchemaTransform() throws Exception { + assumeFalse( + "SchemaTransform tests do not support dynamic SpannerConfig overrides for Omni", isOmni()); int numRecords = 100; final Schema tableSchema = Schema.builder().addInt64Field("Key").addStringField("Value").build(); @@ -258,20 +262,22 @@ public void testSequentialWrite() throws Exception { .apply("Gen mutations1", ParDo.of(new GenerateMutations(options.getTable()))) .apply( "write to table1", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(databaseName)); + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(databaseName))); p.apply("second step", GenerateSequence.from(numRecords).to(2 * numRecords)) .apply("Gen mutations2", ParDo.of(new GenerateMutations(options.getTable()))) .apply("wait", Wait.on(stepOne.getOutput())) .apply( "write to table2", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(databaseName)); + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(databaseName))); PCollectionView dialectView = p.apply("PG Dialect", Create.of(Dialect.POSTGRESQL)) @@ -282,10 +288,11 @@ public void testSequentialWrite() throws Exception { .apply("Gen pg mutations1", ParDo.of(new GenerateMutations(options.getTable()))) .apply( "write to pg table1", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(pgDatabaseName) + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(pgDatabaseName)) .withDialectView(dialectView)); p.apply("pg second step", GenerateSequence.from(numRecords).to(2 * numRecords)) @@ -293,10 +300,11 @@ public void testSequentialWrite() throws Exception { .apply("pg wait", Wait.on(pgStepOne.getOutput())) .apply( "write to pg table2", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(pgDatabaseName) + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(pgDatabaseName)) .withDialectView(dialectView)); PipelineResult result = p.run(); @@ -313,10 +321,11 @@ public void testReportFailures() throws Exception { .apply("Generate mu", ParDo.of(new GenerateMutations(options.getTable(), new DivBy2()))) .apply( "Write db", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(databaseName) + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(databaseName)) .withFailureMode(SpannerIO.FailureMode.REPORT_FAILURES)); PCollectionView dialectView = @@ -326,10 +335,11 @@ public void testReportFailures() throws Exception { .apply("Generate pg mu", ParDo.of(new GenerateMutations(options.getTable(), new DivBy2()))) .apply( "Write pg db", - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(pgDatabaseName) + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(pgDatabaseName)) .withFailureMode(SpannerIO.FailureMode.REPORT_FAILURES) .withDialectView(dialectView)); @@ -348,10 +358,11 @@ public void testFailFast() throws Exception { p.apply(GenerateSequence.from(0).to(2 * numRecords)) .apply(ParDo.of(new GenerateMutations(options.getTable(), new DivBy2()))) .apply( - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(databaseName)); + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(databaseName))); PipelineResult result = p.run(); result.waitUntilFinish(); @@ -369,10 +380,11 @@ public void testPgFailFast() throws Exception { p.apply(GenerateSequence.from(0).to(2 * numRecords)) .apply(ParDo.of(new GenerateMutations(options.getTable(), new DivBy2()))) .apply( - SpannerIO.write() - .withProjectId(project) - .withInstanceId(options.getInstanceId()) - .withDatabaseId(pgDatabaseName) + SpannerTestHelper.setUpSpannerIO( + SpannerIO.write() + .withProjectId(project) + .withInstanceId(options.getInstanceId()) + .withDatabaseId(pgDatabaseName)) .withDialectView(dialectView)); PipelineResult result = p.run(); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/IntegrationTestEnv.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/IntegrationTestEnv.java index b36004a5cd15..b00568f8d40c 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/IntegrationTestEnv.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/IntegrationTestEnv.java @@ -33,8 +33,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; import org.apache.beam.sdk.io.common.IOITHelper; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.commons.lang3.RandomStringUtils; import org.junit.rules.ExternalResource; import org.slf4j.Logger; @@ -73,12 +73,11 @@ protected void before() throws Throwable { final ChangeStreamTestPipelineOptions options = IOITHelper.readIOTestPipelineOptions(ChangeStreamTestPipelineOptions.class); - projectId = - Optional.ofNullable(options.getProjectId()) - .orElseGet(() -> options.as(GcpOptions.class).getProject()); - instanceId = options.getInstanceId(); + projectId = SpannerTestHelper.getProject(options, options.getProjectId()); + instanceId = SpannerTestHelper.getInstanceId(options.getInstanceId()); generateDatabaseIds(options); - spanner = + + SpannerOptions.Builder spannerBuilder = SpannerOptions.newBuilder() .setProjectId(projectId) .setHost(host) @@ -86,9 +85,9 @@ protected void before() throws Throwable { .setSessionPoolOption( SessionPoolOptions.newBuilder() .setWaitForMinSessionsDuration(java.time.Duration.ofMinutes(5)) - .build()) - .build() - .getService(); + .build()); + spannerBuilder = SpannerTestHelper.setUpSpannerOptions(spannerBuilder); + spanner = spannerBuilder.build().getService(); databaseAdminClient = spanner.getDatabaseAdminClient(); metadataTableName = generateTableName(METADATA_TABLE_NAME_PREFIX); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java index e6178cbf5402..f66e88ddbeb0 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java @@ -42,6 +42,7 @@ import org.apache.beam.runners.direct.DirectRunner; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod; import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider; @@ -137,10 +138,11 @@ public void testReadSpannerChangeStreamImpl(TestPipeline testPipeline, String ro final Timestamp endAt = deleteTimestamps.getRight(); SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); if (role != null) { spannerConfig = spannerConfig.withDatabaseRole(StaticValueProvider.of(role)); } @@ -197,10 +199,11 @@ public void testReadSpannerChangeStreamFilteredByTransactionTag() { final Timestamp endAt = deleteTimestamps.getRight(); final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); // Filter records to only those from transactions with tag "app=beam;action=update" final PCollection tokens = diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedByTimestampAndTransactionIdIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedByTimestampAndTransactionIdIT.java index 04c09a2e12ce..c4708e11a618 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedByTimestampAndTransactionIdIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedByTimestampAndTransactionIdIT.java @@ -37,6 +37,7 @@ import org.apache.beam.sdk.coders.BooleanCoder; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod; import org.apache.beam.sdk.state.BagState; @@ -102,10 +103,11 @@ public static void setup() throws InterruptedException, ExecutionException, Time @Test public void testTransactionBoundaries() { final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); // Commit a initial transaction to get the timestamp to start reading from. List mutations = new ArrayList<>(); mutations.add(insertRecordMutation(0, "FirstName0", "LastName0")); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyGloballyIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyGloballyIT.java index 513e5aeb2d76..4ecb721aa74a 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyGloballyIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyGloballyIT.java @@ -34,6 +34,7 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.state.BagState; import org.apache.beam.sdk.state.StateSpec; @@ -93,10 +94,11 @@ public static void setup() throws InterruptedException, ExecutionException, Time @Test public void testOrderedWithinKey() { final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); // Get the time increment interval at which to flush data changes ordered by key. final long timeIncrementInSeconds = 10; diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyIT.java index e1731099204d..30d748ab1c79 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamOrderedWithinKeyIT.java @@ -33,6 +33,7 @@ import java.util.stream.StreamSupport; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.ChangeStreamRecordMetadata; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.testing.PAssert; @@ -88,10 +89,11 @@ public static void setup() throws InterruptedException, ExecutionException, Time public void testOrderedWithinKey() { LOG.info("Test pipeline: {}", pipeline); final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); // Commit a initial transaction to get the timestamp to start reading from. List mutations = new ArrayList<>(); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTableIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTableIT.java index 9318dad7ec6d..0b5831f05e1d 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTableIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTableIT.java @@ -43,6 +43,7 @@ import org.apache.beam.runners.direct.DirectRunner; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod; import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider; @@ -149,10 +150,11 @@ public void testReadSpannerChangeStreamImpl( final Timestamp endAt = deleteTimestamps.getRight(); SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); if (role != null) { spannerConfig = spannerConfig.withDatabaseRole(StaticValueProvider.of(role)); } @@ -213,10 +215,11 @@ public void testReadSpannerChangeStreamFilteredByTransactionTag() { final Timestamp endAt = deleteTimestamps.getRight(); final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); // Filter records to only those from transactions with tag "app=beam;action=update" final PCollection tokens = diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTablePostgresIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTablePostgresIT.java index 129a4334d1bb..f017e72eb0ae 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTablePostgresIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPlacementTablePostgresIT.java @@ -37,6 +37,7 @@ import java.util.Optional; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod; import org.apache.beam.sdk.options.ValueProvider; @@ -125,10 +126,11 @@ private void testReadSpannerChangeStreamImpl(List tvfNameList) { final Timestamp endAt = deleteTimestamps.getRight(); final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId) + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)) .withHost(ValueProvider.StaticValueProvider.of(host)); SpannerIO.ReadChangeStream readChangeStream = diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPostgresIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPostgresIT.java index 5f5f55e46964..99f5269e14fd 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPostgresIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamPostgresIT.java @@ -36,6 +36,7 @@ import java.util.Optional; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod; import org.apache.beam.sdk.options.ValueProvider; @@ -114,10 +115,11 @@ public void testReadSpannerChangeStream() { final Timestamp endAt = deleteTimestamps.getRight(); final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId) + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)) .withHost(ValueProvider.StaticValueProvider.of(host)); final PCollection tokens = diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamTransactionBoundariesIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamTransactionBoundariesIT.java index 12e1caa76428..fe0228193815 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamTransactionBoundariesIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamTransactionBoundariesIT.java @@ -34,6 +34,7 @@ import org.apache.beam.sdk.PipelineResult; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; +import org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.Mod; import org.apache.beam.sdk.state.BagState; @@ -90,10 +91,11 @@ public static void setup() throws InterruptedException, ExecutionException, Time public void testTransactionBoundaries() { LOG.info("Test pipeline: {}", pipeline); final SpannerConfig spannerConfig = - SpannerConfig.create() - .withProjectId(projectId) - .withInstanceId(instanceId) - .withDatabaseId(databaseId); + SpannerTestHelper.setUpSpannerConfig( + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId)); // Commit a initial transaction to get the timestamp to start reading from. List mutations = new ArrayList<>(); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamsSchemaTransformIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamsSchemaTransformIT.java index 56d964087128..606af9cfc996 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamsSchemaTransformIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamsSchemaTransformIT.java @@ -17,9 +17,11 @@ */ package org.apache.beam.sdk.io.gcp.spanner.changestreams.it; +import static org.apache.beam.sdk.io.gcp.spanner.SpannerTestHelper.isOmni; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import com.google.cloud.Timestamp; import com.google.cloud.spanner.DatabaseClient; @@ -89,6 +91,8 @@ public void before() { @Test public void testReadSpannerChangeStream() { + assumeFalse( + "SchemaTransform tests do not support dynamic SpannerConfig overrides for Omni", isOmni()); // Defines how many rows are going to be inserted / updated / deleted in the test final int numRows = 5; // Inserts numRows rows and uses the first commit timestamp as the startAt for reading the From 4a8bbbc50d0b8cc4354d4078e1973027a772ce79 Mon Sep 17 00:00:00 2001 From: Elia Liu Date: Tue, 16 Jun 2026 02:34:01 +1000 Subject: [PATCH 392/490] ClickHouseIO: Add DateTime64 support for sub-second timestamp precision (#38510) ClickHouseIO previously only recognized second-precision DateTime, so pipelines emitting sub-second timestamps could not write to DateTime64 columns. This adds TypeName.DATETIME64 with a validated precision (0-9) and ColumnType.dateTime64(precision), plus a no-arg factory that defaults to precision 3. The parser accepts DateTime64[(precision[, 'timezone'])], including a bare DateTime64 and the type nested inside Nullable(...) and Array(...); the timezone argument is accepted but not stored. Beam field-type mapping is Joda DATETIME for precision up to 3, SqlTypes.TIMESTAMP for 4-6, and NanosInstant for 7 and above. The writer encodes the value as a little-endian int64 of epoch_seconds * 10^precision + sub_second_units, using floor division so negative timestamps match ClickHouse. Tests cover the parser, schema mapping, the encoder (Joda and java.time inputs, negative and overflow cases, null input), and ClickHouse testcontainer round-trips for precisions 3, 6 and 9 plus nullable cases. Closes #38466 --- CHANGES.md | 1 + .../beam/sdk/io/clickhouse/ClickHouseIO.java | 3 + .../sdk/io/clickhouse/ClickHouseWriter.java | 40 +++++ .../beam/sdk/io/clickhouse/TableSchema.java | 53 ++++++- .../src/main/javacc/ColumnTypeParser.jj | 27 ++++ .../sdk/io/clickhouse/ClickHouseIOIT.java | 119 +++++++++++++++ .../io/clickhouse/ClickHouseWriterTest.java | 141 ++++++++++++++++++ .../sdk/io/clickhouse/TableSchemaTest.java | 102 +++++++++++++ 8 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriterTest.java diff --git a/CHANGES.md b/CHANGES.md index c70fef0cf8e8..eb24a71d0bfa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -67,6 +67,7 @@ * Support for reading from Delta Lake added (Java) ([#38551](https://github.com/apache/beam/issues/38551)). * Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). +* ClickHouseIO: support writing `DateTime64(precision[, 'timezone'])` columns with sub-second precision (Java) ([#38466](https://github.com/apache/beam/issues/38466)). ## New Features / Improvements diff --git a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java index a8875407b43c..6798e2f2bd75 100644 --- a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java +++ b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java @@ -38,6 +38,8 @@ import org.apache.beam.sdk.schemas.FieldAccessDescriptor; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.schemas.transforms.Select; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; @@ -137,6 +139,7 @@ * {@link TableSchema.TypeName#UINT64} {@link Schema.TypeName#INT64} * {@link TableSchema.TypeName#DATE} {@link Schema.TypeName#DATETIME} * {@link TableSchema.TypeName#DATETIME} {@link Schema.TypeName#DATETIME} + * {@link TableSchema.TypeName#DATETIME64} {@link Schema.TypeName#DATETIME} (precision ≤ 3), {@link SqlTypes#TIMESTAMP} (4–6), or {@link NanosInstant} (≥ 7) * {@link TableSchema.TypeName#ARRAY} {@link Schema.TypeName#ARRAY} * {@link TableSchema.TypeName#ENUM8} {@link Schema.TypeName#STRING} * {@link TableSchema.TypeName#ENUM16} {@link Schema.TypeName#STRING} diff --git a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java index 73735f568646..4d9f072e598f 100644 --- a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java +++ b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriter.java @@ -39,6 +39,39 @@ public class ClickHouseWriter { private static final Instant EPOCH_INSTANT = new Instant(0L); + // 10^0 through 10^9 inclusive — precision is validated in [0, 9] by ColumnType.dateTime64. + private static final long[] POW10 = { + 1L, 10L, 100L, 1_000L, 10_000L, 100_000L, 1_000_000L, 10_000_000L, 100_000_000L, 1_000_000_000L + }; + + /** + * Encodes a timestamp into ClickHouse's {@code DateTime64(precision)} representation: a signed + * 64-bit integer counting ticks of size 10-precision seconds since the Unix epoch. + * + *

      Accepts either a Joda {@link ReadableInstant} (millisecond precision) or a {@link + * java.time.Instant} (nanosecond precision). Sub-tick fractions are truncated toward negative + * infinity, matching ClickHouse's own encoding for negative timestamps. + */ + static long encodeDateTime64(Object value, int precision) { + long epochSecond; + int nanoOfSecond; + if (value instanceof java.time.Instant) { + java.time.Instant inst = (java.time.Instant) value; + epochSecond = inst.getEpochSecond(); + nanoOfSecond = inst.getNano(); + } else if (value instanceof ReadableInstant) { + long millis = ((ReadableInstant) value).getMillis(); + epochSecond = Math.floorDiv(millis, 1000L); + nanoOfSecond = (int) Math.floorMod(millis, 1000L) * 1_000_000; + } else { + throw new IllegalArgumentException( + "DateTime64 requires a Joda ReadableInstant or java.time.Instant, got " + + (value == null ? "null" : value.getClass().getName())); + } + long subSecondTicks = nanoOfSecond / POW10[9 - precision]; + return Math.addExact(Math.multiplyExact(epochSecond, POW10[precision]), subSecondTicks); + } + @SuppressWarnings("unchecked") static void writeNullableValue(ClickHouseOutputStream stream, ColumnType columnType, Object value) throws IOException { @@ -138,6 +171,13 @@ static void writeValue(ClickHouseOutputStream stream, ColumnType columnType, Obj BinaryStreamUtils.writeUnsignedInt32(stream, epochSeconds); break; + case DATETIME64: + int precision = + Preconditions.checkNotNull( + columnType.precision(), "DateTime64 column is missing precision"); + BinaryStreamUtils.writeInt64(stream, encodeDateTime64(value, precision)); + break; + case ARRAY: List values = (List) value; BinaryStreamUtils.writeVarInt(stream, values.size()); diff --git a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/TableSchema.java b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/TableSchema.java index baee77c5f9af..1b9fdffd4c86 100644 --- a/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/TableSchema.java +++ b/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/TableSchema.java @@ -27,6 +27,8 @@ import java.util.stream.Collectors; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -39,6 +41,9 @@ }) public abstract class TableSchema implements Serializable { + private static final Schema.FieldType NANOS_INSTANT_TYPE = + Schema.FieldType.logicalType(new NanosInstant()); + public abstract List columns(); public static TableSchema of(Column... columns) { @@ -76,6 +81,22 @@ public static Schema.FieldType getEquivalentFieldType(ColumnType columnType) { case DATETIME: return Schema.FieldType.DATETIME; + case DATETIME64: + // Pick the narrowest Beam logical type that still round-trips the requested precision: + // ≤ 3 (milliseconds) → Joda DATETIME, keeping existing pipelines unchanged. + // 4–6 (down to microseconds) → SqlTypes.TIMESTAMP (MicrosInstant) — interoperable + // with BigQueryIO, Avro and Beam SQL. + // ≥ 7 (sub-microsecond) → NanosInstant, the only built-in type that preserves + // full nanosecond precision through Row construction. + int p = columnType.precision(); + if (p <= 3) { + return Schema.FieldType.DATETIME; + } else if (p <= 6) { + return Schema.FieldType.logicalType(SqlTypes.TIMESTAMP); + } else { + return NANOS_INSTANT_TYPE; + } + case STRING: return Schema.FieldType.STRING; @@ -163,6 +184,7 @@ public enum TypeName { // Primitive types DATE, DATETIME, + DATETIME64, ENUM8, ENUM16, FIXEDSTRING, @@ -238,6 +260,9 @@ public abstract static class ColumnType implements Serializable { public abstract @Nullable Map tupleTypes(); + /** Sub-second precision (0–9) of {@code DateTime64}. {@code null} for other types. */ + public abstract @Nullable Integer precision(); + public ColumnType withNullable(boolean nullable) { return toBuilder().nullable(nullable).build(); } @@ -258,6 +283,26 @@ public static ColumnType fixedString(int size) { .build(); } + /** Default {@code DateTime64} precision in ClickHouse. */ + public static final int DEFAULT_DATETIME64_PRECISION = 3; + + /** Returns a {@code DateTime64} type with ClickHouse's default precision of 3. */ + public static ColumnType dateTime64() { + return dateTime64(DEFAULT_DATETIME64_PRECISION); + } + + public static ColumnType dateTime64(int precision) { + if (precision < 0 || precision > 9) { + throw new IllegalArgumentException( + "DateTime64 precision must be in [0, 9], got " + precision); + } + return ColumnType.builder() + .typeName(TypeName.DATETIME64) + .nullable(false) + .precision(precision) + .build(); + } + public static ColumnType enum8(Map enumValues) { return ColumnType.builder() .typeName(TypeName.ENUM8) @@ -296,13 +341,17 @@ public static ColumnType tuple(Map elements) { * * @param str string representation of ClickHouse type * @return type of ClickHouse column + * @throws IllegalArgumentException if {@code str} is not a valid ClickHouse column type */ public static ColumnType parse(String str) { try { return new org.apache.beam.sdk.io.clickhouse.impl.parser.ColumnTypeParser( new StringReader(str)) .parse(); - } catch (org.apache.beam.sdk.io.clickhouse.impl.parser.ParseException e) { + } catch (org.apache.beam.sdk.io.clickhouse.impl.parser.ParseException + | org.apache.beam.sdk.io.clickhouse.impl.parser.TokenMgrError + | IllegalArgumentException e) { + // Funnel lexical, syntactic and validation failures into one error surface. throw new IllegalArgumentException("failed to parse", e); } } @@ -367,6 +416,8 @@ abstract static class Builder { public abstract Builder tupleTypes(Map tupleElements); + public abstract Builder precision(@Nullable Integer precision); + public abstract ColumnType build(); } } diff --git a/sdks/java/io/clickhouse/src/main/javacc/ColumnTypeParser.jj b/sdks/java/io/clickhouse/src/main/javacc/ColumnTypeParser.jj index 5bb9ba4171a6..53ad991b67c3 100644 --- a/sdks/java/io/clickhouse/src/main/javacc/ColumnTypeParser.jj +++ b/sdks/java/io/clickhouse/src/main/javacc/ColumnTypeParser.jj @@ -79,6 +79,7 @@ TOKEN : { < ARRAY : "ARRAY" > | < DATE : "DATE" > + | < DATETIME64 : "DATETIME64" > | < DATETIME : "DATETIME" > | < ENUM8 : "ENUM8" > | < ENUM16 : "ENUM16" > @@ -206,6 +207,7 @@ private ColumnType primitive() : { TypeName type; String size; + ColumnType ct; } { ( @@ -214,10 +216,35 @@ private ColumnType primitive() : ( ( size = integer() ) ) { return ColumnType.fixedString(Integer.valueOf(size)); } + | + (ct = dateTime64()) { return ct; } ) } +private ColumnType dateTime64() : +{ + String precision = null; +} +{ + ( + + ( + ( precision = integer() ) + // The timezone is display-only metadata; accept the syntax and ignore the value. + ( string() )? + + )? + ) + { + // Bare DateTime64 defaults to precision 3, matching ClickHouse. + if (precision == null) { + return ColumnType.dateTime64(); + } + return ColumnType.dateTime64(Integer.parseInt(precision)); + } +} + private ColumnType nullable() : { ColumnType ct; diff --git a/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIOIT.java b/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIOIT.java index 8ce412c5f88c..da435f206842 100644 --- a/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIOIT.java +++ b/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIOIT.java @@ -32,6 +32,8 @@ import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.annotations.DefaultSchema; import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.util.ReleaseInfo; @@ -49,6 +51,18 @@ @RunWith(JUnit4.class) public class ClickHouseIOIT extends BaseClickHouseTest { + private static final long MICROS_PER_SECOND = 1_000_000L; + private static final long NANOS_PER_SECOND = 1_000_000_000L; + + // Shared DateTime64 test instant 2026-05-15T12:34:56Z; its .789012345 sub-second component + // exercises every precision bucket. + private static final long TEST_EPOCH_SECONDS = 1_778_848_496L; + // Nano-of-second; the trailing 345 is not micro-aligned. + private static final long TEST_NANOS_OF_SECOND = 789_012_345L; + // The same sub-second component truncated to whole microseconds. + private static final long TEST_MICROS_OF_SECOND = 789_012L; + private static final long TEST_MICRO_ALIGNED_NANOS_OF_SECOND = TEST_MICROS_OF_SECOND * 1_000L; + @Rule public TestPipeline pipeline = TestPipeline.create(); @Test @@ -480,6 +494,111 @@ public void testPojo() throws Exception { assertEquals(12L, sum1); } + @Test + public void testDateTime64Millis() throws Exception { + Schema schema = Schema.of(Schema.Field.of("ts", FieldType.DATETIME)); + DateTime ts = new DateTime(2026, 5, 15, 12, 34, 56, 789, DateTimeZone.UTC); + Row row = Row.withSchema(schema).addValue(ts).build(); + + executeSql("CREATE TABLE test_datetime64_ms (ts DateTime64(3, 'UTC')) ENGINE=Log"); + + pipeline.apply(Create.of(row).withRowSchema(schema)).apply(write("test_datetime64_ms")); + pipeline.run().waitUntilFinish(); + + // toUnixTimestamp64Milli returns the underlying tick count, which is the most stable thing to + // assert across CH versions (string formatting may include trailing zeros depending on + // version). + long ticks = executeQueryAsLong("SELECT toUnixTimestamp64Milli(ts) FROM test_datetime64_ms"); + assertEquals(ts.getMillis(), ticks); + } + + @Test + public void testDateTime64Micros() throws Exception { + Schema schema = Schema.of(Schema.Field.of("ts", FieldType.logicalType(SqlTypes.TIMESTAMP))); + // Micro-aligned nanos, so MicrosInstant accepts the value. + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_MICRO_ALIGNED_NANOS_OF_SECOND); + Row row = Row.withSchema(schema).addValue(ts).build(); + + executeSql("CREATE TABLE test_datetime64_us (ts DateTime64(6)) ENGINE=Log"); + + pipeline.apply(Create.of(row).withRowSchema(schema)).apply(write("test_datetime64_us")); + pipeline.run().waitUntilFinish(); + + long ticks = executeQueryAsLong("SELECT toUnixTimestamp64Micro(ts) FROM test_datetime64_us"); + assertEquals(TEST_EPOCH_SECONDS * MICROS_PER_SECOND + TEST_MICROS_OF_SECOND, ticks); + } + + @Test + public void testDateTime64Nanos() throws Exception { + // DateTime64(9) must preserve full nanosecond precision. Use NanosInstant directly + // because SqlTypes.TIMESTAMP (MicrosInstant) rejects non-micro-aligned nanos like the + // trailing 345. + Schema schema = Schema.of(Schema.Field.of("ts", FieldType.logicalType(new NanosInstant()))); + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_NANOS_OF_SECOND); + Row row = Row.withSchema(schema).addValue(ts).build(); + + executeSql("CREATE TABLE test_datetime64_ns (ts DateTime64(9)) ENGINE=Log"); + + pipeline.apply(Create.of(row).withRowSchema(schema)).apply(write("test_datetime64_ns")); + pipeline.run().waitUntilFinish(); + + long ticks = executeQueryAsLong("SELECT toUnixTimestamp64Nano(ts) FROM test_datetime64_ns"); + assertEquals(TEST_EPOCH_SECONDS * NANOS_PER_SECOND + TEST_NANOS_OF_SECOND, ticks); + } + + @Test + public void testNullableDateTime64() throws Exception { + Schema schema = + Schema.of(Schema.Field.nullable("ts", FieldType.logicalType(SqlTypes.TIMESTAMP))); + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_MICRO_ALIGNED_NANOS_OF_SECOND); + Row row1 = Row.withSchema(schema).addValue(ts).build(); + Row row2 = Row.withSchema(schema).addValue(null).build(); + + executeSql("CREATE TABLE test_nullable_datetime64 (ts Nullable(DateTime64(6))) ENGINE=Log"); + + pipeline + .apply(Create.of(row1, row2).withRowSchema(schema)) + .apply(write("test_nullable_datetime64")); + pipeline.run().waitUntilFinish(); + + long total = executeQueryAsLong("SELECT COUNT(*) FROM test_nullable_datetime64"); + long nonNull = executeQueryAsLong("SELECT COUNT(ts) FROM test_nullable_datetime64"); + assertEquals(2L, total); + assertEquals(1L, nonNull); + } + + @Test + public void testNullableDateTime64Nanos() throws Exception { + // Nullable columns take the writeNullableValue path; verify it preserves full nanosecond + // precision alongside an actual null. + Schema schema = + Schema.of(Schema.Field.nullable("ts", FieldType.logicalType(new NanosInstant()))); + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_NANOS_OF_SECOND); + Row row1 = Row.withSchema(schema).addValue(ts).build(); + Row row2 = Row.withSchema(schema).addValue(null).build(); + + executeSql("CREATE TABLE test_nullable_datetime64_ns (ts Nullable(DateTime64(9))) ENGINE=Log"); + + pipeline + .apply(Create.of(row1, row2).withRowSchema(schema)) + .apply(write("test_nullable_datetime64_ns")); + pipeline.run().waitUntilFinish(); + + long total = executeQueryAsLong("SELECT COUNT(*) FROM test_nullable_datetime64_ns"); + long nonNull = executeQueryAsLong("SELECT COUNT(ts) FROM test_nullable_datetime64_ns"); + long ticks = + executeQueryAsLong( + "SELECT toUnixTimestamp64Nano(ts) FROM test_nullable_datetime64_ns" + + " WHERE ts IS NOT NULL"); + assertEquals(2L, total); + assertEquals(1L, nonNull); + assertEquals(TEST_EPOCH_SECONDS * NANOS_PER_SECOND + TEST_NANOS_OF_SECOND, ticks); + } + @Test public void testUserAgentInQueryLog() throws Exception { Schema schema = Schema.of(Schema.Field.of("f0", FieldType.INT64)); diff --git a/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriterTest.java b/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriterTest.java new file mode 100644 index 000000000000..89f5c8b7c85f --- /dev/null +++ b/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/ClickHouseWriterTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.clickhouse; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ClickHouseWriter}. */ +@RunWith(JUnit4.class) +public class ClickHouseWriterTest { + + private static final long MICROS_PER_SECOND = 1_000_000L; + private static final long NANOS_PER_SECOND = 1_000_000_000L; + + // Shared test instant 2026-05-15T12:34:56Z; its .789012345 sub-second component exercises + // every precision bucket. + private static final long TEST_EPOCH_SECONDS = 1_778_848_496L; + // Nano-of-second; the trailing 345 is not micro-aligned. + private static final long TEST_NANOS_OF_SECOND = 789_012_345L; + // The same sub-second component truncated to whole microseconds. + private static final long TEST_MICROS_OF_SECOND = 789_012L; + private static final long TEST_MICRO_ALIGNED_NANOS_OF_SECOND = TEST_MICROS_OF_SECOND * 1_000L; + + // Long.MAX_VALUE nanoseconds past the epoch: 2262-04-11T23:47:16.854775807Z, the last + // instant representable in DateTime64(9). + private static final long MAX_NANOS_EPOCH_SECONDS = 9_223_372_036L; + private static final long MAX_NANOS_NANO_OF_SECOND = 854_775_807L; + + @Test + public void encodeDateTime64MillisFromJoda() { + DateTime jodaTs = new DateTime(2026, 5, 15, 12, 34, 56, 789, DateTimeZone.UTC); + long expectedMillis = jodaTs.getMillis(); + assertEquals(expectedMillis, ClickHouseWriter.encodeDateTime64(jodaTs.toInstant(), 3)); + } + + @Test + public void encodeDateTime64MicrosFromJavaInstant() { + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_MICRO_ALIGNED_NANOS_OF_SECOND); + long expectedMicros = TEST_EPOCH_SECONDS * MICROS_PER_SECOND + TEST_MICROS_OF_SECOND; + assertEquals(expectedMicros, ClickHouseWriter.encodeDateTime64(ts, 6)); + } + + @Test + public void encodeDateTime64NanosFromJavaInstant() { + // The non-micro-aligned trailing 345 must survive the encoding. + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_NANOS_OF_SECOND); + long expectedNanos = TEST_EPOCH_SECONDS * NANOS_PER_SECOND + TEST_NANOS_OF_SECOND; + assertEquals(expectedNanos, ClickHouseWriter.encodeDateTime64(ts, 9)); + } + + @Test + public void encodeDateTime64Precision7TruncatesBelow100Nanos() { + // Precision 7 means 100 ns ticks: .789012345 becomes 7890123 ticks, dropping the final 45. + java.time.Instant ts = + java.time.Instant.ofEpochSecond(TEST_EPOCH_SECONDS, TEST_NANOS_OF_SECOND); + long expected = TEST_EPOCH_SECONDS * 10_000_000L + 7_890_123L; + assertEquals(expected, ClickHouseWriter.encodeDateTime64(ts, 7)); + } + + @Test + public void encodeDateTime64NanosTruncatesSubNanoFromJoda() { + // Joda only carries ms precision, so encoding into nanos shifts left by 6 with no loss. + DateTime jodaTs = new DateTime(2030, 1, 1, 0, 0, 0, 123, DateTimeZone.UTC); + long expected = jodaTs.getMillis() * 1_000_000L; + assertEquals(expected, ClickHouseWriter.encodeDateTime64(jodaTs.toInstant(), 9)); + } + + @Test + public void encodeDateTime64HandlesNegativeMillisWithFloorDivision() { + // -1ms maps to (-1s, +999ms), encoded at precision 3 should be exactly -1. + org.joda.time.Instant jodaTs = new org.joda.time.Instant(-1L); + assertEquals(-1L, ClickHouseWriter.encodeDateTime64(jodaTs, 3)); + } + + @Test + public void encodeDateTime64ZeroPrecisionRoundsTowardEpochSeconds() { + java.time.Instant ts = java.time.Instant.ofEpochSecond(42L, 999_999_999L); + // Precision 0 means whole-second ticks; sub-second component is truncated. + assertEquals(42L, ClickHouseWriter.encodeDateTime64(ts, 0)); + } + + @Test + public void encodeDateTime64NanosMaxRepresentableInstant() { + java.time.Instant ts = + java.time.Instant.ofEpochSecond(MAX_NANOS_EPOCH_SECONDS, MAX_NANOS_NANO_OF_SECOND); + assertEquals(Long.MAX_VALUE, ClickHouseWriter.encodeDateTime64(ts, 9)); + } + + @Test(expected = ArithmeticException.class) + public void encodeDateTime64NanosOverflowsPastYear2262() { + // Math.multiplyExact must fail loudly instead of silently wrapping around. + java.time.Instant ts = java.time.Instant.ofEpochSecond(MAX_NANOS_EPOCH_SECONDS + 1, 0L); + ClickHouseWriter.encodeDateTime64(ts, 9); + } + + @Test(expected = ArithmeticException.class) + public void encodeDateTime64NanosOverflowsOneNanoPastMax() { + // One nanosecond past the last representable tick overflows in Math.addExact. + java.time.Instant ts = + java.time.Instant.ofEpochSecond(MAX_NANOS_EPOCH_SECONDS, MAX_NANOS_NANO_OF_SECOND + 1); + ClickHouseWriter.encodeDateTime64(ts, 9); + } + + @Test(expected = IllegalArgumentException.class) + public void encodeDateTime64RejectsUnsupportedValue() { + ClickHouseWriter.encodeDateTime64("not-a-timestamp", 3); + } + + @Test + public void encodeDateTime64RejectsNull() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, () -> ClickHouseWriter.encodeDateTime64(null, 3)); + assertEquals( + "DateTime64 requires a Joda ReadableInstant or java.time.Instant, got null", + e.getMessage()); + } +} diff --git a/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/TableSchemaTest.java b/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/TableSchemaTest.java index f560d6268afb..2ce9c27d02b1 100644 --- a/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/TableSchemaTest.java +++ b/sdks/java/io/clickhouse/src/test/java/org/apache/beam/sdk/io/clickhouse/TableSchemaTest.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.clickhouse; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import java.util.HashMap; import java.util.Map; @@ -39,6 +40,60 @@ public void testParseDateTime() { assertEquals(ColumnType.DATETIME, ColumnType.parse("DateTime")); } + @Test + public void testParseDateTime64Millis() { + assertEquals(ColumnType.dateTime64(3), ColumnType.parse("DateTime64(3)")); + } + + @Test + public void testParseDateTime64MicrosWithTimezone() { + // The timezone argument is display-only metadata; the parser accepts and ignores it. + assertEquals(ColumnType.dateTime64(6), ColumnType.parse("DateTime64(6, 'UTC')")); + } + + @Test + public void testParseBareDateTime64DefaultsToPrecision3() { + assertEquals(ColumnType.dateTime64(3), ColumnType.parse("DateTime64")); + } + + @Test + public void testParseDateTime64Nanos() { + assertEquals(ColumnType.dateTime64(9), ColumnType.parse("DateTime64(9)")); + } + + @Test + public void testParseNullableDateTime64() { + assertEquals( + ColumnType.dateTime64(6).withNullable(true), ColumnType.parse("Nullable(DateTime64(6))")); + } + + @Test + public void testParseArrayOfDateTime64() { + assertEquals( + ColumnType.array(ColumnType.dateTime64(3)), ColumnType.parse("Array(DateTime64(3))")); + } + + @Test + public void testParseDateTime64OutOfRangePrecisionFailsToParse() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> ColumnType.parse("DateTime64(10)")); + assertEquals("failed to parse", e.getMessage()); + } + + @Test + public void testParseDateTime64NegativePrecisionFailsToParse() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> ColumnType.parse("DateTime64(-1)")); + assertEquals("failed to parse", e.getMessage()); + } + + @Test + public void testParseDateTime64GarbagePrecisionFailsToParse() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> ColumnType.parse("DateTime64(abc)")); + assertEquals("failed to parse", e.getMessage()); + } + @Test public void testParseFloat32() { assertEquals(ColumnType.FLOAT32, ColumnType.parse("Float32")); @@ -198,6 +253,53 @@ public void testEquivalentSchema() { assertEquals(expected, TableSchema.getEquivalentSchema(tableSchema)); } + @Test + public void testEquivalentSchemaDateTime64Millis() { + // Precision ≤ 3 keeps the legacy Joda-backed DATETIME so that existing pipelines using + // millisecond timestamps continue to work without code changes. + TableSchema tableSchema = TableSchema.of(TableSchema.Column.of("ts", ColumnType.dateTime64(3))); + Schema expected = Schema.of(Schema.Field.of("ts", Schema.FieldType.DATETIME)); + assertEquals(expected, TableSchema.getEquivalentSchema(tableSchema)); + } + + @Test + public void testEquivalentSchemaDateTime64Micros() { + // Precision 4–6 maps to SqlTypes.TIMESTAMP (MicrosInstant) — interoperable with + // BigQueryIO and Beam SQL, sufficient for microsecond ticks. + TableSchema tableSchema = TableSchema.of(TableSchema.Column.of("ts", ColumnType.dateTime64(6))); + Schema expected = + Schema.of( + Schema.Field.of( + "ts", + Schema.FieldType.logicalType( + org.apache.beam.sdk.schemas.logicaltypes.SqlTypes.TIMESTAMP))); + assertEquals(expected, TableSchema.getEquivalentSchema(tableSchema)); + } + + @Test + public void testEquivalentSchemaDateTime64Nanos() { + // Precision 7–9 needs nanosecond precision; MicrosInstant rejects non-micro-aligned + // nanos, so the mapping must use NanosInstant. + TableSchema tableSchema = TableSchema.of(TableSchema.Column.of("ts", ColumnType.dateTime64(9))); + Schema expected = + Schema.of( + Schema.Field.of( + "ts", + Schema.FieldType.logicalType( + new org.apache.beam.sdk.schemas.logicaltypes.NanosInstant()))); + assertEquals(expected, TableSchema.getEquivalentSchema(tableSchema)); + } + + @Test(expected = IllegalArgumentException.class) + public void testDateTime64RejectsNegativePrecision() { + ColumnType.dateTime64(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testDateTime64RejectsPrecisionAboveNine() { + ColumnType.dateTime64(10); + } + @Test public void testParseTupleSingle() { Map m1 = new HashMap<>(); From d9dcdafbab51f3f436855deede73a10b2b005f52 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Mon, 15 Jun 2026 13:52:58 -0400 Subject: [PATCH 393/490] Revert "Upgrade expansion jar to java17 (#38931)" (#38965) This reverts commit b3a62ee97c7ce436fc70dfc7d13f94dd97038b1b. --- .github/actions/setup-environment-action/action.yml | 2 +- .../workflows/beam_PreCommit_Xlang_Generated_Transforms.yml | 2 +- sdks/java/expansion-service/container/Dockerfile | 2 +- sdks/java/io/expansion-service/build.gradle | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-environment-action/action.yml b/.github/actions/setup-environment-action/action.yml index daa3daa1cd52..06633c6c7279 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -74,7 +74,7 @@ runs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: ${{ inputs.java-version == 'default' && ((contains(github.job, 'Xlang') || contains(github.job, 'XVR') || contains(github.job, 'PreCommit_Java')) && '17' || '11') || inputs.java-version }} + java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: diff --git a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml index 1dfab40f552d..959f36234d70 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -102,7 +102,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: '17' + java-version: default python-version: ${{ matrix.python_version }} - name: Set PY_VER_CLEAN id: set_py_ver_clean diff --git a/sdks/java/expansion-service/container/Dockerfile b/sdks/java/expansion-service/container/Dockerfile index 513dd6b75b88..968f5cd2ac25 100644 --- a/sdks/java/expansion-service/container/Dockerfile +++ b/sdks/java/expansion-service/container/Dockerfile @@ -16,7 +16,7 @@ # limitations under the License. ############################################################################### -FROM eclipse-temurin:17 +FROM eclipse-temurin:11 LABEL Author "Apache Beam " ARG TARGETOS ARG TARGETARCH diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index f7b241a75944..70a3fce538b6 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -25,8 +25,8 @@ applyJavaNature( exportJavadoc: false, validateShadowJar: false, shadowClosure: {}, - // iceberg requires Java11+ and delta lake requires Java17+ - requireJavaVersion: JavaVersion.VERSION_17 + // iceberg requires Java11+ + requireJavaVersion: JavaVersion.VERSION_11 ) // We don't want to use the latest version for the entire beam sdk since beam Java users can override it themselves. From d2f7e823c2ea3490fedc07d7eb8ec46920c9b90c Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Mon, 15 Jun 2026 15:46:09 -0400 Subject: [PATCH 394/490] Add support for STDDEV_POP and STDDEV_SAMP in VarianceFn (#38871) * Add support for STDDEV_POP and STDDEV_SAMP in VarianceFn * Add DSL integration tests for STDDEV_POP and STDDEV_SAMP * Address review feedback: make fields final and add null check * Address review feedback: handle numerical instability and overflow in stddev * Address review feedback: return infinity on standard deviation overflow instead of throwing exception --- .../transform/BeamBuiltinAggregations.java | 2 + .../sql/impl/transform/agg/VarianceFn.java | 38 +++++++++++++--- .../BeamSqlDslAggregationVarianceTest.java | 43 ++++++++++++++++++- .../impl/transform/agg/VarianceFnTest.java | 25 ++++++++++- 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/BeamBuiltinAggregations.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/BeamBuiltinAggregations.java index 3fc299bd5a33..2800edfbb99a 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/BeamBuiltinAggregations.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/BeamBuiltinAggregations.java @@ -83,6 +83,8 @@ public class BeamBuiltinAggregations { typeName -> new DropNullFn(BeamBuiltinAggregations.createBitAnd(typeName))) .put("VAR_POP", t -> VarianceFn.newPopulation(t.getTypeName())) .put("VAR_SAMP", t -> VarianceFn.newSample(t.getTypeName())) + .put("STDDEV_POP", t -> VarianceFn.newPopulationStddev(t.getTypeName())) + .put("STDDEV_SAMP", t -> VarianceFn.newSampleStddev(t.getTypeName())) .put("COVAR_POP", t -> CovarianceFn.newPopulation(t.getTypeName())) .put("COVAR_SAMP", t -> CovarianceFn.newSample(t.getTypeName())) .put("COUNTIF", typeName -> CountIf.combineFn()) diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFn.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFn.java index dd2cd3b20952..906bac7add52 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFn.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFn.java @@ -75,8 +75,12 @@ public class VarianceFn extends Combine.CombineFn decimalConverter; + private final boolean isSample; // flag to determine return value should be Variance Pop or Sample + // When true, extractOutput returns the square root of the variance (i.e. standard deviation). + // Beam's enumerable bridge cannot translate a SQRT call layered on top of a window VAR_SAMP, so + // STDDEV_SAMP / STDDEV_POP are computed end-to-end inside this combiner instead. + private final boolean isStddev; + private final SerializableFunction decimalConverter; public static VarianceFn newPopulation(Schema.TypeName typeName) { return newPopulation(BigDecimalConverter.forSqlType(typeName)); @@ -85,7 +89,7 @@ public static VarianceFn newPopulation(Schema.TypeName typeName) { public static VarianceFn newPopulation( SerializableFunction decimalConverter) { - return new VarianceFn<>(POP, decimalConverter); + return new VarianceFn<>(POP, false, decimalConverter); } public static VarianceFn newSample(Schema.TypeName typeName) { @@ -95,11 +99,21 @@ public static VarianceFn newSample(Schema.TypeName typeName) { public static VarianceFn newSample( SerializableFunction decimalConverter) { - return new VarianceFn<>(SAMPLE, decimalConverter); + return new VarianceFn<>(SAMPLE, false, decimalConverter); } - private VarianceFn(boolean isSample, SerializableFunction decimalConverter) { + public static VarianceFn newSampleStddev(Schema.TypeName typeName) { + return new VarianceFn<>(SAMPLE, true, BigDecimalConverter.forSqlType(typeName)); + } + + public static VarianceFn newPopulationStddev(Schema.TypeName typeName) { + return new VarianceFn<>(POP, true, BigDecimalConverter.forSqlType(typeName)); + } + + private VarianceFn( + boolean isSample, boolean isStddev, SerializableFunction decimalConverter) { this.isSample = isSample; + this.isStddev = isStddev; this.decimalConverter = decimalConverter; } @@ -133,7 +147,19 @@ public Coder getAccumulatorCoder( @Override public T extractOutput(VarianceAccumulator accumulator) { - return decimalConverter.apply(getVariance(accumulator)); + BigDecimal result = getVariance(accumulator); + if (result != null && isStddev) { + double doubleVal = result.doubleValue(); + if (doubleVal < 0.0) { + doubleVal = 0.0; // Clamp negative variance due to numerical instability + } + double sqrtVal = Math.sqrt(doubleVal); + if (Double.isInfinite(sqrtVal)) { + return decimalConverter.apply(result.sqrt(MATH_CTX)); + } + result = BigDecimal.valueOf(sqrtVal); + } + return decimalConverter.apply(result); } private BigDecimal getVariance(VarianceAccumulator variance) { diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlDslAggregationVarianceTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlDslAggregationVarianceTest.java index 808b27aaac4c..e2c548acf718 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlDslAggregationVarianceTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlDslAggregationVarianceTest.java @@ -30,7 +30,10 @@ import org.junit.Rule; import org.junit.Test; -/** Integration tests for {@code VAR_POP} and {@code VAR_SAMP}. */ +/** + * Integration tests for {@code VAR_POP}, {@code VAR_SAMP}, {@code STDDEV_POP} and {@code + * STDDEV_SAMP}. + */ public class BeamSqlDslAggregationVarianceTest { private static final double PRECISION = 1e-7; @@ -94,4 +97,42 @@ public void testSampleVarianceInt() { pipeline.run().waitUntilFinish(); } + + @Test + public void testPopulationStddevDouble() { + String sql = "SELECT STDDEV_POP(f_double) FROM PCOLLECTION GROUP BY f_int2"; + + PAssert.that(boundedInput.apply(SqlTransform.query(sql))) + .satisfies(matchesScalar(5.138887357, PRECISION)); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void testPopulationStddevInt() { + String sql = "SELECT STDDEV_POP(f_int) FROM PCOLLECTION GROUP BY f_int2"; + + PAssert.that(boundedInput.apply(SqlTransform.query(sql))).satisfies(matchesScalar(5)); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void testSampleStddevDouble() { + String sql = "SELECT STDDEV_SAMP(f_double) FROM PCOLLECTION GROUP BY f_int2"; + + PAssert.that(boundedInput.apply(SqlTransform.query(sql))) + .satisfies(matchesScalar(5.550632739, PRECISION)); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void testSampleStddevInt() { + String sql = "SELECT STDDEV_SAMP(f_int) FROM PCOLLECTION GROUP BY f_int2"; + + PAssert.that(boundedInput.apply(SqlTransform.query(sql))).satisfies(matchesScalar(5)); + + pipeline.run().waitUntilFinish(); + } } diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFnTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFnTest.java index f7a8ad1fa06b..0671a3caaa68 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFnTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/VarianceFnTest.java @@ -26,6 +26,7 @@ import java.util.Arrays; import org.apache.beam.sdk.coders.CoderRegistry; import org.apache.beam.sdk.coders.VarIntCoder; +import org.apache.beam.sdk.schemas.Schema; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -51,18 +52,38 @@ public static Iterable varianceFns() { VarianceFn.newSample(BigDecimal::intValue), newVarianceAccumulator(FIFTEEN, FOUR, ZERO), 5 + }, + { + VarianceFn.newPopulationStddev(Schema.TypeName.INT32), + newVarianceAccumulator(new BigDecimal(36), new BigDecimal(4), ZERO), + 3 + }, + { + VarianceFn.newSampleStddev(Schema.TypeName.INT32), + newVarianceAccumulator(new BigDecimal(36), new BigDecimal(5), ZERO), + 3 + }, + { + VarianceFn.newPopulationStddev(Schema.TypeName.DOUBLE), + newVarianceAccumulator(new BigDecimal("1e700"), BigDecimal.ONE, ZERO), + Double.POSITIVE_INFINITY + }, + { + VarianceFn.newPopulationStddev(Schema.TypeName.FLOAT), + newVarianceAccumulator(new BigDecimal("1e700"), BigDecimal.ONE, ZERO), + Float.POSITIVE_INFINITY } }); } private VarianceFn varianceFn; private VarianceAccumulator testAccumulatorInput; - private int expectedExtractedResult; + private Object expectedExtractedResult; public VarianceFnTest( VarianceFn varianceFn, VarianceAccumulator testAccumulatorInput, - int expectedExtractedResult) { + Object expectedExtractedResult) { this.varianceFn = varianceFn; this.testAccumulatorInput = testAccumulatorInput; From 4f0858aed38cbbd1302f361043329dec671908ad Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Mon, 15 Jun 2026 15:56:06 -0400 Subject: [PATCH 395/490] Update BEAM_DEV_SDK_CONTAINER_TAG version (#38963) --- sdks/python/apache_beam/runners/dataflow/internal/names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/names.py b/sdks/python/apache_beam/runners/dataflow/internal/names.py index b2dee3c7044e..674658b8af71 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/names.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/names.py @@ -35,6 +35,6 @@ # Update this tag whenever there is a change that # requires changes to SDK harness container or SDK harness launcher. -BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260603' +BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260615' DATAFLOW_CONTAINER_IMAGE_REPOSITORY = 'gcr.io/cloud-dataflow/v1beta3' From f127d9bfe80c82c38a16bbdbd342a22e8b976f01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:21:04 -0400 Subject: [PATCH 396/490] Bump protobufjs from 8.4.0 to 8.6.0 in /sdks/typescript (#38968) Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 8.4.0 to 8.6.0. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v8.4.0...protobufjs-v8.6.0) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 8.6.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/typescript/package-lock.json | 15 +++++++-------- sdks/typescript/package.json | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index c7e294836db1..721be1ef76f6 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -19,7 +19,7 @@ "fast-deep-equal": "^3.1.3", "find-git-root": "^1.0.4", "long": "^4.0.0", - "protobufjs": "~8.4.0", + "protobufjs": "~8.6.0", "queue-typescript": "^1.0.1", "serialize-closures": "^0.2.7", "ts-closure-transform": "^0.1.7", @@ -3791,10 +3791,9 @@ } }, "node_modules/protobufjs": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", - "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", - "hasInstallScript": true, + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.0.tgz", + "integrity": "sha512-PIOO89BMGMXGz2333TVv/OqPNVWm7w30ll/4FtLbtLBaonzJMYwTbAZSSlobjIy9MoUgIAxSVUpK7aP7EpTtkg==", "dependencies": { "long": "^5.3.2" }, @@ -7398,9 +7397,9 @@ } }, "protobufjs": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", - "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.0.tgz", + "integrity": "sha512-PIOO89BMGMXGz2333TVv/OqPNVWm7w30ll/4FtLbtLBaonzJMYwTbAZSSlobjIy9MoUgIAxSVUpK7aP7EpTtkg==", "requires": { "long": "^5.3.2" }, diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index b6b16be9a82d..a9468b04ac8b 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -47,7 +47,7 @@ "fast-deep-equal": "^3.1.3", "find-git-root": "^1.0.4", "long": "^4.0.0", - "protobufjs": "~8.4.0", + "protobufjs": "~8.6.0", "queue-typescript": "^1.0.1", "serialize-closures": "^0.2.7", "ts-closure-transform": "^0.1.7", From 1023fd27f6f02047fd3773b94368a92e32cb328b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:23:48 -0400 Subject: [PATCH 397/490] Bump form-data from 2.5.5 to 2.5.6 in /sdks/typescript (#38967) Bumps [form-data](https://github.com/form-data/form-data) from 2.5.5 to 2.5.6. - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v2.5.5...v2.5.6) --- updated-dependencies: - dependency-name: form-data dependency-version: 2.5.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/typescript/package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index 721be1ef76f6..d84040ab71c5 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -2180,14 +2180,14 @@ } }, "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -2744,9 +2744,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dependencies": { "function-bind": "^1.1.2" }, @@ -6227,14 +6227,14 @@ } }, "form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } @@ -6637,9 +6637,9 @@ } }, "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "requires": { "function-bind": "^1.1.2" } From b73bd6fd6d16781f406baae0684843ee6c520630 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Mon, 15 Jun 2026 22:42:19 -0400 Subject: [PATCH 398/490] Use Flink 2.0 for load tests (#38279) --- .github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml | 4 ++-- .github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml | 4 ++-- .github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml | 4 ++-- .github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml | 4 ++-- .../workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml | 4 ++-- .../workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml | 4 ++-- .../workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml | 4 ++-- .../beam_LoadTests_Python_Combine_Flink_Streaming.yml | 4 ++-- .github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml | 4 ++-- .../workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml | 4 ++-- .../beam_LoadTests_Python_ParDo_Flink_Streaming.yml | 4 ++-- .github/workflows/beam_PreCommit_Flink_Container.yml | 4 ++-- .github/workflows/beam_Publish_Docker_Snapshots.yml | 4 ++-- .test-infra/dataproc/flink_cluster.sh | 6 +++--- 14 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml index eb5d44521229..7efb22da4170 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-cogbk-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-cogbk-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml index 4978cd778275..044e987556ee 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-combine-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-combine-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml index 74181f1708da..174aa25e5431 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-gbk-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-gbk-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml index 1055b18b119a..7a02fd601290 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-pardo-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-pardo-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml index 1c63c632ee19..22b66ad4c805 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-sideinput-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-sideinput-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml index 429eed69f182..c603651a57ea 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-cogbk-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-cogbk-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml index 17ac98ffd804..13168a002065 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-cmb-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-cmb-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml index 0ea3876371b2..57e8b14df1be 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-cmb-flink-streaming-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-cmb-flink-streaming-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml index 7dfc9c2a5403..a792bf3ec720 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-gbk-flk-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-gbk-flk-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml index 06c3f8479f60..3fefa634d2f6 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-pardo-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-pardo-flink-batch-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml index b63dc9596e70..26896710a8e3 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-pardo-flink-stream-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-pardo-flink-stream-${{ github.run_id }} jobs: diff --git a/.github/workflows/beam_PreCommit_Flink_Container.yml b/.github/workflows/beam_PreCommit_Flink_Container.yml index 0febb8981176..9d4ff5e9636c 100644 --- a/.github/workflows/beam_PreCommit_Flink_Container.yml +++ b/.github/workflows/beam_PreCommit_Flink_Container.yml @@ -70,12 +70,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-precommit-flink-container-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-precommit-flink-container-${{ github.run_id }} DOCKER_REGISTRY: gcr.io DOCKER_REPOSITORY_ROOT: ${{ github.event_name == 'pull_request_target' && 'gcr.io/apache-beam-testing/beam-sdk-pr' || 'gcr.io/apache-beam-testing/beam-sdk' }} diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml b/.github/workflows/beam_Publish_Docker_Snapshots.yml index d4aa75286116..5e4412b2650c 100644 --- a/.github/workflows/beam_Publish_Docker_Snapshots.yml +++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml @@ -84,10 +84,10 @@ jobs: -Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \ -Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \ -Pdocker-pull-licenses - - name: run Publish Docker Snapshots script for Flink 1.17 + - name: run Publish Docker Snapshots script for Flink 2.0 uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:1.17:job-server-container:dockerPush + gradle-command: :runners:flink:2.0:job-server-container:dockerPush arguments: | -Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \ -Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \ diff --git a/.test-infra/dataproc/flink_cluster.sh b/.test-infra/dataproc/flink_cluster.sh index 4a97850f5ac1..0cb76c815d55 100755 --- a/.test-infra/dataproc/flink_cluster.sh +++ b/.test-infra/dataproc/flink_cluster.sh @@ -17,7 +17,7 @@ # Provide the following environment to run this script: # # GCLOUD_ZONE: Google cloud zone. Optional. Default: "us-central1-a" -# DATAPROC_VERSION: Dataproc version. Optional. Default: 2.2 +# DATAPROC_VERSION: Dataproc version. Optional. Default: 3.0-debian # CLUSTER_NAME: Cluster name # GCS_BUCKET: GCS bucket url for Dataproc resources (init actions) # HARNESS_IMAGES_TO_PULL: Urls to SDK Harness' images to pull on dataproc workers (optional: 0, 1 or multiple urls for every harness image) @@ -35,7 +35,7 @@ # HARNESS_IMAGES_TO_PULL='gcr.io//python:latest gcr.io//java:latest' \ # JOB_SERVER_IMAGE=gcr.io//job-server-flink:latest \ # ARTIFACTS_DIR=gs:// \ -# FLINK_DOWNLOAD_URL=https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz \ +# FLINK_DOWNLOAD_URL=https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz \ # HADOOP_DOWNLOAD_URL=https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-9.0.jar \ # FLINK_NUM_WORKERS=2 \ # FLINK_TASKMANAGER_SLOTS=1 \ @@ -46,7 +46,7 @@ set -Eeuxo pipefail # GCloud properties GCLOUD_ZONE="${GCLOUD_ZONE:=us-central1-a}" -DATAPROC_VERSION="${DATAPROC_VERSION:=2.2-debian}" +DATAPROC_VERSION="${DATAPROC_VERSION:=3.0-debian}" GCLOUD_REGION=`echo $GCLOUD_ZONE | sed -E "s/(-[a-z])?$//"` MASTER_NAME="$CLUSTER_NAME-m" From 70e3759a395553684602d6be0b5f02adc9cfaa4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 07:31:53 -0400 Subject: [PATCH 399/490] Bump cloud.google.com/go/spanner from 1.91.0 to 1.92.0 in /sdks (#38976) Bumps [cloud.google.com/go/spanner](https://github.com/googleapis/google-cloud-go) from 1.91.0 to 1.92.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/spanner/v1.91.0...spanner/v1.92.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/spanner dependency-version: 1.92.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 1d6c461baee4..bc427296c6fa 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -30,7 +30,7 @@ require ( cloud.google.com/go/datastore v1.24.0 cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 - cloud.google.com/go/spanner v1.91.0 + cloud.google.com/go/spanner v1.92.0 cloud.google.com/go/storage v1.62.3 github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.25 diff --git a/sdks/go.sum b/sdks/go.sum index 49edbd2680d1..2c8a4867ae6c 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -90,8 +90,8 @@ cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkN cloud.google.com/go/pubsub/v2 v2.6.0 h1:8pjR0id+GTB+krKx5G6AGJoYrHog58w2Q89PCOrfM64= cloud.google.com/go/pubsub/v2 v2.6.0/go.mod h1:4anqvV/w8Pcgu2tO0qr2XgsF3GXHowzryfQ5gOnVmWY= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= -cloud.google.com/go/spanner v1.91.0 h1:XwXfcZ0kc1NT9Uu2IsThFiWtYptB+WgLn/KZEZcyzRg= -cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= +cloud.google.com/go/spanner v1.92.0 h1:cfeMNmtFjz+OYzQVCIuGBw4Cik4CbF2ptXMuRQcUar0= +cloud.google.com/go/spanner v1.92.0/go.mod h1:rCDPfWXNX0h+t484r+crCEaaMKbJfoWkHRDKU3H3+oY= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= From 210d22dfb87bbd5f0ae808e830431f1873f8a343 Mon Sep 17 00:00:00 2001 From: tejasiyer-dev Date: Tue, 16 Jun 2026 05:15:55 -0700 Subject: [PATCH 400/490] Fix AsyncWrapperTest timeout and flakiness issues. (#38970) * Fix AsyncWrapperTest timeout and flakiness issues. * Fix AsyncWrapperTest to prevent new race conditions and premature timeouts. --- .../beam/sdk/transforms/AsyncWrapperTest.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/AsyncWrapperTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/AsyncWrapperTest.java index e95f586e8849..183b1851459c 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/AsyncWrapperTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/AsyncWrapperTest.java @@ -230,21 +230,20 @@ private void waitForEmpty(AsyncWrapper asyncWrapper) { } private void waitForEmpty(AsyncWrapper asyncWrapper, int timeoutSeconds) { - int count = 0; + long limit = System.currentTimeMillis() + timeoutSeconds * 1000L; while (!asyncWrapper.isEmpty()) { + if (System.currentTimeMillis() > limit) { + throw new RuntimeException("Timed out waiting for async dofn to be empty"); + } try { - Thread.sleep(1000); + Thread.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } - count += 1; - if (count > timeoutSeconds) { - throw new RuntimeException("Timed out waiting for async dofn to be empty"); - } } try { - Thread.sleep(1000); + Thread.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -419,7 +418,7 @@ public void testMultiKey() { // execution task has not finished processing yet. @Test public void testLongItem() { - BasicDofn dofn = new BasicDofn(1000); + BasicDofn dofn = new BasicDofn(500); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); @@ -438,7 +437,7 @@ public void testLongItem() { assertEquals(0, dofn.getProcessed()); assertEquals(1, fakeBagState.items.size()); - waitForEmpty(asyncWrapper, 20); + waitForEmpty(asyncWrapper, 2); result = asyncWrapper.commitFinishedItemsDirect( @@ -538,7 +537,7 @@ public void testMultiElementDofn() { // Identical elements should not spawn multiple concurrent background executions. @Test public void testDuplicates() { - BasicDofn dofn = new BasicDofn(1000); + BasicDofn dofn = new BasicDofn(10); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); @@ -568,7 +567,7 @@ public void testDuplicates() { // has cleared are correctly tracked and processed. @Test public void testSlowDuplicates() { - BasicDofn dofn = new BasicDofn(5000); + BasicDofn dofn = new BasicDofn(20); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); @@ -581,7 +580,7 @@ public void testSlowDuplicates() { asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); try { - Thread.sleep(10000); + Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -610,7 +609,7 @@ public void testSlowDuplicates() { // and decrement immediately upon execution completion. @Test public void testBufferCount() { - BasicDofn dofn = new BasicDofn(1000); + BasicDofn dofn = new BasicDofn(10); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); @@ -637,7 +636,7 @@ public void testBufferCount() { // the scheduler must block and delay submissions appropriately. @Test public void testBufferStopsAcceptingItems() { - BasicDofn dofn = new BasicDofn(1000); + BasicDofn dofn = new BasicDofn(500); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, @@ -670,7 +669,7 @@ public void testBufferStopsAcceptingItems() { } try { - Thread.sleep(200); + Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -707,7 +706,7 @@ public void testBufferStopsAcceptingItems() { // Verifies actively cancelled elements are cleanly dropped from the buffer during throttling. @Test public void testBufferWithCancellation() { - BasicDofn dofn = new BasicDofn(1000); + BasicDofn dofn = new BasicDofn(10); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); @@ -746,7 +745,7 @@ public void testBufferWithCancellation() { // across multiple keys correctly under heavy multi-threaded load. @Test public void testLoadCorrectness() { - BasicDofn dofn = new BasicDofn(1000); + BasicDofn dofn = new BasicDofn(10); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, @@ -791,14 +790,14 @@ public void testLoadCorrectness() { timers.get(key)); })); try { - Thread.sleep(random.nextInt(200)); + Thread.sleep(random.nextInt(2)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } try { - Thread.sleep(3000 + random.nextInt(2000)); + Thread.sleep(1000 + random.nextInt(1000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -834,7 +833,7 @@ public void testLoadCorrectness() { } } try { - Thread.sleep(1000 + random.nextInt(2000)); + Thread.sleep(10 + random.nextInt(20)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -854,7 +853,7 @@ public void testLoadCorrectness() { // must complete cleanly without thread or lock deadlocks. @Test public void testResetStateConcurrentTeardown() { - BasicDofn dofn = new BasicDofn(500); + BasicDofn dofn = new BasicDofn(10); AsyncWrapper asyncWrapper = new AsyncWrapper<>( dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); @@ -867,7 +866,7 @@ public void testResetStateConcurrentTeardown() { KV.of("key1", "1"), GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); try { - Thread.sleep(50); + Thread.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } From e2a741feb4d730e2dce4499ee139a59be795494e Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Tue, 16 Jun 2026 15:59:44 +0200 Subject: [PATCH 401/490] fix failure in opentelemetry-gcp-auth-extension jar (#38938) --- ...igure.spi.AutoConfigurationCustomizerProvider | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider deleted file mode 100644 index bf1ba2cad985..000000000000 --- a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider +++ /dev/null @@ -1,16 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider From c3bee97ec4a028bcb144a6b802fd8029092f9263 Mon Sep 17 00:00:00 2001 From: claudevdm <33973061+claudevdm@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:15:42 -0400 Subject: [PATCH 402/490] Bump jsonpickle upper bound. (#38769) * Raise jsonpickle upper bound. * add compat guard * Increase lower bound * remove compat check * remove import --- sdks/python/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/setup.py b/sdks/python/setup.py index aabe0395f6dd..027b3f65039c 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -426,7 +426,7 @@ def get_portability_package_data(): 'fasteners>=0.3,<1.0', 'grpcio>=1.33.1,<2,!=1.48.0,!=1.59.*,!=1.60.*,!=1.61.*,!=1.62.0,!=1.62.1,!=1.66.*,!=1.67.*,!=1.68.*,!=1.69.*,!=1.70.*', # pylint: disable=line-too-long 'httplib2>=0.8,<0.32.0', - 'jsonpickle>=3.0.0,<4.0.0', + 'jsonpickle>=3.0.4,<5.0.0', # numpy can have breaking changes in minor versions. # Use a strict upper bound. 'numpy>=1.14.3,<2.5.0', # Update pyproject.toml as well. From 2eaa88ea74d0b4a66c4f8f87f876eab9e18c46e0 Mon Sep 17 00:00:00 2001 From: scwhittle Date: Tue, 16 Jun 2026 20:30:09 +0200 Subject: [PATCH 403/490] [Dataflow Java] Clarify which portions of DataflowWorkerLoggingOptions are deprecated (#38960) Clarify which portions of DataflowWorkerLoggingOptions are deprecated in preference of equivalent fields in SdkHarnessOptions --- .../options/DataflowWorkerLoggingOptions.java | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowWorkerLoggingOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowWorkerLoggingOptions.java index ab412d6cfabd..89d9f085063c 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowWorkerLoggingOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowWorkerLoggingOptions.java @@ -30,13 +30,11 @@ /** * Options that are used to control logging configuration on the Dataflow worker. * - * @deprecated This interface will no longer be the source of truth for worker logging configuration - * once jobs are executed using a dedicated SDK harness instead of user code being co-located - * alongside Dataflow worker code. Consider set corresponding options within {@link - * org.apache.beam.sdk.options.SdkHarnessOptions} to ensure forward compatibility. + *

      Some options in this interface are no longer the source of truth for worker logging + * configuration. Consider using the corresponding options within {@link + * org.apache.beam.sdk.options.SdkHarnessOptions} to ensure compatibility with other runners. */ @Description("Options that are used to control logging configuration on the Dataflow worker.") -@Deprecated public interface DataflowWorkerLoggingOptions extends PipelineOptions { /** The set of log levels that can be used on the Dataflow worker. */ enum Level { @@ -59,11 +57,24 @@ enum Level { TRACE } - /** This option controls the default log level of all loggers without a log level override. */ + /** + * This option controls the default log level of all loggers without a log level override. + * + * @deprecated Prefer {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getDefaultSdkHarnessLogLevel()} which works + * across runners. + */ @Description("Controls the default log level of all loggers without a log level override.") @Default.Enum("INFO") + @Deprecated Level getDefaultWorkerLogLevel(); + /** + * @deprecated Prefer {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getDefaultSdkHarnessLogLevel()} which works + * across runners. + */ + @Deprecated void setDefaultWorkerLogLevel(Level level); /** @@ -104,14 +115,25 @@ enum Level { *

      Note that the message may be filtered depending on the {@link #getDefaultWorkerLogLevel * defaultWorkerLogLevel} or if a {@code System.out} override is specified via {@link * #getWorkerLogLevelOverrides workerLogLevelOverrides}. + * + * @deprecated Prefer using {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getSdkHarnessLogLevelOverrides()} to override + * the 'System.out' logger as this works across runners. */ @Description( "Controls the log level given to messages printed to System.out. Note that the " + "message may be filtered depending on the defaultWorkerLogLevel or if a 'System.out' " + "override is specified via workerLogLevelOverrides.") @Default.Enum("INFO") + @Deprecated Level getWorkerSystemOutMessageLevel(); + /** + * @deprecated Prefer using {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getSdkHarnessLogLevelOverrides()} to override + * the 'System.out' logger as this works across runners. + */ + @Deprecated void setWorkerSystemOutMessageLevel(Level level); /** @@ -120,14 +142,25 @@ enum Level { *

      Note that the message may be filtered depending on the {@link #getDefaultWorkerLogLevel * defaultWorkerLogLevel} or if a {@code System.err} override is specified via {@link * #getWorkerLogLevelOverrides workerLogLevelOverrides}. + * + * @deprecated Prefer using {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getSdkHarnessLogLevelOverrides()} to override + * the 'System.err' logger as this works across runners. */ @Description( "Controls the log level given to messages printed to System.err. Note that the " + "message may be filtered depending on the defaultWorkerLogLevel or if a 'System.err' " + "override is specified via workerLogLevelOverrides.") @Default.Enum("ERROR") + @Deprecated Level getWorkerSystemErrMessageLevel(); + /** + * @deprecated Prefer using {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getSdkHarnessLogLevelOverrides()} to override + * the 'System.err' logger as this works across runners. + */ + @Deprecated void setWorkerSystemErrMessageLevel(Level level); /** @@ -138,6 +171,10 @@ enum Level { *

      See {@link WorkerLogLevelOverrides} for more information on how to configure logging on a * per {@link Class}, {@link Package}, or name basis. If used from the command line, the expected * format is {"Name":"Level",...}, further details on {@link WorkerLogLevelOverrides#from}. + * + * @deprecated Prefer using {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getSdkHarnessLogLevelOverrides()} which works + * across runners. */ @Description( "This option controls the log levels for specifically named loggers. " @@ -149,8 +186,15 @@ enum Level { + "level. System.out and System.err levels are configured via loggers of the corresponding " + "name. Also, note that when multiple overrides are specified, the exact name followed by " + "the closest parent takes precedence.") + @Deprecated WorkerLogLevelOverrides getWorkerLogLevelOverrides(); + /** + * @deprecated Prefer using {@link + * org.apache.beam.sdk.options.SdkHarnessOptions#getSdkHarnessLogLevelOverrides()} which works + * across runners. + */ + @Deprecated void setWorkerLogLevelOverrides(WorkerLogLevelOverrides value); /** From 5a8efa4118a43f350a8f4bb7c0058b0a62bff98e Mon Sep 17 00:00:00 2001 From: Nikita Grover <145201799+nikitagrover19@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:07:29 +0530 Subject: [PATCH 404/490] Fix WriteToPubSub to pass ordering_key to publish() method (#37345) * Fix WriteToPubSub to pass ordering_key to publish() method Fixes #36201 * Update pubsub_test.py * Update pubsub_test.py * Trigger CI rerun * Retry CI (flake) * Apply yapf formatting * Address review comments: use message_to_proto_str and skip ordering key test on Dataflow * Add Dataflow warning in WriteToPubSub.expand() for ordering key support * Update pubsub_integration_test.py * Update PR and modification in beam_PostCommit_Python.json Updated PR number and modification count. * Update pubsub_integration_test.py * Update pubsub_integration_test.py * Fix PubSub error * Update pubsub_integration_test.py * Fix formatting: remove trailing whitespace * Fix ordering key integration test: retry pull loop, fix indentation * Fix import order: google imports before apache_beam (isort) * Fix import ordering in pubsub_integration_test.py * Simplify publish kwargs, conditionally enable message ordering, add retry loop in integration test * Use with_attributes to initialize with_ordering and apply in PublisherClient setup * Fix PubSub tests: enable message ordering in PublisherClient to exercise full ordering flow * Rename to publish_with_ordering_key, gate Dataflow warning, fix test assertions --------- Co-authored-by: Nikita Grover --- .../trigger_files/beam_PostCommit_Python.json | 4 +- .../beam/sdk/io/gcp/pubsub/ExternalWrite.java | 6 ++ .../beam/sdk/io/gcp/pubsub/PubsubIO.java | 5 +- .../apache_beam/io/external/gcp/pubsub.py | 11 ++- sdks/python/apache_beam/io/gcp/pubsub.py | 48 +++++++--- .../io/gcp/pubsub_integration_test.py | 90 +++++++++++++++++++ sdks/python/apache_beam/io/gcp/pubsub_test.py | 72 +++++++++++++++ 7 files changed, 219 insertions(+), 17 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index def2ac984082..11064375d62e 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "pr": "38069", + "pr": "37345", "modification": 49 -} +} diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java index 658d1fc29e32..23b14b68a08b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java @@ -53,6 +53,7 @@ public static class Configuration { private String topic; private @Nullable String idAttribute; private @Nullable String timestampAttribute; + private boolean publishWithOrderingKey = false; public void setTopic(String topic) { this.topic = topic; @@ -65,6 +66,10 @@ public void setIdLabel(@Nullable String idAttribute) { public void setTimestampAttribute(@Nullable String timestampAttribute) { this.timestampAttribute = timestampAttribute; } + + public void setPublishWithOrderingKey(Boolean publishWithOrderingKey) { + this.publishWithOrderingKey = publishWithOrderingKey != null && publishWithOrderingKey; + } } public static class WriteBuilder @@ -85,6 +90,7 @@ public PTransform, PDone> buildExternal(Configuration config if (config.timestampAttribute != null) { writeBuilder.setTimestampAttribute(config.timestampAttribute); } + writeBuilder.setPublishWithOrderingKey(config.publishWithOrderingKey); writeBuilder.setDynamicDestinations(false); return writeBuilder.build(); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java index d62d294ed2a7..57005745044b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java @@ -1727,7 +1727,10 @@ public void startBundle(StartBundleContext c) throws IOException { this.pubsubClient = getPubsubClientFactory() .newClient( - getTimestampAttribute(), null, c.getPipelineOptions().as(PubsubOptions.class)); + getTimestampAttribute(), + null, + c.getPipelineOptions().as(PubsubOptions.class), + Write.this.getPubsubRootUrl()); } @ProcessElement diff --git a/sdks/python/apache_beam/io/external/gcp/pubsub.py b/sdks/python/apache_beam/io/external/gcp/pubsub.py index a2a3430f9a1a..3125c0422275 100644 --- a/sdks/python/apache_beam/io/external/gcp/pubsub.py +++ b/sdks/python/apache_beam/io/external/gcp/pubsub.py @@ -117,6 +117,7 @@ def expand(self, pbegin): # this is not implemented yet on the Java side: # ('with_attributes', bool), ('timestamp_attribute', typing.Optional[str]), + ('publish_with_ordering_key', bool), ]) @@ -135,6 +136,7 @@ def __init__( with_attributes=False, id_label=None, timestamp_attribute=None, + publish_with_ordering_key=False, expansion_service=None): """Initializes ``WriteToPubSub``. @@ -150,18 +152,23 @@ def __init__( in a ReadFromPubSub PTransform to deduplicate messages. timestamp_attribute: If set, will set an attribute for each Cloud Pub/Sub message with the given name and the message's publish time as the value. + publish_with_ordering_key: If True, enables ordering key support when + publishing messages. The ordering key must be set on each + PubsubMessage via the ``ordering_key`` attribute. """ self.params = WriteToPubsubSchema( topic=topic, id_label=id_label, # with_attributes=with_attributes, - timestamp_attribute=timestamp_attribute) + timestamp_attribute=timestamp_attribute, + publish_with_ordering_key=publish_with_ordering_key) self.expansion_service = expansion_service self.with_attributes = with_attributes def expand(self, pvalue): if self.with_attributes: - pcoll = pvalue | 'ToProto' >> Map(pubsub.WriteToPubSub.to_proto_str) + pcoll = pvalue | 'ToProto' >> Map( + pubsub.WriteToPubSub.message_to_proto_str) else: pcoll = pvalue | 'ToProto' >> Map( lambda x: pubsub.PubsubMessage(x, {})._to_proto_str()) diff --git a/sdks/python/apache_beam/io/gcp/pubsub.py b/sdks/python/apache_beam/io/gcp/pubsub.py index 276103f52760..55856f504787 100644 --- a/sdks/python/apache_beam/io/gcp/pubsub.py +++ b/sdks/python/apache_beam/io/gcp/pubsub.py @@ -31,8 +31,9 @@ """ # pytype: skip-file - +import logging import re +import time from typing import Any from typing import NamedTuple from typing import Optional @@ -388,7 +389,8 @@ def __init__( topic: str, with_attributes: bool = False, id_label: Optional[str] = None, - timestamp_attribute: Optional[str] = None) -> None: + timestamp_attribute: Optional[str] = None, + publish_with_ordering_key: bool = False) -> None: """Initializes ``WriteToPubSub``. Args: @@ -404,9 +406,13 @@ def __init__( in a ReadFromPubSub PTransform to deduplicate messages. timestamp_attribute: If set, will set an attribute for each Cloud Pub/Sub message with the given name and the message's publish time as the value. + publish_with_ordering_key: If True, enables message ordering on the + PublisherClient. Messages with an ordering_key will be delivered + in order. Requires messages to have ordering_key set. """ super().__init__() self.with_attributes = with_attributes + self.publish_with_ordering_key = publish_with_ordering_key self.id_label = id_label self.timestamp_attribute = timestamp_attribute self.project, self.topic_name = parse_topic(topic) @@ -430,7 +436,16 @@ def bytes_to_proto_str(element: Union[bytes, str]) -> bytes: def expand(self, pcoll): # Store pipeline options for use in DoFn self.pipeline_options = pcoll.pipeline.options if pcoll.pipeline else None - + # Warn Dataflow users to use the XLang path for ordering key support, + # since _PubSubWriteDoFn._flush() is not used by Dataflow's implementation. + runner = self.pipeline_options.get_all_options().get( + 'runner', '') if self.pipeline_options else '' + if 'Dataflow' in str(runner) and self.publish_with_ordering_key: + logging.warning( + 'WriteToPubSub ordering_key support is not available on Dataflow ' + 'via this transform. Use the XLang WriteToPubSub path instead: ' + 'apache_beam.io.external.gcp.pubsub.WriteToPubSub with ' + 'publish_with_ordering_key=True.') if self.with_attributes: pcoll = pcoll | 'ToProtobufX' >> ParDo( _AddMetricsAndMap( @@ -457,6 +472,9 @@ def display_data(self): True, label='With Attributes').drop_if_none(), 'timestamp_attribute': DisplayDataItem( self.timestamp_attribute, label='Timestamp Attribute'), + 'publish_with_ordering_key': DisplayDataItem( + self.publish_with_ordering_key, + label='Publish With Ordering Key').drop_if_none(), } @@ -563,6 +581,7 @@ def __init__(self, transform): self.id_label = transform.id_label self.timestamp_attribute = transform.timestamp_attribute self.with_attributes = transform.with_attributes + self.with_ordering = transform.publish_with_ordering_key # TODO(https://github.com/apache/beam/issues/18939): Add support for # id_label and timestamp_attribute. @@ -597,7 +616,7 @@ def __init__(self, transform): output_labels_supported = False # Log debug information for troubleshooting - import logging + runner_info = getattr( pipeline_options, 'runner', 'None') if pipeline_options else 'No options' @@ -628,7 +647,13 @@ def __init__(self, transform): def setup(self): from google.cloud import pubsub - self._pub_client = pubsub.PublisherClient() + if self.with_ordering: + self._pub_client = pubsub.PublisherClient( + publisher_options=pubsub.types.PublisherOptions( + enable_message_ordering=True, + )) + else: + self._pub_client = pubsub.PublisherClient() self._topic = self._pub_client.topic_path( self.project, self.short_topic_name) @@ -647,8 +672,6 @@ def _flush(self): if not self._buffer: return - import time - # The elements in buffer are serialized protobuf bytes from the previous # transforms. We need to deserialize them to extract data and attributes. futures = [] @@ -656,12 +679,13 @@ def _flush(self): # Deserialize the protobuf to get the original PubsubMessage pubsub_msg = PubsubMessage._from_proto_str(elem) - # Publish with the correct data and attributes + # Publish with the correct data, attributes, and ordering_key + kwargs = {} if self.with_attributes and pubsub_msg.attributes: - future = self._pub_client.publish( - self._topic, pubsub_msg.data, **pubsub_msg.attributes) - else: - future = self._pub_client.publish(self._topic, pubsub_msg.data) + kwargs.update(pubsub_msg.attributes) + if pubsub_msg.ordering_key: + kwargs['ordering_key'] = pubsub_msg.ordering_key + future = self._pub_client.publish(self._topic, pubsub_msg.data, **kwargs) futures.append(future) diff --git a/sdks/python/apache_beam/io/gcp/pubsub_integration_test.py b/sdks/python/apache_beam/io/gcp/pubsub_integration_test.py index 8387fe734fc1..e67c5f2a3708 100644 --- a/sdks/python/apache_beam/io/gcp/pubsub_integration_test.py +++ b/sdks/python/apache_beam/io/gcp/pubsub_integration_test.py @@ -305,6 +305,96 @@ def test_batch_write_with_attributes(self): """Test WriteToPubSub in batch mode with attributes.""" self._test_batch_write(with_attributes=True) + @pytest.mark.it_postcommit + def test_batch_write_with_ordering_key(self): + """Test WriteToPubSub in batch mode with ordering keys. + + Dataflow's Native Pub/Sub Sink does not support ordering_key + (see https://github.com/apache/beam/issues/36201), so this test + only applies to runners using Beam's Python WriteToPubSub Sink. + Dataflow users should use the XLang WriteToPubSub path instead + (apache_beam.io.external.gcp.pubsub.WriteToPubSub with + publish_with_ordering_key=True). + """ + if self.runner_name == 'TestDataflowRunner': + self.skipTest( + 'Dataflow Native PubSub Sink does not support ordering_key ' + '(see https://github.com/apache/beam/issues/36201).') + from google.pubsub_v1.types import Subscription + + from apache_beam.options.pipeline_options import PipelineOptions + from apache_beam.options.pipeline_options import StandardOptions + from apache_beam.transforms import Create + + ordering_topic = self.pub_client.create_topic( + name=self.pub_client.topic_path( + self.project, 'psit_topic_ordering' + self.uuid)) + ordering_sub = self.sub_client.create_subscription( + request=Subscription( + name=self.sub_client.subscription_path( + self.project, 'psit_sub_ordering' + self.uuid), + topic=ordering_topic.name, + enable_message_ordering=True, + )) + time.sleep(10) + + try: + test_messages = [ + PubsubMessage( + b'order_data001', {'attr': 'value1'}, ordering_key='key1'), + PubsubMessage( + b'order_data002', {'attr': 'value2'}, ordering_key='key1'), + PubsubMessage( + b'order_data003', {'attr': 'value3'}, ordering_key='key2'), + ] + + pipeline_options = PipelineOptions() + pipeline_options.view_as(StandardOptions).streaming = False + + with TestPipeline(options=pipeline_options) as p: + messages = p | 'CreateMessages' >> Create(test_messages) + _ = messages | 'WriteToPubSub' >> WriteToPubSub( + ordering_topic.name, + with_attributes=True, + publish_with_ordering_key=True) + + time.sleep(10) + + # Retry pulling to handle PubSub delivery delays + received_messages = [] + deadline = time.time() + 60 # wait up to 60 seconds + while time.time() < deadline: + response = self.sub_client.pull( + request={ + 'subscription': ordering_sub.name, + 'max_messages': 10, + }) + received_messages.extend(response.received_messages) + if len(received_messages) >= len(test_messages): + break + time.sleep(5) + + self.assertEqual(len(received_messages), len(test_messages)) + + received_map = { + msg.message.data: msg.message + for msg in received_messages + } + self.assertEqual(received_map[b'order_data001'].ordering_key, 'key1') + self.assertEqual(received_map[b'order_data002'].ordering_key, 'key1') + self.assertEqual(received_map[b'order_data003'].ordering_key, 'key2') + + ack_ids = [msg.ack_id for msg in received_messages] + self.sub_client.acknowledge( + request={ + 'subscription': ordering_sub.name, + 'ack_ids': ack_ids, + }) + finally: + self.sub_client.delete_subscription( + request={'subscription': ordering_sub.name}) + self.pub_client.delete_topic(request={'topic': ordering_topic.name}) + if __name__ == '__main__': logging.getLogger().setLevel(logging.DEBUG) diff --git a/sdks/python/apache_beam/io/gcp/pubsub_test.py b/sdks/python/apache_beam/io/gcp/pubsub_test.py index 5650e920e635..c35de62fca1f 100644 --- a/sdks/python/apache_beam/io/gcp/pubsub_test.py +++ b/sdks/python/apache_beam/io/gcp/pubsub_test.py @@ -467,6 +467,7 @@ def test_display_data(self): DisplayDataItemMatcher('id_label', 'id'), DisplayDataItemMatcher('with_attributes', True), DisplayDataItemMatcher('timestamp_attribute', 'time'), + DisplayDataItemMatcher('publish_with_ordering_key', False), ] hc.assert_that(dd.items, hc.contains_inanyorder(*expected_items)) @@ -1098,6 +1099,77 @@ def test_write_to_pubsub_with_attributes_no_overwrite(self, unused_mock): Lineage.query(p.result.metrics(), Lineage.SINK), set(["pubsub:topic:fakeprj.a_topic"])) + def test_write_messages_with_ordering_key(self, mock_pubsub): + """Test WriteToPubSub with ordering_key in messages.""" + data = b'data' + ordering_key = 'order-123' + attributes = {'key': 'value'} + payloads = [PubsubMessage(data, attributes, ordering_key=ordering_key)] + + options = PipelineOptions([]) + options.view_as(StandardOptions).streaming = True + with TestPipeline(options=options) as p: + _ = ( + p + | Create(payloads) + | WriteToPubSub( + 'projects/fakeprj/topics/a_topic', + with_attributes=True, + publish_with_ordering_key=True)) + + # Verify that publish was called with ordering_key + mock_pubsub.return_value.publish.assert_called() + call_args = mock_pubsub.return_value.publish.call_args + + # Check that ordering_key was passed as a keyword argument + self.assertIn('ordering_key', call_args.kwargs) + self.assertEqual(call_args.kwargs['ordering_key'], ordering_key) + + def test_write_messages_with_ordering_key_no_attributes(self, mock_pubsub): + """Test WriteToPubSub with ordering_key but no attributes.""" + data = b'data' + ordering_key = 'order-456' + payloads = [PubsubMessage(data, None, ordering_key=ordering_key)] + + options = PipelineOptions([]) + options.view_as(StandardOptions).streaming = True + with TestPipeline(options=options) as p: + _ = ( + p + | Create(payloads) + | WriteToPubSub( + 'projects/fakeprj/topics/a_topic', + with_attributes=True, + publish_with_ordering_key=True)) + + # Verify that publish was called with ordering_key + mock_pubsub.return_value.publish.assert_called() + call_args = mock_pubsub.return_value.publish.call_args + + # Check that ordering_key was passed + self.assertIn('ordering_key', call_args.kwargs) + self.assertEqual(call_args.kwargs['ordering_key'], ordering_key) + + def test_write_messages_without_ordering_key(self, mock_pubsub): + """Test WriteToPubSub without ordering_key (backward compatibility).""" + data = b'data' + attributes = {'key': 'value'} + payloads = [PubsubMessage(data, attributes)] # No ordering_key + + options = PipelineOptions([]) + options.view_as(StandardOptions).streaming = True + with TestPipeline(options=options) as p: + _ = ( + p + | Create(payloads) + | WriteToPubSub( + 'projects/fakeprj/topics/a_topic', with_attributes=True)) + + # Verify that publish was called + mock_pubsub.return_value.publish.assert_called() + call_args = mock_pubsub.return_value.publish.call_args + self.assertNotIn('ordering_key', call_args.kwargs) + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) From 780d68d8f97605852e4bb5fb8298bb12ff29725d Mon Sep 17 00:00:00 2001 From: Lalit Yadav Date: Wed, 17 Jun 2026 04:04:50 -0500 Subject: [PATCH 405/490] Add drain support for Dataflow and Flink (#38786) * Add drain support for Dataflow and Flink * Add PipelineResult drain API --- .../flink/FlinkDetachedRunnerResult.java | 60 +++++++++++- .../beam/runners/flink/FlinkRunnerResult.java | 6 ++ .../runners/flink/FlinkRunnerResultTest.java | 92 +++++++++++++++++++ .../runners/dataflow/DataflowPipelineJob.java | 67 +++++++++----- .../dataflow/DataflowPipelineJobTest.java | 46 ++++++++++ .../org/apache/beam/sdk/PipelineResult.java | 13 +++ 6 files changed, 255 insertions(+), 29 deletions(-) diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java index f7d82065b658..b26e865526dd 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java @@ -18,6 +18,7 @@ package org.apache.beam.runners.flink; import java.io.IOException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import org.apache.beam.runners.core.metrics.MetricsContainerStepMap; import org.apache.beam.runners.flink.metrics.FlinkMetricContainer; @@ -25,6 +26,8 @@ import org.apache.beam.sdk.metrics.MetricResults; import org.apache.flink.api.common.JobStatus; import org.apache.flink.core.execution.JobClient; +import org.apache.flink.core.execution.SavepointFormatType; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +42,7 @@ public class FlinkDetachedRunnerResult implements PipelineResult { private JobClient jobClient; private int jobCheckIntervalInSecs; + private volatile @Nullable CompletableFuture drainSavepointFuture; FlinkDetachedRunnerResult(JobClient jobClient, int jobCheckIntervalInSecs) { this.jobClient = jobClient; @@ -47,10 +51,25 @@ public class FlinkDetachedRunnerResult implements PipelineResult { @Override public State getState() { + CompletableFuture drainFuture = drainSavepointFuture; + if (drainFuture != null) { + try { + return getDrainState(drainFuture); + } catch (IOException e) { + LOG.warn("Failed to drain Flink job. Querying Flink job state instead.", e); + } + } + return getFlinkJobState(); + } + + private State getFlinkJobState() { try { return toBeamJobState(jobClient.getJobStatus().get()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Fail to get flink job state", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Failed to get Flink job state", e); + } catch (ExecutionException e) { + throw new RuntimeException("Failed to get Flink job state", e); } } @@ -66,7 +85,11 @@ private MetricsContainerStepMap getMetricsContainerStepMap() { .getAccumulators() .get() .getOrDefault(FlinkMetricContainer.ACCUMULATOR_NAME, new MetricsContainerStepMap()); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("Fail to get flink job accumulators", e); + return new MetricsContainerStepMap(); + } catch (ExecutionException e) { LOG.warn("Fail to get flink job accumulators", e); return new MetricsContainerStepMap(); } @@ -76,12 +99,40 @@ private MetricsContainerStepMap getMetricsContainerStepMap() { public State cancel() throws IOException { try { this.jobClient.cancel().get(); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Fail to cancel flink job", e); + } catch (ExecutionException e) { throw new RuntimeException("Fail to cancel flink job", e); } return getState(); } + @Override + public synchronized State drain() throws IOException { + CompletableFuture drainFuture = drainSavepointFuture; + if (drainFuture == null || drainFuture.isCompletedExceptionally()) { + drainFuture = this.jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + drainSavepointFuture = drainFuture; + } + return getDrainState(drainFuture); + } + + private State getDrainState(CompletableFuture drainFuture) throws IOException { + if (!drainFuture.isDone()) { + return State.RUNNING; + } + try { + drainFuture.get(); + return State.DONE; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Failed to drain Flink job", e); + } catch (ExecutionException e) { + throw new IOException("Failed to drain Flink job", e.getCause()); + } + } + @Override public State waitUntilFinish() { return waitUntilFinish(Duration.millis(Long.MAX_VALUE)); @@ -100,6 +151,7 @@ public State waitUntilFinish(Duration duration) { try { Thread.sleep(jobCheckIntervalInSecs * 1000L); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new RuntimeException(e); } } diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkRunnerResult.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkRunnerResult.java index d892049bce4b..c0cce5349f20 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkRunnerResult.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkRunnerResult.java @@ -64,6 +64,12 @@ public State cancel() { return State.DONE; } + @Override + public State drain() { + // We can only be called here when we are done. + return State.DONE; + } + @Override public State waitUntilFinish() { return State.DONE; diff --git a/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkRunnerResultTest.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkRunnerResultTest.java index ba0981617fe3..908d940f5efb 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkRunnerResultTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkRunnerResultTest.java @@ -19,9 +19,20 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.io.IOException; import java.util.Collections; +import java.util.concurrent.CompletableFuture; import org.apache.beam.sdk.PipelineResult; +import org.apache.flink.api.common.JobStatus; +import org.apache.flink.core.execution.JobClient; +import org.apache.flink.core.execution.SavepointFormatType; import org.joda.time.Duration; import org.junit.Test; @@ -47,4 +58,85 @@ public void testCancelDoesNotThrowAnException() { result.cancel(); assertThat(result.getState(), is(PipelineResult.State.DONE)); } + + @Test + public void testDrainDoneResultDoesNotThrowAnException() throws Exception { + FlinkRunnerResult result = new FlinkRunnerResult(Collections.emptyMap(), 100); + assertThat(result.drain(), is(PipelineResult.State.DONE)); + } + + @Test + public void testDetachedDrainReturnsRunningThenDone() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture drainFuture = new CompletableFuture<>(); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(drainFuture); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + assertThat(result.drain(), is(PipelineResult.State.RUNNING)); + assertThat(result.getState(), is(PipelineResult.State.RUNNING)); + + drainFuture.complete("savepoint"); + assertThat(result.getState(), is(PipelineResult.State.DONE)); + verify(jobClient).stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + } + + @Test + public void testDetachedDrainFailureThrowsIOException() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture drainFuture = new CompletableFuture<>(); + RuntimeException failure = new RuntimeException("savepoint failed"); + drainFuture.completeExceptionally(failure); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(drainFuture); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + try { + result.drain(); + fail("Expected IOException"); + } catch (IOException e) { + assertThat(e.getMessage(), is("Failed to drain Flink job")); + assertSame(failure, e.getCause()); + } + } + + @Test + public void testDetachedGetStateFallsBackAfterDrainFailure() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture drainFuture = new CompletableFuture<>(); + drainFuture.completeExceptionally(new RuntimeException("savepoint failed")); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(drainFuture); + when(jobClient.getJobStatus()).thenReturn(CompletableFuture.completedFuture(JobStatus.RUNNING)); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + try { + result.drain(); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(result.getState(), is(PipelineResult.State.RUNNING)); + } + } + + @Test + public void testDetachedDrainRetriesAfterFailure() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture failedDrainFuture = new CompletableFuture<>(); + failedDrainFuture.completeExceptionally(new RuntimeException("savepoint failed")); + CompletableFuture retryDrainFuture = new CompletableFuture<>(); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(failedDrainFuture, retryDrainFuture); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + try { + result.drain(); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(result.drain(), is(PipelineResult.State.RUNNING)); + } + + retryDrainFuture.complete("savepoint"); + assertThat(result.getState(), is(PipelineResult.State.DONE)); + verify(jobClient, times(2)).stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + } } diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java index 400f161dee2f..0d7e5eaf68d9 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java @@ -432,55 +432,69 @@ private Exception processJobMessages( } private AtomicReference> cancelState = new AtomicReference<>(); + private AtomicReference> drainState = new AtomicReference<>(); @SuppressWarnings("Slf4jFormatShouldBeConst") @Override public State cancel() throws IOException { - // Enforce that a cancel() call on the job is done at most once - as - // a workaround for Dataflow service's current bugs with multiple - // cancellation, where it may sometimes return an error when cancelling - // a job that was already cancelled, but still report the job state as - // RUNNING. - // To partially work around these issues, we absorb duplicate cancel() - // calls. This, of course, doesn't address the case when the job terminates - // externally almost concurrently to calling cancel(), but at least it - // makes it possible to safely call cancel() multiple times and from - // multiple threads in one program. - FutureTask tentativeCancelTask = + return requestJobState(cancelState, "JOB_STATE_CANCELLED", "cancel", "Cancel"); + } + + @Override + public State drain() throws IOException { + return requestJobState(drainState, "JOB_STATE_DRAINED", "drain", "Drain"); + } + + @SuppressWarnings("Slf4jFormatShouldBeConst") + private State requestJobState( + AtomicReference> requestedState, + String dataflowRequestedState, + String action, + String capitalizedAction) + throws IOException { + // Enforce that a lifecycle request on the job is done at most once. This preserves the + // existing cancel() behavior and keeps duplicate drain() calls idempotent from one client. + FutureTask tentativeTask = new FutureTask<>( () -> { Job content = new Job(); content.setProjectId(getProjectId()); String currentJobId = getJobId(); content.setId(currentJobId); - content.setRequestedState("JOB_STATE_CANCELLED"); + content.setRequestedState(dataflowRequestedState); try { Job job = dataflowClient.updateJob(currentJobId, content); return MonitoringUtil.toState(job.getCurrentState()); } catch (IOException e) { State state = getState(); + String message = e.getMessage(); if (state.isTerminal()) { - LOG.warn("Cancel failed because job is already terminated. State is {}", state); + LOG.warn( + "{} failed because job is already terminated. State is {}", + capitalizedAction, + state); return state; - } else if (e.getMessage().contains("has terminated")) { + } else if (message != null && message.contains("has terminated")) { // This handles the case where the getState() call above returns RUNNING but the - // cancel was rejected because the job is in fact done. Hopefully, someday we can + // request was rejected because the job is in fact done. Hopefully, someday we can // delete this code if there is better consistency between the State and whether - // Cancel succeeds. + // lifecycle requests succeed. // // Example message: // Workflow modification failed. Causes: (7603adc9e9bff51e): Cannot perform // operation 'cancel' on Job: 2017-04-01_22_50_59-9269855660514862348. Job has // terminated in state SUCCESS: Workflow job: // 2017-04-01_22_50_59-9269855660514862348 succeeded. - LOG.warn("Cancel failed because job is already terminated.", e); + LOG.warn("{} failed because job is already terminated.", capitalizedAction, e); return state; } else { String errorMsg = String.format( - "Failed to cancel job in state %s, " - + "please go to the Developers Console to cancel it manually: %s", + "Failed to %s job in state %s, " + + "please go to the Developers Console to %s it manually: %s", + action, state, + action, MonitoringUtil.getJobMonitoringPageURL( getProjectId(), getRegion(), getJobId())); LOG.warn(errorMsg); @@ -488,14 +502,17 @@ public State cancel() throws IOException { } } }); - if (cancelState.compareAndSet(null, tentativeCancelTask)) { - // This thread should perform cancellation, while others will - // only wait for the result. - cancelState.get().run(); + if (requestedState.compareAndSet(null, tentativeTask)) { + // This thread should perform the lifecycle request, while others will only wait for the + // result. + requestedState.get().run(); } try { - return cancelState.get().get(); - } catch (InterruptedException | ExecutionException e) { + return requestedState.get().get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } catch (ExecutionException e) { throw new IOException(e); } } diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java index 54ba10df9d1c..4b088eb41a7d 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java @@ -406,6 +406,26 @@ public void testCancelUnterminatedJobThatSucceeds() throws IOException { verifyNoMoreInteractions(mockJobs); } + @Test + public void testDrainUnterminatedJobThatSucceeds() throws IOException { + Dataflow.Projects.Locations.Jobs.Update update = + mock(Dataflow.Projects.Locations.Jobs.Update.class); + when(mockJobs.update(eq(PROJECT_ID), eq(REGION_ID), eq(JOB_ID), any(Job.class))) + .thenReturn(update); + when(update.execute()).thenReturn(new Job().setCurrentState("JOB_STATE_DRAINING")); + + DataflowPipelineJob job = + new DataflowPipelineJob(DataflowClient.create(options), JOB_ID, options, null); + + assertEquals(State.RUNNING, job.drain()); + Job content = new Job(); + content.setProjectId(PROJECT_ID); + content.setId(JOB_ID); + content.setRequestedState("JOB_STATE_DRAINED"); + verify(mockJobs).update(eq(PROJECT_ID), eq(REGION_ID), eq(JOB_ID), eq(content)); + verifyNoMoreInteractions(mockJobs); + } + @Test public void testCancelUnterminatedJobThatFails() throws IOException { Dataflow.Projects.Locations.Jobs.Get statusRequest = @@ -432,6 +452,32 @@ public void testCancelUnterminatedJobThatFails() throws IOException { job.cancel(); } + @Test + public void testCancelUnterminatedJobWithNullFailureMessage() throws IOException { + Dataflow.Projects.Locations.Jobs.Get statusRequest = + mock(Dataflow.Projects.Locations.Jobs.Get.class); + + Job statusResponse = new Job(); + statusResponse.setCurrentState("JOB_STATE_RUNNING"); + when(mockJobs.get(PROJECT_ID, REGION_ID, JOB_ID)).thenReturn(statusRequest); + when(statusRequest.execute()).thenReturn(statusResponse); + + Dataflow.Projects.Locations.Jobs.Update update = + mock(Dataflow.Projects.Locations.Jobs.Update.class); + when(mockJobs.update(eq(PROJECT_ID), eq(REGION_ID), eq(JOB_ID), any(Job.class))) + .thenReturn(update); + when(update.execute()).thenThrow(new IOException()); + + DataflowPipelineJob job = + new DataflowPipelineJob(DataflowClient.create(options), JOB_ID, options, null); + + thrown.expect(IOException.class); + thrown.expectMessage( + "Failed to cancel job in state RUNNING, " + + "please go to the Developers Console to cancel it manually:"); + job.cancel(); + } + /** * Test that {@link DataflowPipelineJob#cancel} doesn't throw if the Dataflow service returns * non-terminal state even though the cancel API call failed, which can happen in practice. diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java index 91313f3924aa..46cca7833e52 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java @@ -43,6 +43,19 @@ public interface PipelineResult { */ State cancel() throws IOException; + /** + * Drains the pipeline execution. + * + *

      Draining requests that the runner stop accepting new input and finish processing data that + * has already entered the pipeline. + * + * @throws IOException if there is a problem executing the drain request. + * @throws UnsupportedOperationException if the runner does not support draining. + */ + default State drain() throws IOException { + throw new UnsupportedOperationException("Runner does not support draining."); + } + /** * Waits until the pipeline finishes and returns the final status. It times out after the given * duration. From 999fa36e7913c25570c691d8790e56b9f8f5c52a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 08:16:40 -0400 Subject: [PATCH 406/490] Bump google.golang.org/api from 0.284.0 to 0.285.0 in /sdks (#38997) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.284.0 to 0.285.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.284.0...v0.285.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.285.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 4 ++-- sdks/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index bc427296c6fa..8efee765d121 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.21.0 golang.org/x/sys v0.46.0 golang.org/x/text v0.38.0 - google.golang.org/api v0.284.0 + google.golang.org/api v0.285.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 @@ -204,5 +204,5 @@ require ( golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index 2c8a4867ae6c..4fca53fb0cc8 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1326,8 +1326,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.284.0 h1:i+cKTgeQRcRySkP7QTl5PDO7/pAm8EcMFIUMlNbk4Vc= -google.golang.org/api v0.284.0/go.mod h1:AU44fU+XVZOCcd8uLaBIa/ZgzgPf/0qqY3+m7lQaado= +google.golang.org/api v0.285.0 h1:B7eHHoKGAX/LrPkQvhQqnGwjgWxofbdGwCTQvpm8FkM= +google.golang.org/api v0.285.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1425,8 +1425,8 @@ google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 h1:cTHF8xtqtBN5sQ4 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From 78c7815e67c4a66f6e40857b0ece19da73e3a1a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 08:17:04 -0400 Subject: [PATCH 407/490] Bump github.com/aws/aws-sdk-go-v2/service/s3 in /sdks (#38998) Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.103.3 to 1.104.0. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.103.3...service/s3/v1.104.0) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.104.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 8efee765d121..eafde005389a 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -36,7 +36,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.25 github.com/aws/aws-sdk-go-v2/credentials v1.19.24 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27 - github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 github.com/aws/smithy-go v1.27.2 github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 diff --git a/sdks/go.sum b/sdks/go.sum index 4fca53fb0cc8..3e35e75fe006 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -253,8 +253,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+u github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 h1:ta8csKy5vN91F3i5gGR85lFV0srBqySEji7Jroes6rE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= From 4de175c4faec1286694cf0f04d679f3876e0ab70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:30:59 -0400 Subject: [PATCH 408/490] Bump github.com/aws/aws-sdk-go-v2/feature/s3/manager in /sdks (#38999) Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.22.27 to 1.22.28. - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/feature/s3/manager/v1.22.27...feature/s3/manager/v1.22.28) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.22.28 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index eafde005389a..84922261fe61 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -35,7 +35,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.25 github.com/aws/aws-sdk-go-v2/credentials v1.19.24 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 github.com/aws/smithy-go v1.27.2 github.com/docker/go-connections v0.7.0 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index 3e35e75fe006..2610159c6693 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -219,8 +219,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEt github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27 h1:gb+HtIZdwcIoLxv/xwGumQr1DmGmGGCQnjKKVVSMYsU= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.27/go.mod h1:2b/8jZl/qwUMBZpSAcxX+IdM3zj6RUyfnB2IdLt9I+I= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 h1:ez4y5o7sa0uaRI8BquYOXtZpioUPhbQEh7Igm88oV9U= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28/go.mod h1:TpmZOrQA12XKEpVypgBGZSQBsm1WUTndCiSnbDsbvug= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= From 3991c3a634d936b2144ce5cdf2e7f6206666870e Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:41:40 -0400 Subject: [PATCH 409/490] fix sentence (#38989) --- website/www/site/content/en/documentation/dsls/sql/ddl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/www/site/content/en/documentation/dsls/sql/ddl.md b/website/www/site/content/en/documentation/dsls/sql/ddl.md index 11d588a4c35b..1befb5c8ecc8 100644 --- a/website/www/site/content/en/documentation/dsls/sql/ddl.md +++ b/website/www/site/content/en/documentation/dsls/sql/ddl.md @@ -156,7 +156,7 @@ CREATE DATABASE other_catalog.sales_data; {{< /tab >}} {{< tab USE >}}

      Sets the active Database for the current session. This simplifies queries by allowing you -to reference Databases directly without their fully-qualified names (for example, my_db instead of my_catalog.my_db)

      +to reference Tables directly without their fully-qualified names (for example, my_table instead of my_catalog.my_db.my_table)

      Note: All subsequent TABLE commands will be executed under this Database, unless fully qualified.

      From 325e67d5e8f6d16cdd3ff4b63fa7b2acede1475b Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Wed, 17 Jun 2026 10:22:37 -0400 Subject: [PATCH 410/490] address DOM text reinterpreted as HTML alert (#38949) * address DOM text reinterpreted as HTML alert * address gemini comments * fix a few more gemini comments * address more gemini * address gemini --- website/www/site/assets/js/bootstrap.js | 46 ++++++++++++++++--- website/www/site/assets/js/bootstrap/alert.js | 7 ++- .../www/site/assets/js/bootstrap/carousel.js | 9 +++- .../www/site/assets/js/bootstrap/collapse.js | 6 ++- .../www/site/assets/js/bootstrap/dropdown.js | 7 ++- website/www/site/assets/js/bootstrap/modal.js | 8 +++- .../www/site/assets/js/bootstrap/tooltip.js | 9 +++- 7 files changed, 80 insertions(+), 12 deletions(-) diff --git a/website/www/site/assets/js/bootstrap.js b/website/www/site/assets/js/bootstrap.js index 01fbbcbaa9fd..a10c907ab81a 100755 --- a/website/www/site/assets/js/bootstrap.js +++ b/website/www/site/assets/js/bootstrap.js @@ -109,7 +109,12 @@ if (typeof jQuery === 'undefined') { selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = $(selector) + var $parent + try { + $parent = selector ? $(document).find(selector) : $() + } catch (e) { + $parent = $() + } if (e) e.preventDefault() @@ -502,7 +507,14 @@ if (typeof jQuery === 'undefined') { var clickHandler = function (e) { var href var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + var selector = $this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + var $target + try { + $target = selector ? $(document).find(selector) : $() + } catch (e) { + $target = $() + } + if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') @@ -691,7 +703,11 @@ if (typeof jQuery === 'undefined') { var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - return $(target) + try { + return target ? $(document).find(target) : $() + } catch (e) { + return $() + } } @@ -773,7 +789,12 @@ if (typeof jQuery === 'undefined') { selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = selector && $(selector) + var $parent + try { + $parent = selector && $(document).find(selector) + } catch (e) { + $parent = $() + } return $parent && $parent.length ? $parent : $this.parent() } @@ -1230,7 +1251,13 @@ if (typeof jQuery === 'undefined') { $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var selector = $this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + var $target + try { + $target = selector ? $(document).find(selector) : $() + } catch (e) { + $target = $() + } var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() @@ -1550,11 +1577,18 @@ if (typeof jQuery === 'undefined') { .css(isVertical ? 'top' : 'left', '') } + function sanitizeHtml(string) { + if (typeof DOMPurify !== 'undefined' && typeof string === 'string') { + return DOMPurify.sanitize(string) + } + return string + } + Tooltip.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](this.options.html ? sanitizeHtml(title) : title) $tip.removeClass('fade in top bottom left right') } diff --git a/website/www/site/assets/js/bootstrap/alert.js b/website/www/site/assets/js/bootstrap/alert.js index 5536755df91c..28eca46229e9 100755 --- a/website/www/site/assets/js/bootstrap/alert.js +++ b/website/www/site/assets/js/bootstrap/alert.js @@ -31,7 +31,12 @@ selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = $(selector) + var $parent + try { + $parent = selector ? $(document).find(selector) : $() + } catch (e) { + $parent = $() + } if (e) e.preventDefault() diff --git a/website/www/site/assets/js/bootstrap/carousel.js b/website/www/site/assets/js/bootstrap/carousel.js index 6cdbc79ce1c1..848c7d8986f6 100755 --- a/website/www/site/assets/js/bootstrap/carousel.js +++ b/website/www/site/assets/js/bootstrap/carousel.js @@ -208,7 +208,14 @@ var clickHandler = function (e) { var href var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + var selector = $this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + var $target + try { + $target = selector ? $(document).find(selector) : $() + } catch (e) { + $target = $() + } + if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') diff --git a/website/www/site/assets/js/bootstrap/collapse.js b/website/www/site/assets/js/bootstrap/collapse.js index 9e26465d9057..79f953770936 100755 --- a/website/www/site/assets/js/bootstrap/collapse.js +++ b/website/www/site/assets/js/bootstrap/collapse.js @@ -159,7 +159,11 @@ var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - return $(target) + try { + return target ? $(document).find(target) : $() + } catch (e) { + return $() + } } diff --git a/website/www/site/assets/js/bootstrap/dropdown.js b/website/www/site/assets/js/bootstrap/dropdown.js index df6be86940dd..f89eea7419b3 100755 --- a/website/www/site/assets/js/bootstrap/dropdown.js +++ b/website/www/site/assets/js/bootstrap/dropdown.js @@ -29,7 +29,12 @@ selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - var $parent = selector && $(selector) + var $parent + try { + $parent = selector && $(document).find(selector) + } catch (e) { + $parent = $() + } return $parent && $parent.length ? $parent : $this.parent() } diff --git a/website/www/site/assets/js/bootstrap/modal.js b/website/www/site/assets/js/bootstrap/modal.js index 5049cccf3693..bc30367f74d0 100755 --- a/website/www/site/assets/js/bootstrap/modal.js +++ b/website/www/site/assets/js/bootstrap/modal.js @@ -320,7 +320,13 @@ $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var selector = $this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + var $target + try { + $target = selector ? $(document).find(selector) : $() + } catch (e) { + $target = $() + } var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() diff --git a/website/www/site/assets/js/bootstrap/tooltip.js b/website/www/site/assets/js/bootstrap/tooltip.js index 7094b34dce7d..59e9490cc617 100755 --- a/website/www/site/assets/js/bootstrap/tooltip.js +++ b/website/www/site/assets/js/bootstrap/tooltip.js @@ -302,11 +302,18 @@ .css(isVertical ? 'top' : 'left', '') } + function sanitizeHtml(string) { + if (typeof DOMPurify !== 'undefined' && typeof string === 'string') { + return DOMPurify.sanitize(string) + } + return string + } + Tooltip.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](this.options.html ? sanitizeHtml(title) : title) $tip.removeClass('fade in top bottom left right') } From a4a20284368623012983ae7070b8b355c94fd8fb Mon Sep 17 00:00:00 2001 From: Radek Stankiewicz Date: Mon, 8 Jun 2026 14:45:35 +0200 Subject: [PATCH 411/490] Align gcpauth extension implementation with ability to turn it off easily while still being added as dependency. --- .../beam/gradle/BeamModulePlugin.groovy | 1 + .../build.gradle | 1 + .../gcp/auth/ConfigurableOption.java | 1 + ...thAutoConfigurationCustomizerProvider.java | 196 +++++++------ ...toConfigurationCustomizerProviderTest.java | 276 +++++++++++------- .../apache_beam/yaml/test_utils/__init__.py | 18 -- 6 files changed, 282 insertions(+), 211 deletions(-) delete mode 100644 sdks/python/apache_beam/yaml/test_utils/__init__.py diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index abeede24709a..e8c7a8791935 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -851,6 +851,7 @@ class BeamModulePlugin implements Plugin { log4j2_log4j12_api : "org.apache.logging.log4j:log4j-1.2-api:$log4j2_version", mockito_core : "org.mockito:mockito-core:4.11.0", mockito_inline : "org.mockito:mockito-inline:4.11.0", + mockito_junit_jupiter : "org.mockito:mockito-junit-jupiter:4.11.0", mongo_java_driver : "org.mongodb:mongodb-driver-sync:5.5.0", mongo_bson : "org.mongodb:bson:5.5.0", mongodb_driver_core : "org.mongodb:mongodb-driver-core:5.5.0", diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle index 1b08b867b291..a1e18805bf3f 100644 --- a/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle @@ -37,6 +37,7 @@ dependencies { testImplementation library.java.junit testImplementation library.java.mockito_core testImplementation library.java.mockito_inline + testImplementation library.java.mockito_junit_jupiter testImplementation library.java.jupiter_api testImplementation library.java.jupiter_params testRuntimeOnly library.java.jupiter_engine diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java index e18e5693e3a1..8cdbe00af6ef 100644 --- a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java @@ -61,6 +61,7 @@ enum ConfigurableOption { *
    • {@code metrics} - Enables authentication for metric exports. *
    • {@code traces} - Enables authentication for trace exports. *
    • {@code all} - Enables authentication for all exports. + *
    • {@code none} - Disables authentication for all exports. * * *

      The values are case-sensitive. Whitespace around commas and values is ignored. Can be diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java index ce2b142591bd..453fb5717008 100644 --- a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -20,15 +20,20 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import com.google.auth.oauth2.GoogleCredentials; import com.google.auto.service.AutoService; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -41,8 +46,9 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GoogleAuthException.Reason; @@ -62,25 +68,34 @@ * @see AutoConfigurationCustomizerProvider * @see GoogleCredentials */ -@AutoService(AutoConfigurationCustomizerProvider.class) @Internal +@AutoService(AutoConfigurationCustomizerProvider.class) public class GcpAuthAutoConfigurationCustomizerProvider implements AutoConfigurationCustomizerProvider { + private static final Logger LOG = + Logger.getLogger(GcpAuthAutoConfigurationCustomizerProvider.class.getName()); + private static final String SIGNAL_TARGET_WARNING_FIX_SUGGESTION = + String.format( + "You may safely ignore this warning if it is intentional, otherwise please configure the '%s' by exporting valid values to environment variable: %s or by setting valid values in system property: %s.", + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getUserReadableName(), + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getEnvironmentVariable(), + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty()); + static final String QUOTA_USER_PROJECT_HEADER = "x-goog-user-project"; static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; static final String SIGNAL_TYPE_TRACES = "traces"; static final String SIGNAL_TYPE_METRICS = "metrics"; static final String SIGNAL_TYPE_ALL = "all"; - - private @Nullable GoogleCredentials credentials; + static final String SIGNAL_TYPE_NONE = "none"; /** * Customizes the provided {@link AutoConfigurationCustomizer} such that authenticated exports to * GCP Telemetry API are possible from the configured OTLP exporter. * - *

      This method performs the following: + *

      This method attempts to retrieve Google Application Default Credentials (ADC) and performs + * the following: * *

        *
      • Verifies whether the configured OTLP endpoint (base or signal specific) is a known GCP @@ -99,10 +114,32 @@ public class GcpAuthAutoConfigurationCustomizerProvider */ @Override public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) { + java.util.function.Supplier credentialsSupplier = + new java.util.function.Supplier() { + private @javax.annotation.Nullable GoogleCredentials credentials; + + @Override + public synchronized GoogleCredentials get() { + if (credentials == null) { + try { + credentials = GoogleCredentials.getApplicationDefault(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + } + return credentials; + } + }; autoConfiguration - .addSpanExporterCustomizer(this::customizeSpanExporter) - .addMetricExporterCustomizer(this::customizeMetricExporter) - .addResourceCustomizer(this::customizeResource); + .addSpanExporterCustomizer( + (spanExporter, configProperties) -> + customizeSpanExporter(spanExporter, credentialsSupplier, configProperties)) + .addMetricExporterCustomizer( + (metricExporter, configProperties) -> + customizeMetricExporter(metricExporter, credentialsSupplier, configProperties)) + .addResourceCustomizer( + (resource, configProperties) -> + customizeResource(resource, credentialsSupplier, configProperties)); } @Override @@ -110,136 +147,106 @@ public int order() { return Integer.MAX_VALUE - 1; } - private synchronized GoogleCredentials getCredentials() { - if (credentials == null) { - try { - credentials = GoogleCredentials.getApplicationDefault(); - } catch (IOException e) { - throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); - } - } - return credentials; - } - - private SpanExporter customizeSpanExporter( - SpanExporter exporter, ConfigProperties configProperties) { + private static SpanExporter customizeSpanExporter( + SpanExporter exporter, + java.util.function.Supplier credentialsSupplier, + ConfigProperties configProperties) { if (isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties)) { - return addAuthorizationHeaders(exporter, configProperties); + return addAuthorizationHeaders(exporter, credentialsSupplier.get(), configProperties); + } else { + String[] params = { + SIGNAL_TYPE_TRACES, SIGNAL_TYPE_NONE, SIGNAL_TARGET_WARNING_FIX_SUGGESTION + }; + LOG.log( + Level.WARNING, + "GCP Authentication Extension is not configured for signal type: {0} or is configured with signal type: {1}. {2}", + params); } return exporter; } - private MetricExporter customizeMetricExporter( - MetricExporter exporter, ConfigProperties configProperties) { + private static MetricExporter customizeMetricExporter( + MetricExporter exporter, + java.util.function.Supplier credentialsSupplier, + ConfigProperties configProperties) { if (isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties)) { - return addAuthorizationHeaders(exporter, configProperties); + return addAuthorizationHeaders(exporter, credentialsSupplier.get(), configProperties); + } else { + String[] params = { + SIGNAL_TYPE_METRICS, SIGNAL_TYPE_NONE, SIGNAL_TARGET_WARNING_FIX_SUGGESTION + }; + LOG.log( + Level.WARNING, + "GCP Authentication Extension is not configured for signal type: {0} or is configured with signal type: {1}. {2}", + params); } return exporter; } // Checks if the auth extension is configured to target the passed signal for authentication. private static boolean isSignalTargeted(String checkSignal, ConfigProperties configProperties) { - String endpoint = configProperties.getString("otel.exporter.otlp." + checkSignal + ".endpoint"); - if (endpoint == null) { - endpoint = configProperties.getString("otel.exporter.otlp.endpoint"); - } - if (endpoint == null) { - return false; - } - - try { - java.net.URI uri = new java.net.URI(endpoint); - String host = uri.getHost(); - String scheme = uri.getScheme(); - if (host == null - || scheme == null - || !scheme.equalsIgnoreCase("https") - || (!host.equalsIgnoreCase("telemetry.googleapis.com") - && !host.equalsIgnoreCase("telemetry.mtls.googleapis.com"))) { - return false; - } - } catch (java.net.URISyntaxException e) { - return false; - } - String userSpecifiedTargetedSignals = ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback( configProperties, () -> SIGNAL_TYPE_ALL); - return stream(userSpecifiedTargetedSignals.split(",")) - .map(String::trim) - .anyMatch( - targetedSignal -> - targetedSignal.equals(checkSignal) || targetedSignal.equals(SIGNAL_TYPE_ALL)); - } - - private boolean isAnySignalTargeted(ConfigProperties configProperties) { - return isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties) - || isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties); + List targetedSignals = + stream(userSpecifiedTargetedSignals.split(",")).map(String::trim).collect(toList()); + if (targetedSignals.contains(SIGNAL_TYPE_NONE)) { + return false; + } + return targetedSignals.contains(checkSignal) || targetedSignals.contains(SIGNAL_TYPE_ALL); } // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and // OtlpHttpSpanExporter. - private SpanExporter addAuthorizationHeaders( - SpanExporter exporter, ConfigProperties configProperties) { + private static SpanExporter addAuthorizationHeaders( + SpanExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) { if (exporter instanceof OtlpHttpSpanExporter) { - SpanExporter result = + OtlpHttpSpanExporterBuilder builder = ((OtlpHttpSpanExporter) exporter) .toBuilder() - .setHeaders(() -> getRequiredHeaderMap(configProperties)) - .build(); - exporter.shutdown(); - return result; + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); } else if (exporter instanceof OtlpGrpcSpanExporter) { - SpanExporter result = + OtlpGrpcSpanExporterBuilder builder = ((OtlpGrpcSpanExporter) exporter) .toBuilder() - .setHeaders(() -> getRequiredHeaderMap(configProperties)) - .build(); - exporter.shutdown(); - return result; + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); } return exporter; } // Adds authorization headers to the calls made by the OtlpGrpcMetricExporter and // OtlpHttpMetricExporter. - private MetricExporter addAuthorizationHeaders( - MetricExporter exporter, ConfigProperties configProperties) { + private static MetricExporter addAuthorizationHeaders( + MetricExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) { if (exporter instanceof OtlpHttpMetricExporter) { - MetricExporter result = + OtlpHttpMetricExporterBuilder builder = ((OtlpHttpMetricExporter) exporter) .toBuilder() - .setHeaders(() -> getRequiredHeaderMap(configProperties)) - .build(); - exporter.shutdown(); - return result; + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); } else if (exporter instanceof OtlpGrpcMetricExporter) { - MetricExporter result = + OtlpGrpcMetricExporterBuilder builder = ((OtlpGrpcMetricExporter) exporter) .toBuilder() - .setHeaders(() -> getRequiredHeaderMap(configProperties)) - .build(); - exporter.shutdown(); - return result; + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); } return exporter; } - private Map getRequiredHeaderMap(ConfigProperties configProperties) { - GoogleCredentials creds = getCredentials(); + private static Map getRequiredHeaderMap( + GoogleCredentials credentials, ConfigProperties configProperties) { Map> gcpHeaders; try { // this also refreshes the credentials, if required - gcpHeaders = creds.getRequestMetadata(); + gcpHeaders = credentials.getRequestMetadata(); } catch (IOException e) { throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); } - if (gcpHeaders == null) { - return Map.of(); - } Map flattenedHeaders = gcpHeaders.entrySet().stream() - .filter(entry -> entry.getKey() != null && entry.getValue() != null) .collect( toMap( Map.Entry::getKey, @@ -262,16 +269,19 @@ private Map getRequiredHeaderMap(ConfigProperties configProperti } // Updates the current resource with the attributes required for ingesting OTLP data on GCP. - private Resource customizeResource(Resource resource, ConfigProperties configProperties) { - if (!isAnySignalTargeted(configProperties)) { + private static Resource customizeResource( + Resource resource, + java.util.function.Supplier credentialsSupplier, + ConfigProperties configProperties) { + if (!isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties) + && !isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties)) { return resource; } - String gcpProjectId; try { gcpProjectId = ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties); } catch (ConfigurationException e) { - gcpProjectId = getCredentials().getProjectId(); + gcpProjectId = credentialsSupplier.get().getProjectId(); if (gcpProjectId == null || gcpProjectId.isEmpty()) { throw e; } diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java index e1db55c3635b..92aa72c2a79d 100644 --- a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java @@ -23,7 +23,6 @@ import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_ALL; import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_METRICS; import static org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_TRACES; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -58,6 +57,7 @@ import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.trace.data.SpanData; @@ -80,9 +80,11 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -92,12 +94,14 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; /** * Copied from open-telemetry. Link: * https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java */ +@ExtendWith(MockitoExtension.class) class GcpAuthAutoConfigurationCustomizerProviderTest { private static final String DUMMY_GCP_RESOURCE_PROJECT_ID = "my-gcp-resource-project-id"; @@ -109,36 +113,37 @@ class GcpAuthAutoConfigurationCustomizerProviderTest { @Captor private ArgumentCaptor>> traceHeaderSupplierCaptor; @Captor private ArgumentCaptor>> metricHeaderSupplierCaptor; - private static final ImmutableMap DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER = - ImmutableMap.builder() - .put("otel.exporter.otlp.traces.endpoint", "https://telemetry.googleapis.com/v1/traces") - .put("otel.traces.exporter", "otlp") - .put("otel.metrics.exporter", "none") - .put("otel.logs.exporter", "none") - .put("otel.resource.attributes", "foo=bar") - .build(); - - private static final ImmutableMap DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER = - ImmutableMap.builder() - .put("otel.exporter.otlp.metrics.endpoint", "https://telemetry.googleapis.com/v1/metrics") - .put("otel.traces.exporter", "none") - .put("otel.metrics.exporter", "otlp") - .put("otel.logs.exporter", "none") - .put("otel.resource.attributes", "foo=bar") - .build(); + private static final ImmutableMap defaultOtelPropertiesSpanExporter = + ImmutableMap.of( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none", + "otel.resource.attributes", + "foo=bar"); + + private static final ImmutableMap defaultOtelPropertiesMetricExporter = + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics", + "otel.traces.exporter", + "none", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none", + "otel.resource.attributes", + "foo=bar"); @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); } - @AfterEach - public void teardown() { - System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty()); - System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty()); - System.clearProperty(ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty()); - } - // TODO: Use parameterized test for testing traces customizer for http & grpc. @Test void testTraceCustomizerOtlpHttp() { @@ -236,7 +241,6 @@ void testTraceCustomizerOtlpGrpc() { .isTrue(); Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); - assertThat(exportedSpans).isNotEmpty(); for (SpanData spanData : exportedSpans) { assertThat(spanData.getResource().getAttributes().asMap()) @@ -249,6 +253,7 @@ void testTraceCustomizerOtlpGrpc() { } } + // TODO: Use parameterized test for testing metrics customizer for http & grpc. @Test void testMetricCustomizerOtlpHttp() { // Set resource project system property @@ -289,7 +294,6 @@ void testMetricCustomizerOtlpHttp() { Mockito.verify(mockOtlpHttpMetricExporter, Mockito.atLeast(1)) .export(Mockito.anyCollection()); - assertThat(exportedMetrics).isNotEmpty(); for (MetricData metricData : exportedMetrics) { assertThat(metricData.getResource().getAttributes().asMap()) @@ -298,9 +302,8 @@ void testMetricCustomizerOtlpHttp() { assertThat(metricData.getResource().getAttributes().asMap()) .containsEntry(AttributeKey.stringKey("foo"), "bar"); assertThat(metricData.getLongSumData().getPoints()).isNotEmpty(); - for (io.opentelemetry.sdk.metrics.data.PointData pointData : - metricData.getLongSumData().getPoints()) { - assertThat(pointData.getAttributes().asMap()) + for (LongPointData longPointData : metricData.getLongSumData().getPoints()) { + assertThat(longPointData.getAttributes().asMap()) .containsKey(AttributeKey.longKey("work_loop")); } } @@ -347,7 +350,6 @@ void testMetricCustomizerOtlpGrpc() { Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.atLeast(1)) .export(Mockito.anyCollection()); - assertThat(exportedMetrics).isNotEmpty(); for (MetricData metricData : exportedMetrics) { assertThat(metricData.getResource().getAttributes().asMap()) @@ -356,9 +358,8 @@ void testMetricCustomizerOtlpGrpc() { assertThat(metricData.getResource().getAttributes().asMap()) .containsEntry(AttributeKey.stringKey("foo"), "bar"); assertThat(metricData.getLongSumData().getPoints()).isNotEmpty(); - for (io.opentelemetry.sdk.metrics.data.PointData pointData : - metricData.getLongSumData().getPoints()) { - assertThat(pointData.getAttributes().asMap()) + for (LongPointData longPointData : metricData.getLongSumData().getPoints()) { + assertThat(longPointData.getAttributes().asMap()) .containsKey(AttributeKey.longKey("work_loop")); } } @@ -375,8 +376,7 @@ void testCustomizerFailWithMissingResourceProject() { googleCredentialsMockedStatic .when(GoogleCredentials::getApplicationDefault) .thenReturn(mockedGoogleCredentials); - - assertThrows( + Assert.assertThrows( ConfigurationException.class, () -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter)); } @@ -457,12 +457,16 @@ void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws IOExce } } + @AfterEach + void clearProperties() { + System.clearProperty("google.cloud.project"); + System.clearProperty("google.otel.auth.target.signals"); + } + @ParameterizedTest @MethodSource("provideProjectIdBehaviorTestCases") @SuppressWarnings("CannotMockMethod") void testProjectIdBehavior(ProjectIdTestBehavior testCase) throws IOException { - System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty()); - System.clearProperty(ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty()); // configure environment according to test case String userSpecifiedProjectId = testCase.getUserSpecifiedProjectId(); @@ -507,7 +511,7 @@ void testProjectIdBehavior(ProjectIdTestBehavior testCase) throws IOException { if (testCase.getExpectedToThrow()) { // expect exception to be thrown when project ID is not available - assertThrows( + Assert.assertThrows( ConfigurationException.class, () -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter)); // verify getProjectId() was called to attempt fallback @@ -626,14 +630,14 @@ private static Stream provideTargetSignalBehaviorTestCases() { Arguments.of( TargetSignalBehavior.builder() .setConfiguredTargetSignals("traces") - .setUserSpecifiedOtelProperties(DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER) + .setUserSpecifiedOtelProperties(defaultOtelPropertiesSpanExporter) .setExpectedIsMetricsSignalModified(false) .setExpectedIsTraceSignalModified(true) .build()), Arguments.of( TargetSignalBehavior.builder() .setConfiguredTargetSignals("metrics") - .setUserSpecifiedOtelProperties(DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER) + .setUserSpecifiedOtelProperties(defaultOtelPropertiesMetricExporter) .setExpectedIsMetricsSignalModified(true) .setExpectedIsTraceSignalModified(false) .build()), @@ -641,17 +645,17 @@ private static Stream provideTargetSignalBehaviorTestCases() { TargetSignalBehavior.builder() .setConfiguredTargetSignals("all") .setUserSpecifiedOtelProperties( - ImmutableMap.builder() - .put( - "otel.exporter.otlp.metrics.endpoint", - "https://telemetry.googleapis.com/v1/metrics") - .put( - "otel.exporter.otlp.traces.endpoint", - "https://telemetry.googleapis.com/v1/traces") - .put("otel.traces.exporter", "otlp") - .put("otel.metrics.exporter", "otlp") - .put("otel.logs.exporter", "none") - .build()) + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) .setExpectedIsMetricsSignalModified(true) .setExpectedIsTraceSignalModified(true) .build()), @@ -659,17 +663,17 @@ private static Stream provideTargetSignalBehaviorTestCases() { TargetSignalBehavior.builder() .setConfiguredTargetSignals("metrics, traces") .setUserSpecifiedOtelProperties( - ImmutableMap.builder() - .put( - "otel.exporter.otlp.metrics.endpoint", - "https://telemetry.googleapis.com/v1/metrics") - .put( - "otel.exporter.otlp.traces.endpoint", - "https://telemetry.googleapis.com/v1/traces") - .put("otel.traces.exporter", "otlp") - .put("otel.metrics.exporter", "otlp") - .put("otel.logs.exporter", "none") - .build()) + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) .setExpectedIsMetricsSignalModified(true) .setExpectedIsTraceSignalModified(true) .build()), @@ -677,17 +681,17 @@ private static Stream provideTargetSignalBehaviorTestCases() { TargetSignalBehavior.builder() .setConfiguredTargetSignals("") .setUserSpecifiedOtelProperties( - ImmutableMap.builder() - .put( - "otel.exporter.otlp.metrics.endpoint", - "https://telemetry.googleapis.com/v1/metrics") - .put( - "otel.exporter.otlp.traces.endpoint", - "https://telemetry.googleapis.com/v1/traces") - .put("otel.traces.exporter", "otlp") - .put("otel.metrics.exporter", "otlp") - .put("otel.logs.exporter", "none") - .build()) + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) .setExpectedIsMetricsSignalModified(true) .setExpectedIsTraceSignalModified(true) .build()), @@ -697,9 +701,9 @@ private static Stream provideTargetSignalBehaviorTestCases() { .setUserSpecifiedOtelProperties( ImmutableMap.of( "otel.exporter.otlp.metrics.endpoint", - "https://telemetry.googleapis.com/v1/metrics", + "https://localhost:4813/v1/metrics", "otel.exporter.otlp.traces.endpoint", - "https://telemetry.googleapis.com/v1/traces", + "https://localhost:4813/v1/traces", "otel.traces.exporter", "none", "otel.metrics.exporter", @@ -713,17 +717,17 @@ private static Stream provideTargetSignalBehaviorTestCases() { TargetSignalBehavior.builder() .setConfiguredTargetSignals("metric, trace") .setUserSpecifiedOtelProperties( - ImmutableMap.builder() - .put( - "otel.exporter.otlp.metrics.endpoint", - "https://telemetry.googleapis.com/v1/metrics") - .put( - "otel.exporter.otlp.traces.endpoint", - "https://telemetry.googleapis.com/v1/traces") - .put("otel.traces.exporter", "otlp") - .put("otel.metrics.exporter", "otlp") - .put("otel.logs.exporter", "none") - .build()) + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) .setExpectedIsMetricsSignalModified(false) .setExpectedIsTraceSignalModified(false) .build()), @@ -731,19 +735,91 @@ private static Stream provideTargetSignalBehaviorTestCases() { TargetSignalBehavior.builder() .setConfiguredTargetSignals("metrics, trace") .setUserSpecifiedOtelProperties( - ImmutableMap.builder() - .put( - "otel.exporter.otlp.metrics.endpoint", - "https://telemetry.googleapis.com/v1/metrics") - .put( - "otel.exporter.otlp.traces.endpoint", - "https://telemetry.googleapis.com/v1/traces") - .put("otel.traces.exporter", "otlp") - .put("otel.metrics.exporter", "otlp") - .put("otel.logs.exporter", "none") - .build()) + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) .setExpectedIsMetricsSignalModified(true) .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("none") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, trace, none") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all, none") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, none") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) .build())); } @@ -1114,7 +1190,7 @@ private void prepareMockBehaviorForGoogleCredentials() { private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) { return buildOpenTelemetrySdkWithExporter( - spanExporter, OtlpHttpMetricExporter.getDefault(), DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER); + spanExporter, OtlpHttpMetricExporter.getDefault(), defaultOtelPropertiesSpanExporter); } @SuppressWarnings("UnusedMethod") @@ -1126,7 +1202,7 @@ private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(MetricExporter metricExporter) { return buildOpenTelemetrySdkWithExporter( - OtlpHttpSpanExporter.getDefault(), metricExporter, DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER); + OtlpHttpSpanExporter.getDefault(), metricExporter, defaultOtelPropertiesMetricExporter); } @SuppressWarnings("UnusedMethod") diff --git a/sdks/python/apache_beam/yaml/test_utils/__init__.py b/sdks/python/apache_beam/yaml/test_utils/__init__.py deleted file mode 100644 index 89aea21adcf0..000000000000 --- a/sdks/python/apache_beam/yaml/test_utils/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""Helper utilities for YAML integration tests.""" From 3f82c20e5ec88ff74845313b2032450a386b02ac Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 17 Jun 2026 12:40:58 -0400 Subject: [PATCH 412/490] Disable public IP for Dataflow Python and Go Validate runner tests (#38990) --- .github/trigger_files/beam_PostCommit_Go.json | 2 +- ...am_PostCommit_Python_ValidatesRunner_Dataflow.json | 2 +- sdks/go/test/build.gradle | 3 +++ sdks/python/scripts/run_integration_test.sh | 11 ++++++++++- sdks/python/test-suites/dataflow/common.gradle | 2 ++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Go.json b/.github/trigger_files/beam_PostCommit_Go.json index 7ab7bcd9a9c6..37dd25bf9029 100644 --- a/.github/trigger_files/beam_PostCommit_Go.json +++ b/.github/trigger_files/beam_PostCommit_Go.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 2 + "modification": 3 } diff --git a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json index e0266d62f2e0..f1ba03a243ee 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 4 + "modification": 5 } diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index 677134716062..8fcb09166146 100644 --- a/sdks/go/test/build.gradle +++ b/sdks/go/test/build.gradle @@ -36,8 +36,10 @@ task dataflowValidatesRunner() { dependsOn ":sdks:go:test:goBuild" doLast { + def pipelineOptions = ["--no_use_public_ips"] def options = [ "--runner dataflow", + "--pipeline_opts \"${pipelineOptions.join(' ')}\"", ] exec { if (fork_java_home) { @@ -59,6 +61,7 @@ task dataflowValidatesRunnerARM64() { doLast { def pipelineOptions = [ // Pipeline options piped directly to Go SDK flags. "--machine_type=t2a-standard-1", + "--no_use_public_ips", ] def options = [ "--runner dataflow", diff --git a/sdks/python/scripts/run_integration_test.sh b/sdks/python/scripts/run_integration_test.sh index b9bb2457c961..4206f9ee9717 100755 --- a/sdks/python/scripts/run_integration_test.sh +++ b/sdks/python/scripts/run_integration_test.sh @@ -42,6 +42,8 @@ # flag is specified, all above flag will be ignored. # Please include all required pipeline options when # using this flag. +# additional_opts -> List of space separated pipeline options. Unlike pipeline_opts, +# it appends to other flags options instead of ignoring them. # # Test related flags: # test_opts -> List of space separated options to configure Pytest test @@ -150,6 +152,11 @@ case $key in shift # past argument shift # past value ;; + --additional_opts) + ADDITIONAL_OPTS="$2" + shift # past argument + shift # past value + ;; --test_opts) TEST_OPTS="$2" shift # past argument @@ -257,7 +264,9 @@ if [[ -z $PIPELINE_OPTS ]]; then fi PIPELINE_OPTS=$(IFS=" " ; echo "${opts[*]}") - + if [[ -n $ADDITIONAL_OPTS ]]; then + PIPELINE_OPTS+=" ${ADDITIONAL_OPTS}" + fi fi # Handle double quotes in PIPELINE_OPTS diff --git a/sdks/python/test-suites/dataflow/common.gradle b/sdks/python/test-suites/dataflow/common.gradle index 27cf2869600c..7c84700e29fa 100644 --- a/sdks/python/test-suites/dataflow/common.gradle +++ b/sdks/python/test-suites/dataflow/common.gradle @@ -245,6 +245,7 @@ task validatesRunnerBatchTests { doLast { def argMap = [ "test_opts" : basicPytestOpts + ["--numprocesses=8"], + "additional_opts": ['--no_use_public_ips'], "sdk_location": project.ext.sdkLocation, "suite" : "validatesRunnerBatchTests-df${pythonVersionSuffix}", "collect": "it_validatesrunner and not no_sickbay_batch" @@ -266,6 +267,7 @@ task validatesRunnerStreamingTests { doFirst { def argMap = [ "test_opts": basicPytestOpts + ["--numprocesses=8"], + "additional_opts": ['--no_use_public_ips'], "streaming": "true", "sdk_location": project.ext.sdkLocation, "suite": "validatesRunnerStreamingTests-df${pythonVersionSuffix}-xdist", From 2a00558ca111b2ff029f6d2239fc3c941805b1cb Mon Sep 17 00:00:00 2001 From: ddebowczyk92 Date: Wed, 17 Jun 2026 20:21:19 +0200 Subject: [PATCH 413/490] Add support for Apache Flink 2.1.3 (#38961) * [runners-flink] Add support for Apache Flink 2.1.3 * Improve lz4-java dependency resolution strategy Explicitly prefer Flink's at.yawk.lz4:lz4-java over org.lz4:lz4-java to ensure the Flink-compatible version is always selected, regardless of version numbers. This is more robust than selectHighestVersion() which could theoretically select org.lz4 if it had a higher version number. * Fix lz4-java capability conflict for examples:java flinkRunnerPreCommit Flink 2.1.3 uses at.yawk.lz4:lz4-java:1.10.3 while Kafka uses org.lz4:lz4-java:1.6.0, causing capability conflicts in configurations that depend on both (like examples:java flinkRunnerPreCommit). Add capability resolution strategy to select the highest version, consistent with the approach in runners/flink/2.1/build.gradle. * Remove unnecessary test resources for Flink 2.1 Flink 2.1 inherits test resources from parent versions via Beam's resource layering mechanism. No version-specific config needed. --- .../test-properties.json | 2 +- .../run_rc_validation_java_quickstart.yml | 2 +- .test-infra/validate-runner/build.gradle | 13 +++++ examples/java/common.gradle | 10 ++++ gradle.properties | 2 +- runners/flink/2.1/build.gradle | 56 +++++++++++++++++++ .../2.1/job-server-container/build.gradle | 26 +++++++++ runners/flink/2.1/job-server/build.gradle | 44 +++++++++++++++ sdks/go/examples/wasm/README.md | 6 +- sdks/go/test/build.gradle | 2 +- .../apache_beam/options/pipeline_options.py | 2 +- .../src/apache_beam/runners/flink.ts | 2 +- 12 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 runners/flink/2.1/build.gradle create mode 100644 runners/flink/2.1/job-server-container/build.gradle create mode 100644 runners/flink/2.1/job-server/build.gradle diff --git a/.github/actions/setup-default-test-properties/test-properties.json b/.github/actions/setup-default-test-properties/test-properties.json index f06de5174e6c..7a3f9890a294 100644 --- a/.github/actions/setup-default-test-properties/test-properties.json +++ b/.github/actions/setup-default-test-properties/test-properties.json @@ -14,7 +14,7 @@ }, "JavaTestProperties": { "SUPPORTED_VERSIONS": ["8", "11", "17", "21", "25"], - "FLINK_VERSIONS": ["1.17", "1.18", "1.19", "1.20", "2.0"], + "FLINK_VERSIONS": ["1.17", "1.18", "1.19", "1.20", "2.0", "2.1"], "SPARK_VERSIONS": ["3"] }, "GoTestProperties": { diff --git a/.github/workflows/run_rc_validation_java_quickstart.yml b/.github/workflows/run_rc_validation_java_quickstart.yml index dce9b7f3fedb..41a6991d14ee 100644 --- a/.github/workflows/run_rc_validation_java_quickstart.yml +++ b/.github/workflows/run_rc_validation_java_quickstart.yml @@ -88,7 +88,7 @@ jobs: - name: Run QuickStart Java Flink Runner uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:2.0:runQuickstartJavaFlinkLocal + gradle-command: :runners:flink:2.1:runQuickstartJavaFlinkLocal arguments: | -Prepourl=${{ env.APACHE_REPO_URL }} \ -Pver=${{ env.RELEASE_VERSION }} diff --git a/.test-infra/validate-runner/build.gradle b/.test-infra/validate-runner/build.gradle index 1817d7014a6c..3992abb24dd4 100644 --- a/.test-infra/validate-runner/build.gradle +++ b/.test-infra/validate-runner/build.gradle @@ -31,6 +31,19 @@ repositories { } } +// Flink 2.1+ uses at.yawk.lz4:lz4-java while Spark uses org.lz4:lz4-java +// Resolve capability conflict by preferring Flink's version when both are present +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} + dependencies { implementation 'com.offbytwo.jenkins:jenkins-client:0.3.8' implementation library.java.jackson_databind diff --git a/examples/java/common.gradle b/examples/java/common.gradle index 10ea43628bc8..0e800e7cf987 100644 --- a/examples/java/common.gradle +++ b/examples/java/common.gradle @@ -35,6 +35,16 @@ configurations.sparkRunnerPreCommit { exclude group: "org.slf4j", module: "jul-to-slf4j" exclude group: "org.slf4j", module: "slf4j-jdk14" } +configurations.flinkRunnerPreCommit { + resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} dependencies { directRunnerPreCommit project(path: ":runners:direct-java", configuration: "shadow") diff --git a/gradle.properties b/gradle.properties index 95e50105a494..0289d02722b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ docker_image_default_repo_root=apache docker_image_default_repo_prefix=beam_ # supported flink versions -flink_versions=1.17,1.18,1.19,1.20,2.0 +flink_versions=1.17,1.18,1.19,1.20,2.0,2.1 # supported spark versions spark_versions=3,4 # supported python versions diff --git a/runners/flink/2.1/build.gradle b/runners/flink/2.1/build.gradle new file mode 100644 index 000000000000..e9092c2977f7 --- /dev/null +++ b/runners/flink/2.1/build.gradle @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +project.ext { + flink_major = '2.1' + flink_version = '2.1.3' + excluded_files = [ + 'main': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapter.java", + "org/apache/beam/runners/flink/FlinkBatchPipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchPortablePipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchTransformTranslators.java", + "org/apache/beam/runners/flink/translation/functions/FlinkNonMergingReduceFunction.java", + // Moved to org.apache.flink.runtime.state.StateBackendFactory + "org/apache/beam/runners/flink/FlinkStateBackendFactory.java", + ], + 'test': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapterTest.java", + "org/apache/beam/runners/flink/batch/NonMergingGroupByKeyTest.java", + "org/apache/beam/runners/flink/batch/ReshuffleTest.java", + ] + ] +} + +// Load the main build script which contains all build logic. +apply from: "../flink_runner.gradle" + +// Flink 2.1 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} diff --git a/runners/flink/2.1/job-server-container/build.gradle b/runners/flink/2.1/job-server-container/build.gradle new file mode 100644 index 000000000000..afdb68a0fc91 --- /dev/null +++ b/runners/flink/2.1/job-server-container/build.gradle @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '../../job-server-container' + +project.ext { + resource_path = basePath +} + +// Load the main build script which contains all build logic. +apply from: "$basePath/flink_job_server_container.gradle" diff --git a/runners/flink/2.1/job-server/build.gradle b/runners/flink/2.1/job-server/build.gradle new file mode 100644 index 000000000000..277ddc07fdaf --- /dev/null +++ b/runners/flink/2.1/job-server/build.gradle @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '../../job-server' + +project.ext { + // Look for the source code in the parent module + main_source_dirs = ["$basePath/src/main/java"] + test_source_dirs = ["$basePath/src/test/java"] + main_resources_dirs = ["$basePath/src/main/resources"] + test_resources_dirs = ["$basePath/src/test/resources"] + archives_base_name = 'beam-runners-flink-2.1-job-server' +} + +// Load the main build script which contains all build logic. +apply from: "$basePath/flink_job_server.gradle" + +// Flink 2.1 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} diff --git a/sdks/go/examples/wasm/README.md b/sdks/go/examples/wasm/README.md index e4ab54d4a3ed..30fd22f624be 100644 --- a/sdks/go/examples/wasm/README.md +++ b/sdks/go/examples/wasm/README.md @@ -68,13 +68,13 @@ cd $BEAM_HOME Expected output should include the following, from which you acquire the latest flink runner version. ```shell -'flink_versions: 1.17,1.18,1.19,1.20' +'flink_versions: 1.17,1.18,1.19,1.20,2.0,2.1' ``` -#### 2. Set to the latest flink runner version i.e. 1.16 +#### 2. Set to the latest flink runner version i.e. 2.1 ```shell -FLINK_VERSION=1.16 +FLINK_VERSION=2.1 ``` #### 3. In a separate terminal, start the flink runner (It should take a few minutes on the first execution) diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index 8fcb09166146..3437ba12f2c7 100644 --- a/sdks/go/test/build.gradle +++ b/sdks/go/test/build.gradle @@ -92,7 +92,7 @@ task flinkValidatesRunner { doFirst { // Copy Flink conf file copy { - from "${project.rootDir}/runners/flink/2.0/src/test/resources/flink-test-config.yaml" + from "${project.rootDir}/runners/flink/${flinkVersion}/src/test/resources/flink-test-config.yaml" into "${project.buildDir}/flink-conf" // Rename the file during the copy process diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index c813939d53f1..978a79bf6172 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -2106,7 +2106,7 @@ def _add_argparse_args(cls, parser): class FlinkRunnerOptions(PipelineOptions): # These should stay in sync with gradle.properties. - PUBLISHED_FLINK_VERSIONS = ['1.17', '1.18', '1.19', '1.20', '2.0'] + PUBLISHED_FLINK_VERSIONS = ['1.17', '1.18', '1.19', '1.20', '2.0', '2.1'] @classmethod def _add_argparse_args(cls, parser): diff --git a/sdks/typescript/src/apache_beam/runners/flink.ts b/sdks/typescript/src/apache_beam/runners/flink.ts index 8f80b971da2a..c8f8f57eb080 100644 --- a/sdks/typescript/src/apache_beam/runners/flink.ts +++ b/sdks/typescript/src/apache_beam/runners/flink.ts @@ -28,7 +28,7 @@ import { JavaJarService } from "../utils/service"; const MAGIC_HOST_NAMES = ["[local]", "[auto]"]; // These should stay in sync with gradle.properties. -const PUBLISHED_FLINK_VERSIONS = ["1.17", "1.18", "1.19", "1.20", "2.0"]; +const PUBLISHED_FLINK_VERSIONS = ["1.17", "1.18", "1.19", "1.20", "2.0", "2.1"]; const defaultOptions = { flinkMaster: "[local]", From 7d70f416ea7f70df513ec7e590cfc9fdb8bad2ba Mon Sep 17 00:00:00 2001 From: "RuiLong J." Date: Wed, 17 Jun 2026 11:58:08 -0700 Subject: [PATCH 414/490] Make ModelManager import more robust (#38936) * Make ModelManager import more robust * Make ModelManager import more robust 2 * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Add model tag to annotations as well * Change to not use function import --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sdks/python/apache_beam/ml/inference/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdks/python/apache_beam/ml/inference/base.py b/sdks/python/apache_beam/ml/inference/base.py index b2441281dd18..f81382bbeecf 100644 --- a/sdks/python/apache_beam/ml/inference/base.py +++ b/sdks/python/apache_beam/ml/inference/base.py @@ -1443,6 +1443,7 @@ def annotations(self): 'model_handler_type': ( f'{self._model_handler.__class__.__module__}' f'.{self._model_handler.__class__.__qualname__}'), + 'model_identifier': self._model_tag, **super().annotations() } @@ -1997,6 +1998,11 @@ def load(): # Ensure the tag we're loading is valid, if not replace it with a valid tag self._cur_tag = self._model_metadata.get_valid_tag(model_tag) if self.use_model_manager: + # Force an import here to avoid missing ModelManager when needed. + # Throw an error if ModelManager is not available since it's required for this code path. + global ModelManager + if ModelManager is None: + from apache_beam.ml.inference.model_manager import ModelManager logging.info("Using Model Manager to manage models automatically.") model_manager = multi_process_shared.MultiProcessShared( lambda: ModelManager(**self._model_manager_args), From 65f955879cd8421990914782cb227ec5771301f8 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:45:08 -0400 Subject: [PATCH 415/490] [Beam SQL Shell] Add Iceberg deps when using icebergio (#38994) * add iceberg sql dep when using icebergio * add maven snapshot repo --- scripts/beam-sql.sh | 73 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/scripts/beam-sql.sh b/scripts/beam-sql.sh index c9ec365b99bc..9df48f6c18d4 100755 --- a/scripts/beam-sql.sh +++ b/scripts/beam-sql.sh @@ -36,6 +36,8 @@ MAVEN_DISTRIBUTION_URL="https://repo.maven.apache.org/maven2/org/apache/maven/ap # Maven Plugin Configuration MAVEN_SHADE_PLUGIN_VERSION="3.5.1" +ICEBERG_VERSION="${ICEBERG_VERSION:-1.10.0}" +APACHE_SNAPSHOT_REPOSITORY_URL="https://repository.apache.org/content/repositories/snapshots/" mkdir -p "${CACHE_DIR}" # Create a temporary directory for our Maven project. @@ -84,6 +86,38 @@ function setup_maven_wrapper() { MAVEN_CMD="${mvnw_script}" } +function add_beam_dependency() { + local artifact_id="$1" + cat >> "${POM_FILE}" << EOL + + org.apache.beam + ${artifact_id} + \${beam.version} + +EOL +} + +function add_dependency() { + local group_id="$1" + local artifact_id="$2" + local version="$3" + cat >> "${POM_FILE}" << EOL + + ${group_id} + ${artifact_id} + ${version} + +EOL +} + +function normalize_beam_version() { + case "${BEAM_VERSION}" in + *-[sS][nN][aA][pP][sS][hH][oO][tT]) + BEAM_VERSION="${BEAM_VERSION%-*}-SNAPSHOT" + ;; + esac +} + function usage() { echo "Usage: $0 [--version ] [--runner ] [--io ] [--list-versions] [--list-ios] [--list-runners] [--debug] [-h|--help]" echo "" @@ -91,6 +125,7 @@ function usage() { echo "" echo "Options:" echo " --version Specify the Apache Beam version (default: ${DEFAULT_BEAM_VERSION})." + echo " SNAPSHOT versions are resolved from Apache's Maven snapshot repository." echo " --runner Specify the Beam runner to use (default: direct)." echo " Supported runners:" echo " direct - DirectRunner (runs locally, good for development)" @@ -208,7 +243,7 @@ function list_runners() { echo "✅ Available runners for Beam ${BEAM_VERSION}:" echo "" - + # Process each runner and provide descriptions while IFS= read -r runner; do case "$runner" in @@ -217,7 +252,7 @@ function list_runners() { echo " Runs locally on your machine. Good for development and testing." ;; "google-cloud-dataflow-java") - echo " dataflow - DataflowRunner" + echo " dataflow - DataflowRunner" echo " Runs on Google Cloud Dataflow for production workloads." ;; flink-*) @@ -273,7 +308,7 @@ function list_runners() { ;; esac done <<< "$runners" - + echo "" echo "💡 Usage: ./beam-sql.sh --runner " echo " Default: direct" @@ -291,7 +326,7 @@ DEBUG_MODE=false while [[ "$#" -gt 0 ]]; do case $1 in - --version) BEAM_VERSION="$2"; shift ;; + --version) BEAM_VERSION="$2"; normalize_beam_version; shift ;; --runner) BEAM_RUNNER=$(echo "$2" | tr '[:upper:]' '[:lower:]'); shift ;; --io) IO_CONNECTORS+=("$2"); shift ;; --list-versions) list_versions; exit 0 ;; @@ -363,7 +398,15 @@ else EOL # Add IO and Runner dependencies for io in "${IO_CONNECTORS[@]}"; do - echo " org.apache.beambeam-sdks-java-io-${io}\${beam.version}" >> "${POM_FILE}" + add_beam_dependency "beam-sdks-java-io-${io}" + case "${io}" in + iceberg) + add_beam_dependency "beam-sdks-java-extensions-sql-iceberg" + add_dependency "org.apache.iceberg" "iceberg-aws" "${ICEBERG_VERSION}" + add_dependency "org.apache.iceberg" "iceberg-aws-bundle" "${ICEBERG_VERSION}" + add_dependency "org.apache.iceberg" "iceberg-gcp" "${ICEBERG_VERSION}" + ;; + esac done RUNNER_ARTIFACT="" case "${BEAM_RUNNER}" in @@ -372,12 +415,25 @@ EOL *) echo "❌ Error: Unsupported runner '${BEAM_RUNNER}'." >&2; exit 1 ;; esac if [ -n "${RUNNER_ARTIFACT}" ]; then - echo " org.apache.beam${RUNNER_ARTIFACT}\${beam.version}" >> "${POM_FILE}" + add_beam_dependency "${RUNNER_ARTIFACT}" fi # Complete the POM with the build section for the maven-shade-plugin cat >> "${POM_FILE}" << EOL + + + apache.snapshots + ${APACHE_SNAPSHOT_REPOSITORY_URL} + + false + + + true + always + + + ${BEAM_VERSION} @@ -395,6 +451,11 @@ cat >> "${POM_FILE}" << EOL + + + true + + From d40bb153540f00f2bfe5691fa423014c28795f28 Mon Sep 17 00:00:00 2001 From: Lalit Yadav Date: Wed, 17 Jun 2026 19:16:03 -0500 Subject: [PATCH 416/490] Add publish_time_field to ReadFromPubSub YAML transform (#38985) * Add publish_time_field to ReadFromPubSub YAML transform * Format YAML PubSub publish time test * Fix YAML PubSub publish time handling --- sdks/python/apache_beam/yaml/yaml_io.py | 21 +++- sdks/python/apache_beam/yaml/yaml_io_test.py | 103 +++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/sdks/python/apache_beam/yaml/yaml_io.py b/sdks/python/apache_beam/yaml/yaml_io.py index 989661a6eae4..77cbc41def32 100644 --- a/sdks/python/apache_beam/yaml/yaml_io.py +++ b/sdks/python/apache_beam/yaml/yaml_io.py @@ -46,6 +46,7 @@ from apache_beam.io.gcp.bigquery import BigQueryDisposition from apache_beam.portability.api import schema_pb2 from apache_beam.typehints import schemas +from apache_beam.utils.timestamp import Timestamp from apache_beam.yaml import json_utils from apache_beam.yaml import yaml_errors from apache_beam.yaml import yaml_provider @@ -316,7 +317,8 @@ def read_from_pubsub( attributes: Optional[Iterable[str]] = None, attributes_map: Optional[str] = None, id_attribute: Optional[str] = None, - timestamp_attribute: Optional[str] = None): + timestamp_attribute: Optional[str] = None, + publish_time_field: Optional[str] = None): """Reads messages from Cloud Pub/Sub. Args: @@ -366,14 +368,19 @@ def read_from_pubsub( ``2015-10-29T23:41:41.123Z``. The sub-second component of the timestamp is optional, and digits beyond the first three (i.e., time units smaller than milliseconds) may be ignored. + publish_time_field: Field to add to output messages with the Pub/Sub + message publish time. If None, no such field is added. """ if topic and subscription: raise TypeError('Only one of topic and subscription may be specified.') elif not topic and not subscription: raise TypeError('One of topic or subscription may be specified.') + if publish_time_field is not None and not publish_time_field.strip(): + raise ValueError('publish_time_field must be a non-empty field name.') + has_publish_time_field = publish_time_field is not None payload_schema, parser = _create_parser(format, schema) extra_fields: list[schema_pb2.Field] = [] - if not attributes and not attributes_map: + if not attributes and not attributes_map and not has_publish_time_field: mapper = lambda msg: parser(msg) else: if isinstance(attributes, str): @@ -384,6 +391,9 @@ def read_from_pubsub( if attributes_map: extra_fields.append( schemas.schema_field(attributes_map, Mapping[str, str])) + if has_publish_time_field: + extra_fields.append( + schemas.schema_field(publish_time_field, Optional[Timestamp])) def mapper(msg): values = parser(msg.data).as_dict() @@ -393,6 +403,10 @@ def mapper(msg): values[attr] = msg.attributes[attr] if attributes_map: values[attributes_map] = msg.attributes + if has_publish_time_field: + values[publish_time_field] = ( + Timestamp.of(msg.publish_time) + if msg.publish_time is not None else None) return beam.Row(**values) output = ( @@ -400,7 +414,8 @@ def mapper(msg): | beam.io.ReadFromPubSub( topic=topic, subscription=subscription, - with_attributes=bool(attributes or attributes_map), + with_attributes=bool( + attributes or attributes_map or has_publish_time_field), id_label=id_attribute, timestamp_attribute=timestamp_attribute) | 'ParseMessage' >> beam.Map(mapper)) diff --git a/sdks/python/apache_beam/yaml/yaml_io_test.py b/sdks/python/apache_beam/yaml/yaml_io_test.py index e6219277bf58..250a54689f5a 100644 --- a/sdks/python/apache_beam/yaml/yaml_io_test.py +++ b/sdks/python/apache_beam/yaml/yaml_io_test.py @@ -15,6 +15,7 @@ # limitations under the License. # +import datetime import io import json import logging @@ -32,6 +33,7 @@ from apache_beam.testing.util import assert_that from apache_beam.testing.util import equal_to from apache_beam.typehints import schemas as schema_utils +from apache_beam.utils.timestamp import Timestamp from apache_beam.yaml.yaml_transform import YamlTransform try: @@ -181,6 +183,107 @@ def test_read_with_attribute_map(self): beam.Row(payload=b'msg2', attrMap={'attr': 'value2'}) ])) + def test_read_with_publish_time_field(self): + publish_time_1 = datetime.datetime( + 2018, 3, 12, 13, 37, 1, 234567, tzinfo=datetime.timezone.utc) + publish_time_2 = datetime.datetime( + 2018, 3, 12, 13, 38, 2, 345678, tzinfo=datetime.timezone.utc) + publish_time_3 = Timestamp.from_utc_datetime( + datetime.datetime( + 2018, 3, 12, 13, 39, 3, 456789, tzinfo=datetime.timezone.utc)) + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + with mock.patch('apache_beam.io.ReadFromPubSub', + FakeReadFromPubSub( + topic='my_topic', + messages=[PubsubMessage(b'msg1', {'attr': 'value1'}, + publish_time=publish_time_1), + PubsubMessage(b'msg2', {'attr': 'value2'}, + publish_time=publish_time_2), + PubsubMessage(b'msg3', {'attr': 'value3'}, + publish_time=publish_time_3), + PubsubMessage(b'msg4', + {'attr': 'value4'})])): + result = p | YamlTransform( + ''' + type: ReadFromPubSub + config: + topic: my_topic + format: RAW + publish_time_field: publish_time + ''') + assert_that( + result, + equal_to([ + beam.Row( + payload=b'msg1', + publish_time=Timestamp.from_utc_datetime(publish_time_1)), + beam.Row( + payload=b'msg2', + publish_time=Timestamp.from_utc_datetime(publish_time_2)), + beam.Row(payload=b'msg3', publish_time=publish_time_3), + beam.Row(payload=b'msg4', publish_time=None) + ])) + + def test_read_with_attributes_and_publish_time_field(self): + publish_time_1 = Timestamp.from_utc_datetime( + datetime.datetime( + 2018, 3, 12, 13, 37, 1, 234567, tzinfo=datetime.timezone.utc)) + publish_time_2 = Timestamp.from_utc_datetime( + datetime.datetime( + 2018, 3, 12, 13, 38, 2, 345678, tzinfo=datetime.timezone.utc)) + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + with mock.patch('apache_beam.io.ReadFromPubSub', + FakeReadFromPubSub( + topic='my_topic', + messages=[PubsubMessage(b'msg1', {'attr': 'value1'}, + publish_time=publish_time_1), + PubsubMessage(b'msg2', {'attr': 'value2'}, + publish_time=publish_time_2) + ])): + result = p | YamlTransform( + ''' + type: ReadFromPubSub + config: + topic: my_topic + format: RAW + attributes: [attr] + attributes_map: attrMap + publish_time_field: publish_time + ''') + assert_that( + result, + equal_to([ + beam.Row( + payload=b'msg1', + attr='value1', + attrMap={'attr': 'value1'}, + publish_time=publish_time_1), + beam.Row( + payload=b'msg2', + attr='value2', + attrMap={'attr': 'value2'}, + publish_time=publish_time_2) + ])) + + def test_read_with_empty_publish_time_field(self): + for publish_time_field in ('', ' '): + with self.subTest(publish_time_field=publish_time_field): + with self.assertRaisesRegex( + ValueError, 'publish_time_field must be a non-empty field name'): + with beam.Pipeline( + options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + _ = p | YamlTransform( + ''' + type: ReadFromPubSub + config: + topic: my_topic + format: RAW + publish_time_field: "%s" + ''' % publish_time_field) + def test_read_with_id_attribute(self): with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( pickle_library='cloudpickle')) as p: From 109a42052c54ed4517f2b88ec16b56de83748695 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:14:02 -0400 Subject: [PATCH 417/490] Bump cloud.google.com/go/bigtable from 1.49.0 to 1.50.0 in /sdks (#39012) Bumps [cloud.google.com/go/bigtable](https://github.com/googleapis/google-cloud-go) from 1.49.0 to 1.50.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.49.0...pubsub/v1.50.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/bigtable dependency-version: 1.50.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 84922261fe61..8bdb10311285 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -26,7 +26,7 @@ toolchain go1.26.2 require ( cloud.google.com/go/bigquery v1.77.0 - cloud.google.com/go/bigtable v1.49.0 + cloud.google.com/go/bigtable v1.50.0 cloud.google.com/go/datastore v1.24.0 cloud.google.com/go/profiler v0.6.0 cloud.google.com/go/pubsub v1.50.2 diff --git a/sdks/go.sum b/sdks/go.sum index 2610159c6693..e8238c1c2264 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -46,8 +46,8 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.77.0 h1:L5AW3jhzEKpFVg4i0mVHxKpxogrqT7dczWBSr4m9MKU= cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= -cloud.google.com/go/bigtable v1.49.0 h1:kdA1jbO8EJIMI752zD50o5z6Wu82cvCwIibhrpJK0tI= -cloud.google.com/go/bigtable v1.49.0/go.mod h1:RTannV5mvoJM8KscLTfRYMPo84u9/j+C3PSyYJGf5Ic= +cloud.google.com/go/bigtable v1.50.0 h1:lihc3U/eVrlIjK55i93K8sol+pKtFozIJ9vooEuNd4I= +cloud.google.com/go/bigtable v1.50.0/go.mod h1:RTannV5mvoJM8KscLTfRYMPo84u9/j+C3PSyYJGf5Ic= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= From bad0d3f22e68cd242b1b736d896959df5d1b6c14 Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Thu, 18 Jun 2026 17:41:59 +0400 Subject: [PATCH 418/490] Exclude tests for batch-dataset (#39014) --- runners/flink/job-server/flink_job_server.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runners/flink/job-server/flink_job_server.gradle b/runners/flink/job-server/flink_job_server.gradle index b85f8fc98aaa..335439cbda91 100644 --- a/runners/flink/job-server/flink_job_server.gradle +++ b/runners/flink/job-server/flink_job_server.gradle @@ -193,7 +193,7 @@ def portableValidatesRunnerTask(String name, String mode, boolean checkpointing, excludeCategories 'org.apache.beam.sdk.testing.UsesTriggeredSideInputs' return } - if (mode == "batch") { + if (mode == "batch" || mode == "batch-dataset") { excludeCategories 'org.apache.beam.sdk.testing.UsesTriggeredSideInputs' } excludeCategories 'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo' From 46c69237abc278666fe6687bec70b1de2066a837 Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Thu, 18 Jun 2026 09:57:01 -0400 Subject: [PATCH 419/490] fix parsing error for typescript (#38898) --- website/www/site/assets/js/page-nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/www/site/assets/js/page-nav.js b/website/www/site/assets/js/page-nav.js index 3487421c958c..21e692f3260d 100644 --- a/website/www/site/assets/js/page-nav.js +++ b/website/www/site/assets/js/page-nav.js @@ -105,7 +105,7 @@ $(document).ready(function() { var items = $(".page-nav > #TableOfContents li"); var itemTags = $('ul', items).siblings('a'); var img = document.createElement("img"); - img.src = "{{ "images/arrow-expandable.svg" | absURL }}"; + img.src = '{{ "images/arrow-expandable.svg" | absURL }}'; img.classList="rotate"; $(itemTags).prepend(img); From 05121340f1381733252156d739ddd9ab5bc7184b Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Thu, 18 Jun 2026 10:07:15 -0400 Subject: [PATCH 420/490] Drop Flink 1.17,1.18 support (#39006) --- .../test-properties.json | 2 +- .../beam_PreCommit_Flink_Container.yml | 2 +- gradle.properties | 2 +- runners/flink/1.17/build.gradle | 25 ------ .../1.17/job-server-container/build.gradle | 26 ------ runners/flink/1.17/job-server/build.gradle | 31 ------- runners/flink/1.18/build.gradle | 25 ------ .../1.18/job-server-container/build.gradle | 26 ------ runners/flink/1.18/job-server/build.gradle | 31 ------- .../streaming/MemoryStateBackendWrapper.java | 80 ------------------- .../flink/streaming/StreamSources.java | 61 -------------- .../streaming/MemoryStateBackendWrapper.java | 28 ++++--- .../flink/streaming/StreamSources.java | 7 ++ sdks/go/examples/wasm/README.md | 2 +- .../apache_beam/options/pipeline_options.py | 2 +- .../src/apache_beam/runners/flink.ts | 2 +- 16 files changed, 29 insertions(+), 323 deletions(-) delete mode 100644 runners/flink/1.17/build.gradle delete mode 100644 runners/flink/1.17/job-server-container/build.gradle delete mode 100644 runners/flink/1.17/job-server/build.gradle delete mode 100644 runners/flink/1.18/build.gradle delete mode 100644 runners/flink/1.18/job-server-container/build.gradle delete mode 100644 runners/flink/1.18/job-server/build.gradle delete mode 100644 runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java delete mode 100644 runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java diff --git a/.github/actions/setup-default-test-properties/test-properties.json b/.github/actions/setup-default-test-properties/test-properties.json index 7a3f9890a294..ddb4a8b73ffb 100644 --- a/.github/actions/setup-default-test-properties/test-properties.json +++ b/.github/actions/setup-default-test-properties/test-properties.json @@ -14,7 +14,7 @@ }, "JavaTestProperties": { "SUPPORTED_VERSIONS": ["8", "11", "17", "21", "25"], - "FLINK_VERSIONS": ["1.17", "1.18", "1.19", "1.20", "2.0", "2.1"], + "FLINK_VERSIONS": ["1.19", "1.20", "2.0", "2.1"], "SPARK_VERSIONS": ["3"] }, "GoTestProperties": { diff --git a/.github/workflows/beam_PreCommit_Flink_Container.yml b/.github/workflows/beam_PreCommit_Flink_Container.yml index 9d4ff5e9636c..1f6c209cfd2b 100644 --- a/.github/workflows/beam_PreCommit_Flink_Container.yml +++ b/.github/workflows/beam_PreCommit_Flink_Container.yml @@ -173,7 +173,7 @@ jobs: gradle-command: :sdks:java:testing:load-tests:run arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CombineLoadTest \ - -Prunner=:runners:flink:1.17 \ + -Prunner=:runners:flink:2.0 \ '-PloadTest.args=${{ env.beam_PreCommit_Flink_Container_test_arguments_3 }} --jobName=flink-tests-java11-${{env.NOW_UTC}}' - name: Teardown Flink diff --git a/gradle.properties b/gradle.properties index 0289d02722b3..c20a8373cb60 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ docker_image_default_repo_root=apache docker_image_default_repo_prefix=beam_ # supported flink versions -flink_versions=1.17,1.18,1.19,1.20,2.0,2.1 +flink_versions=1.19,1.20,2.0,2.1 # supported spark versions spark_versions=3,4 # supported python versions diff --git a/runners/flink/1.17/build.gradle b/runners/flink/1.17/build.gradle deleted file mode 100644 index ae69b879eba9..000000000000 --- a/runners/flink/1.17/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -project.ext { - flink_major = '1.17' - flink_version = '1.17.0' -} - -// Load the main build script which contains all build logic. -apply from: "../flink_runner.gradle" diff --git a/runners/flink/1.17/job-server-container/build.gradle b/runners/flink/1.17/job-server-container/build.gradle deleted file mode 100644 index afdb68a0fc91..000000000000 --- a/runners/flink/1.17/job-server-container/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -def basePath = '../../job-server-container' - -project.ext { - resource_path = basePath -} - -// Load the main build script which contains all build logic. -apply from: "$basePath/flink_job_server_container.gradle" diff --git a/runners/flink/1.17/job-server/build.gradle b/runners/flink/1.17/job-server/build.gradle deleted file mode 100644 index 89915349ae9a..000000000000 --- a/runners/flink/1.17/job-server/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -def basePath = '../../job-server' - -project.ext { - // Look for the source code in the parent module - main_source_dirs = ["$basePath/src/main/java"] - test_source_dirs = ["$basePath/src/test/java"] - main_resources_dirs = ["$basePath/src/main/resources"] - test_resources_dirs = ["$basePath/src/test/resources"] - archives_base_name = 'beam-runners-flink-1.17-job-server' -} - -// Load the main build script which contains all build logic. -apply from: "$basePath/flink_job_server.gradle" diff --git a/runners/flink/1.18/build.gradle b/runners/flink/1.18/build.gradle deleted file mode 100644 index ab6e6b63b773..000000000000 --- a/runners/flink/1.18/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -project.ext { - flink_major = '1.18' - flink_version = '1.18.0' -} - -// Load the main build script which contains all build logic. -apply from: "../flink_runner.gradle" diff --git a/runners/flink/1.18/job-server-container/build.gradle b/runners/flink/1.18/job-server-container/build.gradle deleted file mode 100644 index afdb68a0fc91..000000000000 --- a/runners/flink/1.18/job-server-container/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -def basePath = '../../job-server-container' - -project.ext { - resource_path = basePath -} - -// Load the main build script which contains all build logic. -apply from: "$basePath/flink_job_server_container.gradle" diff --git a/runners/flink/1.18/job-server/build.gradle b/runners/flink/1.18/job-server/build.gradle deleted file mode 100644 index e70fdcc0c581..000000000000 --- a/runners/flink/1.18/job-server/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -def basePath = '../../job-server' - -project.ext { - // Look for the source code in the parent module - main_source_dirs = ["$basePath/src/main/java"] - test_source_dirs = ["$basePath/src/test/java"] - main_resources_dirs = ["$basePath/src/main/resources"] - test_resources_dirs = ["$basePath/src/test/resources"] - archives_base_name = 'beam-runners-flink-1.18-job-server' -} - -// Load the main build script which contains all build logic. -apply from: "$basePath/flink_job_server.gradle" diff --git a/runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java b/runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java deleted file mode 100644 index cbaa6fd3a8c4..000000000000 --- a/runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.flink.streaming; - -import java.util.Collection; -import org.apache.flink.api.common.JobID; -import org.apache.flink.api.common.typeutils.TypeSerializer; -import org.apache.flink.core.fs.CloseableRegistry; -import org.apache.flink.metrics.MetricGroup; -import org.apache.flink.runtime.execution.Environment; -import org.apache.flink.runtime.query.TaskKvStateRegistry; -import org.apache.flink.runtime.state.AbstractKeyedStateBackend; -import org.apache.flink.runtime.state.BackendBuildingException; -import org.apache.flink.runtime.state.KeyGroupRange; -import org.apache.flink.runtime.state.KeyedStateBackendParametersImpl; -import org.apache.flink.runtime.state.KeyedStateHandle; -import org.apache.flink.runtime.state.OperatorStateBackend; -import org.apache.flink.runtime.state.OperatorStateBackendParametersImpl; -import org.apache.flink.runtime.state.OperatorStateHandle; -import org.apache.flink.runtime.state.memory.MemoryStateBackend; -import org.apache.flink.runtime.state.ttl.TtlTimeProvider; - -class MemoryStateBackendWrapper { - static AbstractKeyedStateBackend createKeyedStateBackend( - Environment env, - JobID jobID, - String operatorIdentifier, - TypeSerializer keySerializer, - int numberOfKeyGroups, - KeyGroupRange keyGroupRange, - TaskKvStateRegistry kvStateRegistry, - TtlTimeProvider ttlTimeProvider, - MetricGroup metricGroup, - Collection stateHandles, - CloseableRegistry cancelStreamRegistry) - throws BackendBuildingException { - - MemoryStateBackend backend = new MemoryStateBackend(); - return backend.createKeyedStateBackend( - new KeyedStateBackendParametersImpl<>( - env, - jobID, - operatorIdentifier, - keySerializer, - numberOfKeyGroups, - keyGroupRange, - kvStateRegistry, - ttlTimeProvider, - metricGroup, - stateHandles, - cancelStreamRegistry)); - } - - static OperatorStateBackend createOperatorStateBackend( - Environment env, - String operatorIdentifier, - Collection stateHandles, - CloseableRegistry cancelStreamRegistry) - throws Exception { - MemoryStateBackend backend = new MemoryStateBackend(); - return backend.createOperatorStateBackend( - new OperatorStateBackendParametersImpl( - env, operatorIdentifier, stateHandles, cancelStreamRegistry)); - } -} diff --git a/runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java b/runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java deleted file mode 100644 index 793959c5e693..000000000000 --- a/runners/flink/1.19/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.flink.streaming; - -import org.apache.flink.runtime.operators.testutils.MockEnvironmentBuilder; -import org.apache.flink.streaming.api.functions.source.SourceFunction; -import org.apache.flink.streaming.api.operators.AbstractStreamOperator; -import org.apache.flink.streaming.api.operators.Output; -import org.apache.flink.streaming.api.operators.StreamSource; -import org.apache.flink.streaming.runtime.streamrecord.RecordAttributes; -import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; -import org.apache.flink.streaming.runtime.tasks.OperatorChain; -import org.apache.flink.streaming.runtime.tasks.RegularOperatorChain; -import org.apache.flink.streaming.runtime.tasks.StreamTask; -import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus; - -/** {@link StreamSource} utilities, that bridge incompatibilities between Flink releases. */ -public class StreamSources { - - public static > void run( - StreamSource streamSource, - Object lockingObject, - Output> collector) - throws Exception { - streamSource.run(lockingObject, collector, createOperatorChain(streamSource)); - } - - private static OperatorChain createOperatorChain(AbstractStreamOperator operator) { - return new RegularOperatorChain<>( - operator.getContainingTask(), - StreamTask.createRecordWriterDelegate( - operator.getOperatorConfig(), new MockEnvironmentBuilder().build())); - } - - /** The emitWatermarkStatus method was added in Flink 1.14, so we need to wrap Output. */ - public interface OutputWrapper extends Output { - @Override - default void emitWatermarkStatus(WatermarkStatus watermarkStatus) {} - - /** In Flink 1.19 the {@code recordAttributes} method was added. */ - @Override - default void emitRecordAttributes(RecordAttributes recordAttributes) { - throw new UnsupportedOperationException("emitRecordAttributes not implemented"); - } - } -} diff --git a/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java index 7317788a72ee..cbaa6fd3a8c4 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/MemoryStateBackendWrapper.java @@ -27,8 +27,10 @@ import org.apache.flink.runtime.state.AbstractKeyedStateBackend; import org.apache.flink.runtime.state.BackendBuildingException; import org.apache.flink.runtime.state.KeyGroupRange; +import org.apache.flink.runtime.state.KeyedStateBackendParametersImpl; import org.apache.flink.runtime.state.KeyedStateHandle; import org.apache.flink.runtime.state.OperatorStateBackend; +import org.apache.flink.runtime.state.OperatorStateBackendParametersImpl; import org.apache.flink.runtime.state.OperatorStateHandle; import org.apache.flink.runtime.state.memory.MemoryStateBackend; import org.apache.flink.runtime.state.ttl.TtlTimeProvider; @@ -50,17 +52,18 @@ static AbstractKeyedStateBackend createKeyedStateBackend( MemoryStateBackend backend = new MemoryStateBackend(); return backend.createKeyedStateBackend( - env, - jobID, - operatorIdentifier, - keySerializer, - numberOfKeyGroups, - keyGroupRange, - kvStateRegistry, - ttlTimeProvider, - metricGroup, - stateHandles, - cancelStreamRegistry); + new KeyedStateBackendParametersImpl<>( + env, + jobID, + operatorIdentifier, + keySerializer, + numberOfKeyGroups, + keyGroupRange, + kvStateRegistry, + ttlTimeProvider, + metricGroup, + stateHandles, + cancelStreamRegistry)); } static OperatorStateBackend createOperatorStateBackend( @@ -71,6 +74,7 @@ static OperatorStateBackend createOperatorStateBackend( throws Exception { MemoryStateBackend backend = new MemoryStateBackend(); return backend.createOperatorStateBackend( - env, operatorIdentifier, stateHandles, cancelStreamRegistry); + new OperatorStateBackendParametersImpl( + env, operatorIdentifier, stateHandles, cancelStreamRegistry)); } } diff --git a/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java index d1e38549d8ed..793959c5e693 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/streaming/StreamSources.java @@ -22,6 +22,7 @@ import org.apache.flink.streaming.api.operators.AbstractStreamOperator; import org.apache.flink.streaming.api.operators.Output; import org.apache.flink.streaming.api.operators.StreamSource; +import org.apache.flink.streaming.runtime.streamrecord.RecordAttributes; import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; import org.apache.flink.streaming.runtime.tasks.OperatorChain; import org.apache.flink.streaming.runtime.tasks.RegularOperatorChain; @@ -50,5 +51,11 @@ public static > void run( public interface OutputWrapper extends Output { @Override default void emitWatermarkStatus(WatermarkStatus watermarkStatus) {} + + /** In Flink 1.19 the {@code recordAttributes} method was added. */ + @Override + default void emitRecordAttributes(RecordAttributes recordAttributes) { + throw new UnsupportedOperationException("emitRecordAttributes not implemented"); + } } } diff --git a/sdks/go/examples/wasm/README.md b/sdks/go/examples/wasm/README.md index 30fd22f624be..deb2e9196f41 100644 --- a/sdks/go/examples/wasm/README.md +++ b/sdks/go/examples/wasm/README.md @@ -68,7 +68,7 @@ cd $BEAM_HOME Expected output should include the following, from which you acquire the latest flink runner version. ```shell -'flink_versions: 1.17,1.18,1.19,1.20,2.0,2.1' +'flink_versions: 1.19,1.20,2.0,2.1' ``` #### 2. Set to the latest flink runner version i.e. 2.1 diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index 978a79bf6172..8bd220776312 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -2106,7 +2106,7 @@ def _add_argparse_args(cls, parser): class FlinkRunnerOptions(PipelineOptions): # These should stay in sync with gradle.properties. - PUBLISHED_FLINK_VERSIONS = ['1.17', '1.18', '1.19', '1.20', '2.0', '2.1'] + PUBLISHED_FLINK_VERSIONS = ['1.19', '1.20', '2.0', '2.1'] @classmethod def _add_argparse_args(cls, parser): diff --git a/sdks/typescript/src/apache_beam/runners/flink.ts b/sdks/typescript/src/apache_beam/runners/flink.ts index c8f8f57eb080..bb5137b8f9b8 100644 --- a/sdks/typescript/src/apache_beam/runners/flink.ts +++ b/sdks/typescript/src/apache_beam/runners/flink.ts @@ -28,7 +28,7 @@ import { JavaJarService } from "../utils/service"; const MAGIC_HOST_NAMES = ["[local]", "[auto]"]; // These should stay in sync with gradle.properties. -const PUBLISHED_FLINK_VERSIONS = ["1.17", "1.18", "1.19", "1.20", "2.0", "2.1"]; +const PUBLISHED_FLINK_VERSIONS = ["1.19", "1.20", "2.0", "2.1"]; const defaultOptions = { flinkMaster: "[local]", From 04b493f88a79d0c629fab1367bc0897862fbb19b Mon Sep 17 00:00:00 2001 From: scwhittle Date: Thu, 18 Jun 2026 16:22:44 +0200 Subject: [PATCH 421/490] cancel user code with interruption after timeout (#39018) --- .../src/main/java/org/apache/beam/io/requestresponse/Call.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java b/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java index b318cac17379..616a178d1c39 100644 --- a/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java +++ b/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java @@ -535,6 +535,7 @@ public ResponseT call(RequestT request) throws UserCodeExecutionException { try { return future.get(timeout.getMillis(), TimeUnit.MILLISECONDS); } catch (TimeoutException | InterruptedException e) { + future.cancel(true); throw new UserCodeTimeoutException(e); } catch (ExecutionException e) { parseAndThrow(future, e); From ced44f1c218366b628952241ccbbf6340178eee7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 18 Jun 2026 18:24:06 +0200 Subject: [PATCH 422/490] Fix Milvus ML PreCommit xdist, container startup (#39007) --- .github/trigger_files/beam_PreCommit_Python_ML.json | 2 +- .../apache_beam/ml/rag/enrichment/milvus_search_it_test.py | 1 + .../apache_beam/ml/rag/ingestion/milvus_search_it_test.py | 1 + sdks/python/apache_beam/ml/rag/test_utils.py | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/trigger_files/beam_PreCommit_Python_ML.json b/.github/trigger_files/beam_PreCommit_Python_ML.json index 8ed972c9f579..86bf1193abd9 100644 --- a/.github/trigger_files/beam_PreCommit_Python_ML.json +++ b/.github/trigger_files/beam_PreCommit_Python_ML.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 3 + "revision": 7 } diff --git a/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py b/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py index f37fc4931487..61d94a69e00f 100644 --- a/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py +++ b/sdks/python/apache_beam/ml/rag/enrichment/milvus_search_it_test.py @@ -225,6 +225,7 @@ def __getitem__(self, key): @pytest.mark.require_docker_in_docker +@pytest.mark.no_xdist @unittest.skipUnless( platform.system() == "Linux", "Test runs only on Linux due to lack of support, as yet, for nested " diff --git a/sdks/python/apache_beam/ml/rag/ingestion/milvus_search_it_test.py b/sdks/python/apache_beam/ml/rag/ingestion/milvus_search_it_test.py index eec1ee678fab..21b7bb67c9a9 100644 --- a/sdks/python/apache_beam/ml/rag/ingestion/milvus_search_it_test.py +++ b/sdks/python/apache_beam/ml/rag/ingestion/milvus_search_it_test.py @@ -162,6 +162,7 @@ def drop_collection(client: MilvusClient, collection_name: str): @pytest.mark.require_docker_in_docker +@pytest.mark.no_xdist @unittest.skipIf(not PYMILVUS_AVAILABLE, 'pymilvus is not installed.') @unittest.skipUnless( platform.system() == "Linux", diff --git a/sdks/python/apache_beam/ml/rag/test_utils.py b/sdks/python/apache_beam/ml/rag/test_utils.py index 72f7bde5d80e..b3628b01b2dc 100644 --- a/sdks/python/apache_beam/ml/rag/test_utils.py +++ b/sdks/python/apache_beam/ml/rag/test_utils.py @@ -113,6 +113,7 @@ def __init__( # pylint: disable=bad-super-call self.with_bind_ports(service_container_port, service_host_port) self.with_bind_ports(healthcheck_container_port, healthcheck_host_port) self.cmd = "milvus run standalone" + self.with_command(self.cmd) # Set environment variables needed for Milvus. envs = { From 1c45c4bf351f51d505fd76a5c0ecabf9ccb8aa38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:30:42 -0400 Subject: [PATCH 423/490] Bump nodemailer from 8.0.5 to 9.0.1 in /scripts/ci/issue-report (#39019) Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.5 to 9.0.1. - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.5...v9.0.1) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 9.0.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/ci/issue-report/package-lock.json | 14 +++++++------- scripts/ci/issue-report/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/ci/issue-report/package-lock.json b/scripts/ci/issue-report/package-lock.json index 5d7b321e01b0..6500cfd6dc66 100644 --- a/scripts/ci/issue-report/package-lock.json +++ b/scripts/ci/issue-report/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@octokit/rest": "^21.1.1", "node-fetch": "^2.6.1", - "nodemailer": "^8.0.5" + "nodemailer": "^9.0.1" } }, "node_modules/@octokit/auth-token": { @@ -207,9 +207,9 @@ } }, "node_modules/nodemailer": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", - "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", + "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", "engines": { "node": ">=6.0.0" } @@ -367,9 +367,9 @@ } }, "nodemailer": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", - "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", + "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==" }, "tr46": { "version": "0.0.3", diff --git a/scripts/ci/issue-report/package.json b/scripts/ci/issue-report/package.json index 53a61e3a1d15..7d3bacd407c9 100644 --- a/scripts/ci/issue-report/package.json +++ b/scripts/ci/issue-report/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@octokit/rest": "^21.1.1", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.1", "node-fetch": "^2.6.1" }, "type": "module" From 103cb189dcc1ff2f3cff0f498690ece17975d9f8 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:08:05 -0400 Subject: [PATCH 424/490] remove shardedKey (#39024) --- .../trigger_files/IO_Iceberg_Integration_Tests.json | 2 +- .../java/org/apache/beam/sdk/io/iceberg/AddFiles.java | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index b73af5e61a43..7ab7bcd9a9c6 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 2 } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java index dc8106321db7..010d7f558a12 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java @@ -65,7 +65,6 @@ import org.apache.beam.sdk.transforms.WithKeys; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; -import org.apache.beam.sdk.util.ShardedKey; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PCollectionRowTuple; @@ -226,8 +225,8 @@ public PCollectionRowTuple expand(PCollection input) { batchManifestFiles.withMaxBufferingDuration(checkStateNotNull(intervalTrigger)); } - PCollection, Iterable>> groupedFiles = - keyedFiles.apply("GroupDataFilesIntoBatches", batchDataFiles.withShardedKey()); + PCollection>> groupedFiles = + keyedFiles.apply("GroupDataFilesIntoBatches", batchDataFiles); PCollection> manifests = groupedFiles.apply( @@ -660,7 +659,7 @@ static String getPartitionFromMetrics(Metrics metrics, InputFile inputFile, Tabl * downstream {@link CommitManifestFilesDoFn}. */ static class CreateManifests - extends DoFn, Iterable>, KV> { + extends DoFn>, KV> { private final IcebergCatalogConfig catalogConfig; private final String identifier; private transient @MonotonicNonNull Table table; @@ -672,7 +671,7 @@ public CreateManifests(IcebergCatalogConfig catalogConfig, String identifier) { @ProcessElement public void process( - @Element KV, Iterable> batch, + @Element KV> batch, OutputReceiver> output) throws IOException { if (!batch.getValue().iterator().hasNext()) { @@ -682,7 +681,7 @@ public void process( table = catalogConfig.catalog().loadTable(TableIdentifier.parse(identifier)); } - PartitionSpec spec = checkStateNotNull(table.specs().get(batch.getKey().getKey())); + PartitionSpec spec = checkStateNotNull(table.specs().get(batch.getKey())); String manifestPath = String.format( From a56255cb811f006257d7d58e9e46f06a50d54043 Mon Sep 17 00:00:00 2001 From: Tarun Annapareddy Date: Thu, 18 Jun 2026 11:32:28 -0700 Subject: [PATCH 425/490] Validate Project of staging bucket (#39008) --- sdks/python/apache_beam/io/gcp/gcsio.py | 50 ++++++++++- sdks/python/apache_beam/io/gcp/gcsio_test.py | 92 ++++++++++++++++++++ sdks/python/setup.py | 1 + 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/sdks/python/apache_beam/io/gcp/gcsio.py b/sdks/python/apache_beam/io/gcp/gcsio.py index da5c20aa0e70..7cc68fd1e4a2 100644 --- a/sdks/python/apache_beam/io/gcp/gcsio.py +++ b/sdks/python/apache_beam/io/gcp/gcsio.py @@ -77,6 +77,48 @@ def default_gcs_bucket_name(project, region): region, md5(project.encode('utf8')).hexdigest()) +def _get_project_number(project_id, credentials=None): + """Resolves a project ID to its project number using Cloud Resource Manager API.""" + from google.cloud import resourcemanager_v3 + client = resourcemanager_v3.ProjectsClient(credentials=credentials) + project_info = client.get_project(name=f"projects/{project_id}") + # project_info.name is of the form "projects/PROJECT_NUMBER" + return int(project_info.name.split('/')[-1]) + + +def _validate_bucket_project(bucket, project_id, credentials=None): + """Verifies that the GCS bucket is owned by the executing project.""" + bucket_project_number = bucket.project_number + + # Skip validation if the bucket project number is a mock object + if (type(bucket_project_number).__name__.endswith('Mock') or + hasattr(bucket_project_number, '_mock_self')): + _LOGGER.warning( + 'Bucket project number is a mock object. Skipping ownership validation.' + ) + return + + if bucket_project_number is None: + _LOGGER.warning( + 'Bucket gs://%s does not have a project number. Skipping ownership validation.', + bucket.name) + return + + try: + project_number = _get_project_number(project_id, credentials=credentials) + except Exception as e: + _LOGGER.warning( + 'Failed to resolve project number for project %s: %s. ' + 'Skipping bucket ownership validation.', + project_id, + e) + return + + if bucket_project_number != project_number: + raise ValueError( + f'Bucket gs://{bucket.name} is not owned by project {project_id}.') + + def get_or_create_default_gcs_bucket(options): """Create a default GCS bucket for this project.""" if getattr(options, 'dataflow_kms_key', None): @@ -90,16 +132,18 @@ def get_or_create_default_gcs_bucket(options): return None bucket_name = default_gcs_bucket_name(project, region) - bucket = GcsIO(pipeline_options=options).get_bucket(bucket_name) + gcs = GcsIO(pipeline_options=options) + bucket = gcs.get_bucket(bucket_name) if bucket: + _validate_bucket_project( + bucket, project, credentials=getattr(gcs.client, '_credentials', None)) return bucket else: _LOGGER.warning( 'Creating default GCS bucket for project %s: gs://%s', project, bucket_name) - return GcsIO(pipeline_options=options).create_bucket( - bucket_name, project, location=region) + return gcs.create_bucket(bucket_name, project, location=region) def create_storage_client(pipeline_options, use_credentials=True): diff --git a/sdks/python/apache_beam/io/gcp/gcsio_test.py b/sdks/python/apache_beam/io/gcp/gcsio_test.py index ec4ccbf1cf57..9c4414175e48 100644 --- a/sdks/python/apache_beam/io/gcp/gcsio_test.py +++ b/sdks/python/apache_beam/io/gcp/gcsio_test.py @@ -395,6 +395,98 @@ def test_default_bucket_name_failure(self): DEFAULT_GCP_PROJECT, "us-central1", kms_key="kmskey!")), None) + @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient') + @mock.patch('apache_beam.io.gcp.gcsio.GcsIO') + def test_get_or_create_default_gcs_bucket_ownership_match( + self, mock_gcsio_class, mock_crm_class): + mock_gcsio = mock_gcsio_class.return_value + mock_bucket = mock.Mock() + mock_bucket.project_number = 123456789 + mock_gcsio.get_bucket.return_value = mock_bucket + + mock_crm_client = mock_crm_class.return_value + mock_project_info = mock.Mock() + mock_project_info.name = 'projects/123456789' + mock_crm_client.get_project.return_value = mock_project_info + + options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1') + bucket = gcsio.get_or_create_default_gcs_bucket(options) + + self.assertEqual(bucket, mock_bucket) + mock_gcsio.get_bucket.assert_called_once_with( + 'dataflow-staging-us-central1-77b801c0838aee13391c0d1885860494') + + @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient') + @mock.patch('apache_beam.io.gcp.gcsio.GcsIO') + def test_get_or_create_default_gcs_bucket_ownership_mismatch( + self, mock_gcsio_class, mock_crm_class): + mock_gcsio = mock_gcsio_class.return_value + mock_bucket = mock.Mock() + mock_bucket.project_number = 999999999 + mock_gcsio.get_bucket.return_value = mock_bucket + + mock_crm_client = mock_crm_class.return_value + mock_project_info = mock.Mock() + mock_project_info.name = 'projects/123456789' + mock_crm_client.get_project.return_value = mock_project_info + + options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1') + with self.assertRaises(ValueError) as ctx: + gcsio.get_or_create_default_gcs_bucket(options) + + self.assertIn( + f'is not owned by project {DEFAULT_GCP_PROJECT}', str(ctx.exception)) + + @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient') + @mock.patch('apache_beam.io.gcp.gcsio.GcsIO') + def test_get_or_create_default_gcs_bucket_ownership_no_bucket_number( + self, mock_gcsio_class, mock_crm_class): + mock_gcsio = mock_gcsio_class.return_value + mock_bucket = mock.Mock() + mock_bucket.project_number = None + mock_bucket.name = 'dataflow-staging-us-central1-77b801c0838aee13391c0d1885860494' + mock_gcsio.get_bucket.return_value = mock_bucket + + options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1') + bucket = gcsio.get_or_create_default_gcs_bucket(options) + + self.assertEqual(bucket, mock_bucket) + mock_crm_class.assert_not_called() + + @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient') + @mock.patch('apache_beam.io.gcp.gcsio.GcsIO') + def test_get_or_create_default_gcs_bucket_ownership_crm_error( + self, mock_gcsio_class, mock_crm_class): + mock_gcsio = mock_gcsio_class.return_value + mock_bucket = mock.Mock() + mock_bucket.project_number = 123456789 + mock_gcsio.get_bucket.return_value = mock_bucket + + mock_crm_client = mock_crm_class.return_value + mock_crm_client.get_project.side_effect = Exception("API Disabled") + + options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1') + bucket = gcsio.get_or_create_default_gcs_bucket(options) + + # Should fall back to success (warn only) + self.assertEqual(bucket, mock_bucket) + + @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient') + @mock.patch('apache_beam.io.gcp.gcsio.GcsIO') + def test_get_or_create_default_gcs_bucket_ownership_mock_project_number( + self, mock_gcsio_class, mock_crm_class): + mock_gcsio = mock_gcsio_class.return_value + # Creating a mock bucket without setting project_number (it returns another mock object) + mock_bucket = mock.Mock() + mock_gcsio.get_bucket.return_value = mock_bucket + + options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1') + bucket = gcsio.get_or_create_default_gcs_bucket(options) + + # Verification should be skipped, returning the mock bucket and never calling CRM + self.assertEqual(bucket, mock_bucket) + mock_crm_class.assert_not_called() + def test_exists(self): file_name = 'gs://gcsio-test/dummy_file' file_size = 1234 diff --git a/sdks/python/setup.py b/sdks/python/setup.py index 027b3f65039c..c3543995a85b 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -531,6 +531,7 @@ def get_portability_package_data(): 'google-cloud-datastore>=2.0.0,<3', 'google-cloud-pubsub>=2.1.0,<3', 'google-cloud-storage>=2.18.2,<4', + 'google-cloud-resource-manager>=1.12.0,<2', 'google-cloud-dataflow-client>=0.13.0,<0.14.0', # GCP packages required by tests 'google-cloud-bigquery>=2.0.0,<4', From 1181e0c0f45e7cd24134465c10ce7e8c2ae942ba Mon Sep 17 00:00:00 2001 From: parveensania Date: Fri, 19 Jun 2026 00:42:28 -0700 Subject: [PATCH 426/490] Sanitize logging in FanOutStreamingEngineWorkerHarness (#39029) --- .../harness/FanOutStreamingEngineWorkerHarness.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/FanOutStreamingEngineWorkerHarness.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/FanOutStreamingEngineWorkerHarness.java index 90a185973b84..f3262c17b698 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/FanOutStreamingEngineWorkerHarness.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/FanOutStreamingEngineWorkerHarness.java @@ -303,10 +303,13 @@ private synchronized void consumeWindmillWorkerEndpoints(WindmillEndpoints newWi } LOG.info( - "Consuming new endpoints: {}. previous metadata version: {}, current metadata version: {}, previous endpoint type: {}, current endpoint type: {}", - newWindmillEndpoints, + "Consuming new endpoints. previous metadata version: {}, current metadata version: {}, " + + "windmill endpoint count: {}, global data endpoint count: {}, " + + "previous endpoint type: {}, current endpoint type: {}", activeMetadataVersion, newWindmillEndpoints.version(), + newWindmillEndpoints.windmillEndpoints().size(), + newWindmillEndpoints.globalDataEndpoints().size(), activeMetadataType, newWindmillEndpoints.type()); closeStreamsNotIn(newWindmillEndpoints).join(); From 7aa0722933924b212cfe95bafefec11a4aaaa43f Mon Sep 17 00:00:00 2001 From: scwhittle Date: Fri, 19 Jun 2026 11:29:33 +0200 Subject: [PATCH 427/490] Increase CallTest sleep durations to keep test from being flaky due to timeout not being observed. (#39016) Also decrease configured timeouts from 1 minute to 1 second to speed up test execution. --- .../beam/io/requestresponse/CallTest.java | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java b/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java index 23ced30ada55..0764ab8db405 100644 --- a/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java +++ b/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java @@ -123,7 +123,7 @@ public void givenCallerThrowsQuotaException_emitsIntoFailurePCollection() { @Test public void givenCallerTimeout_emitsFailurePCollection() { - Duration timeout = Duration.standardMinutes(1L); + Duration timeout = Duration.standardSeconds(1L); Result result = pipeline .apply(Create.of(new Request("a"))) @@ -182,8 +182,7 @@ public void givenSetupThrowsQuotaException_throwsError() { @Test public void givenSetupTimeout_throwsError() { - Duration timeout = Duration.standardMinutes(1L); - + Duration timeout = Duration.standardSeconds(1L); pipeline .apply(Create.of(new Request(""))) .apply( @@ -231,7 +230,7 @@ public void givenTeardownThrowsQuotaException_throwsError() { @Test public void givenTeardownTimeout_throwsError() { - Duration timeout = Duration.standardMinutes(1L); + Duration timeout = Duration.standardSeconds(1L); pipeline .apply(Create.of(new Request(""))) .apply( @@ -271,7 +270,7 @@ public void givenValidCaller_emitValidResponse() { private static class ValidCaller implements Caller { @Override - public Response call(Request request) throws UserCodeExecutionException { + public Response call(Request request) { return new Response(request.id); } } @@ -282,7 +281,7 @@ private static class UnSerializableCaller implements Caller { private final UnSerializable nestedThing = new UnSerializable(); @Override - public Response call(Request request) throws UserCodeExecutionException { + public Response call(Request request) { return new Response(request.id); } } @@ -291,10 +290,10 @@ private static class UnSerializableCallerWithSetupTeardown extends UnSerializabl implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class UnSerializable {} @@ -358,11 +357,11 @@ private static class CallerExceedsTimeout implements Caller { private final Duration timeout; CallerExceedsTimeout(Duration timeout) { - this.timeout = timeout.plus(Duration.standardSeconds(1L)); + this.timeout = timeout.plus(Duration.standardSeconds(10L)); } @Override - public Response call(Request request) throws UserCodeExecutionException { + public Response call(Request request) { sleep(timeout); return new Response(request.id); } @@ -397,16 +396,16 @@ private static class SetupExceedsTimeout implements SetupTeardown { private final Duration timeout; private SetupExceedsTimeout(Duration timeout) { - this.timeout = timeout.plus(Duration.standardSeconds(1L)); + this.timeout = timeout.plus(Duration.standardSeconds(10L)); } @Override - public void setup() throws UserCodeExecutionException { + public void setup() { sleep(timeout); } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeExecutionException implements SetupTeardown { @@ -416,7 +415,7 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeQuotaException implements SetupTeardown { @@ -426,7 +425,7 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeTimeoutException implements SetupTeardown { @@ -436,28 +435,28 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class TeardownExceedsTimeout implements SetupTeardown { private final Duration timeout; private TeardownExceedsTimeout(Duration timeout) { - this.timeout = timeout.plus(Duration.standardSeconds(1L)); + this.timeout = timeout.plus(Duration.standardSeconds(10L)); } @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override - public void teardown() throws UserCodeExecutionException { + public void teardown() { sleep(timeout); } } private static class TeardownThrowsUserCodeExecutionException implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override public void teardown() throws UserCodeExecutionException { @@ -467,7 +466,7 @@ public void teardown() throws UserCodeExecutionException { private static class TeardownThrowsUserCodeQuotaException implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override public void teardown() throws UserCodeExecutionException { @@ -477,7 +476,7 @@ public void teardown() throws UserCodeExecutionException { private static class TeardownThrowsUserCodeTimeoutException implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override public void teardown() throws UserCodeExecutionException { @@ -519,14 +518,12 @@ private static class DeterministicRequestCoder extends CustomCoder<@NonNull Requ private static final Coder ID_CODER = StringUtf8Coder.of(); @Override - public void encode(Request value, @NotNull OutputStream outStream) - throws CoderException, IOException { + public void encode(Request value, @NotNull OutputStream outStream) throws IOException { ID_CODER.encode(checkStateNotNull(value).id, outStream); } @Override - public @NonNull Request decode(@NotNull InputStream inStream) - throws CoderException, IOException { + public @NonNull Request decode(@NotNull InputStream inStream) throws IOException { String id = ID_CODER.decode(inStream); return new Request(id); } @@ -542,7 +539,7 @@ private static class DeterministicResponseCoder extends CustomCoder { @Override public void encode(@Nullable Response value, @NotNull OutputStream outStream) - throws CoderException, IOException { + throws IOException { if (value == null) { ID_CODER.encode(null, outStream); return; @@ -551,7 +548,7 @@ public void encode(@Nullable Response value, @NotNull OutputStream outStream) } @Override - public Response decode(@NotNull InputStream inStream) throws CoderException, IOException { + public Response decode(@NotNull InputStream inStream) throws IOException { try { String id = ID_CODER.decode(inStream); return new Response(id); From 17384c57f3c72101e008f1aaaaf49084c9363315 Mon Sep 17 00:00:00 2001 From: scwhittle Date: Fri, 19 Jun 2026 11:30:18 +0200 Subject: [PATCH 428/490] [Dataflow Java] Fix incorrect warning log when specifying logger override. (#39034) --- .../DataflowWorkerLoggingInitializer.java | 12 +++-- .../DataflowWorkerLoggingInitializerTest.java | 44 ++++++++++++++++++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java index 0f1b5c2750bc..d854ae74ebaf 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java @@ -217,17 +217,16 @@ private static void applyOverridesToLogger( boolean directLoggingEnabled) { if (!directLoggingEnabled) { logger.setLevel(overrides.disk); - if (overrides.disk.intValue() != Level.OFF.intValue()) { + if (overrides.direct.intValue() != Level.OFF.intValue()) { LOG.warn( - "Ignoring the disk logging level override for {} because --defaultWorkerDirectLoggerLevel was OFF.", + "Ignoring the direct logging level override for {} because --defaultWorkerDirectLoggerLevel was OFF.", logger.getName()); } return; } // Configure the logger to accept logs of the lower value. The log will be sent directly based - // upon the default - // nonDirectLogLevel or the provided hint. + // upon the default nonDirectLogLevel or the provided hint. if (overrides.direct.intValue() < overrides.disk.intValue()) { logger.setLevel(overrides.direct); if (overrides.disk.intValue() != defaultNonDirectLogLevel.intValue()) { @@ -245,9 +244,8 @@ private static void applyOverridesToLogger( logger.setLevel(overrides.disk); if (overrides.disk.intValue() < defaultNonDirectLogLevel.intValue()) { // We need to provide the hint as otherwise the default would be used and send the log - // directly which is not - // desired. We don't bother if the threshold was increased as the right decision would still - // be made. + // directly which is not desired. We don't bother if the threshold was increased as the + // right decision would still be made. logger.setResourceBundle( DataflowWorkerLoggingHandler.resourceBundleForNonDirectLogLevelHint(overrides.disk)); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializerTest.java index 7537c17a17bb..7569590b63f0 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializerTest.java @@ -134,7 +134,7 @@ public void testWithSdkHarnessConfigurationOverride() { } @Test - public void testWithWorkerCustomLogLevels() { + public void testWithWorkerCustomLogLevels() throws IOException { DataflowWorkerLoggingOptions options = PipelineOptionsFactory.as(DataflowWorkerLoggingOptions.class); options.setWorkerLogLevelOverrides( @@ -153,6 +153,9 @@ public void testWithWorkerCustomLogLevels() { assertEquals(Level.SEVERE, bLogger.getLevel()); assertEquals(0, bLogger.getHandlers().length); assertTrue(aLogger.getUseParentHandlers()); + + List logLines = retrieveLogLines(); + assertThat(logLines, not(hasItem(containsString("Ignoring the direct logging level")))); } @Test @@ -176,7 +179,7 @@ public void testWithDirectLogging() { } @Test - public void testWithSdkHarnessCustomLogLevels() { + public void testWithSdkHarnessCustomLogLevels() throws IOException { SdkHarnessOptions options = PipelineOptionsFactory.as(SdkHarnessOptions.class); options.setSdkHarnessLogLevelOverrides( new SdkHarnessLogLevelOverrides() @@ -194,6 +197,43 @@ public void testWithSdkHarnessCustomLogLevels() { assertEquals(Level.SEVERE, bLogger.getLevel()); assertEquals(0, bLogger.getHandlers().length); assertTrue(aLogger.getUseParentHandlers()); + + List logLines = retrieveLogLines(); + assertThat(logLines, not(hasItem(containsString("Ignoring the direct logging level")))); + } + + @Test + public void testWithSdkHarnessCustomLogLevels_DirectSpecifiedNotEnabled() throws IOException { + SdkHarnessOptions options = PipelineOptionsFactory.as(SdkHarnessOptions.class); + options.setSdkHarnessLogLevelOverrides( + new SdkHarnessLogLevelOverrides() + .addOverrideForName("B", SdkHarnessOptions.LogLevel.DEBUG) + .addOverrideForName("C", SdkHarnessOptions.LogLevel.ERROR)); + DataflowWorkerLoggingOptions loggingOptions = options.as(DataflowWorkerLoggingOptions.class); + loggingOptions.setWorkerDirectLogLevelOverrides( + new WorkerLogLevelOverrides() + .addOverrideForName("A", DataflowWorkerLoggingOptions.Level.TRACE) + .addOverrideForName("C", DataflowWorkerLoggingOptions.Level.TRACE)); + + DataflowWorkerLoggingInitializer.configure(options.as(DataflowWorkerLoggingOptions.class)); + + Logger aLogger = LogManager.getLogManager().getLogger("A"); + assertEquals(0, aLogger.getHandlers().length); + assertEquals(Level.INFO, aLogger.getLevel()); + assertTrue(aLogger.getUseParentHandlers()); + + Logger bLogger = LogManager.getLogManager().getLogger("B"); + assertEquals(Level.FINE, bLogger.getLevel()); + assertEquals(0, bLogger.getHandlers().length); + assertTrue(aLogger.getUseParentHandlers()); + + Logger cLogger = LogManager.getLogManager().getLogger("C"); + assertEquals(Level.SEVERE, cLogger.getLevel()); + assertEquals(0, cLogger.getHandlers().length); + assertTrue(aLogger.getUseParentHandlers()); + + List logLines = retrieveLogLines(); + assertThat(logLines, hasItem(containsString("Ignoring the direct logging level"))); } @Test From 284d6a0c76139887042fa703b27c2f47137f9d8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:25:07 -0400 Subject: [PATCH 429/490] Bump github.com/moby/moby/api from 1.54.2 to 1.55.0 in /sdks (#39031) Bumps [github.com/moby/moby/api](https://github.com/moby/moby) from 1.54.2 to 1.55.0. - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/api/v1.54.2...api/v1.55.0) --- updated-dependencies: - dependency-name: github.com/moby/moby/api dependency-version: 1.55.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 8bdb10311285..b6c261644d06 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -72,7 +72,7 @@ require ( github.com/avast/retry-go/v4 v4.7.0 github.com/fsouza/fake-gcs-server v1.52.3 github.com/golang-cz/devslog v0.0.16 - github.com/moby/moby/api v1.54.2 + github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.4.1 golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a ) diff --git a/sdks/go.sum b/sdks/go.sum index e8238c1c2264..6cae761de1a6 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -687,8 +687,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= -github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= From c751eee8c5e9db6414203b3dacf6deacf0dee40c Mon Sep 17 00:00:00 2001 From: scwhittle Date: Fri, 19 Jun 2026 16:02:30 +0200 Subject: [PATCH 430/490] [FnApi Java] Add support for separate named data streams to provide bundle isolation (#38863) * [FnApi Java] Add support for separate named data streams to provide bundle isolation. This is advertised to the runner via a new NAMED_DATA_STREAMS protocol capability. The runner is then free to assign bundles to named data streams as it chooses to isolate bundle processing from each other. Instead of single data stream from the sdk, the sdk will create a data stream for each name. The benefit of doing so is that the multiplexing currently performed on data stream messages being received allows a slow bundle to fill up buffers and block the shared stream. With separate named streams, bundles on other data streams have separate grpc flow control from the blocked stream and are not affected. --- .../beam_PostCommit_Java_DataflowV2.json | 2 +- ...mmit_Java_ValidatesRunner_Dataflow_V2.json | 2 +- ...PostCommit_Java_ValidatesRunner_Spark.json | 18 +-- CHANGES.md | 1 + .../model/fn_execution/v1/beam_fn_api.proto | 38 ++++- .../model/pipeline/v1/beam_runner_api.proto | 4 + .../fnexecution/control/SdkHarnessClient.java | 2 +- .../fnexecution/data/FnDataService.java | 3 +- .../fnexecution/data/GrpcDataService.java | 13 +- .../fnexecution/data/GrpcDataServiceTest.java | 2 +- .../fn/data/BeamFnDataGrpcMultiplexer.java | 2 +- .../fn/data/BeamFnDataOutboundAggregator.java | 131 ++++++++++++----- .../sdk/util/construction/Environments.java | 1 + .../BeamFnDataOutboundAggregatorTest.java | 135 ++++++++++-------- .../org/apache/beam/fn/harness/FnHarness.java | 49 ++++--- .../harness/control/ProcessBundleHandler.java | 50 ++++--- .../fn/harness/data/BeamFnDataClient.java | 28 +--- .../fn/harness/data/BeamFnDataGrpcClient.java | 91 ++++++++---- .../fn/harness/BeamFnDataWriteRunnerTest.java | 44 +++--- .../beam/fn/harness/FnApiDoFnRunnerTest.java | 2 +- .../PTransformRunnerFactoryTestContext.java | 12 +- .../control/ProcessBundleHandlerTest.java | 47 +++--- .../data/BeamFnDataGrpcClientTest.java | 41 +++--- 23 files changed, 417 insertions(+), 301 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json b/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json index 73012c45df18..d0d502569834 100644 --- a/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json +++ b/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json @@ -1,4 +1,4 @@ { - "modification": 6, + "modification": 7, "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" } diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json index 0726e6343b31..22f6c3813b60 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 4 + "modification": 5 } diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json index f4e3f8b8dbe2..1efc8e9e4405 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json @@ -1,18 +1,4 @@ { - "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", - "https://github.com/apache/beam/pull/35213": "Eliminating getPane() in favor of getPaneInfo()", - "https://github.com/apache/beam/pull/35177": "Introducing WindowedValueReceiver to runners", - "comment": "Modify this file in a trivial way to cause this test suite to run", - "https://github.com/apache/beam/pull/31156": "noting that PR #31156 should run this test", - "https://github.com/apache/beam/pull/31798": "noting that PR #31798 should run this test", - "https://github.com/apache/beam/pull/32546": "noting that PR #32546 should run this test", - "https://github.com/apache/beam/pull/33267": "noting that PR #33267 should run this test", - "https://github.com/apache/beam/pull/33322": "noting that PR #33322 should run this test", - "https://github.com/apache/beam/pull/34123": "noting that PR #34123 should run this test", - "https://github.com/apache/beam/pull/34080": "noting that PR #34080 should run this test", - "https://github.com/apache/beam/pull/34155": "noting that PR #34155 should run this test", - "https://github.com/apache/beam/pull/34560": "noting that PR #34560 should run this test", - "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface", - "https://github.com/apache/beam/pull/35316": "noting that PR #35316 should run this test", - "https://github.com/apache/beam/pull/38011": "noting that PR #38011 should run this test" + "comment": "Modify this file in a trivial way to cause this test suite to run", + "modification": 1 } diff --git a/CHANGES.md b/CHANGES.md index eb24a71d0bfa..0139b8bbd3b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,7 @@ * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). * (Python) Added instrumentation to support off-the-shelf profiling agents when launching Python SDK Harness ([#38853](https://github.com/apache/beam/issues/38853)). +* (Java) Added support to the FnApi Data stream protocol allowing runners to isolate bundles slowly processing input from other bundles. ([#39001](https://github.com/apache/beam/issues/39001)). ## Breaking Changes diff --git a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto index ecef3f2e7a94..80a0fa6f7f28 100644 --- a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto +++ b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto @@ -120,6 +120,9 @@ message RemoteGrpcPort { service BeamFnControl { // Instructions sent by the runner to the SDK requesting different types // of work. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc Control( // A stream of responses to instructions the SDK was asked to be // performed. @@ -130,6 +133,9 @@ service BeamFnControl { // Used to get the full process bundle descriptors for bundles one // is asked to process. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc GetProcessBundleDescriptor(GetProcessBundleDescriptorRequest) returns ( ProcessBundleDescriptor) {} } @@ -416,14 +422,22 @@ message ProcessBundleRequest { // at https://s.apache.org/beam-fn-api-control-data-embedding. Elements elements = 3; - // indicates that the runner has no stare for the keys in this bundle + // Indicates that the runner has no state for the keys in this bundle // so SDk can safely begin stateful processing with a locally-generated - // initial empty state + // initial empty state. bool has_no_state = 4; - // indicates that the runner will never process another bundle for the keys + // Indicates that the runner will never process another bundle for the keys // in this bundle so state need not be included in the bundle commit. bool only_bundle_for_keys = 5; + + // (Optional) If non-empty, the ID of the data stream to use for all data + // requests related to this bundle. See comments at BeamFnData.Data for + // more details. + // + // The runner should only populate this field if the sdk advertises the + // beam:protocol:named_data_streams:v1 capability. + string data_stream_id = 6; } message ProcessBundleResponse { @@ -834,7 +848,15 @@ message Elements { // Stable service BeamFnData { - // Used to send data between harnesses. + // Used to send data between harnesses. Sdks default to using an unnamed data stream + // (without "data_stream_id" header value) for bundles unless the runner requests another named stream to be + // used for a bundle. SDKs can advertise that they support named data streams with the capability + // `beam:protocol:named_data_streams:v1`. + // + // Header metadata has the specified keys pairs: + // - "worker_id": value is the id of the sdk + // - "data_stream_id": value is the id of the data stream, distinguishing it from other data streams from the same + // sdk. This field should only be populated if requested in a received ProcessBundleRequest from the runner. rpc Data( // A stream of data representing input. stream Elements) @@ -900,6 +922,9 @@ message StateResponse { service BeamFnState { // Used to get/append/clear state stored by the runner on behalf of the SDK. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc State( // A stream of state instructions requested of the runner. stream StateRequest) @@ -1295,6 +1320,9 @@ message LogControl {} service BeamFnLogging { // Allows for the SDK to emit log entries which the runner can // associate with the active job. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc Logging( // A stream of log entries batched into lists emitted by the SDK harness. stream LogEntry.List) @@ -1356,6 +1384,8 @@ message WorkerStatusResponse { // API for SDKs to report debug-related statuses to runner during pipeline execution. service BeamFnWorkerStatus { + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc WorkerStatus (stream WorkerStatusResponse) returns (stream WorkerStatusRequest) {} } diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto index 67df8b9e8003..5824c9bf4b73 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto @@ -1689,6 +1689,10 @@ message StandardProtocols { // Indicates whether the SDK supports multimap state. MULTIMAP_STATE = 12 [(beam_urn) = "beam:protocol:multimap_state:v1"]; + + // Indicates whether the SDK supports data stream ids being requested by the runner in + // ProcessBundleRequests. + NAMED_DATA_STREAMS = 13 [(beam_urn) = "beam:protocol:named_data_streams:v1"]; } } diff --git a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java index 682c45e30795..704d298a195d 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java @@ -298,7 +298,7 @@ public ActiveBundle newBundle( ImmutableMap.Builder> receiverBuilder = ImmutableMap.builder(); BeamFnDataOutboundAggregator beamFnDataOutboundAggregator = - fnApiDataService.createOutboundAggregator(() -> bundleId, false); + fnApiDataService.createOutboundAggregator(bundleId, false); for (RemoteInputDestination remoteInput : remoteInputs) { LogicalEndpoint endpoint = LogicalEndpoint.data(bundleId, remoteInput.getPTransformId()); receiverBuilder.put( diff --git a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java index 7c5f110eab28..657ec74553bc 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java @@ -17,7 +17,6 @@ */ package org.apache.beam.runners.fnexecution.data; -import java.util.function.Supplier; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator; import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver; @@ -69,5 +68,5 @@ public interface FnDataService { *

        The returned aggregator is not thread safe. */ BeamFnDataOutboundAggregator createOutboundAggregator( - Supplier processBundleRequestIdSupplier, boolean collectElementsIfNoFlushes); + String processBundleId, boolean collectElementsIfNoFlushes); } diff --git a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java index d4e45c8ccf82..a3a8c3244044 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java @@ -23,7 +23,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; import org.apache.beam.model.fnexecution.v1.BeamFnApi; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.model.fnexecution.v1.BeamFnDataGrpc; @@ -175,13 +174,13 @@ public void unregisterReceiver(String instructionId) { @Override public BeamFnDataOutboundAggregator createOutboundAggregator( - Supplier processBundleRequestIdSupplier, boolean collectElementsIfNoFlushes) { + String instructionId, boolean collectElementsIfNoFlushes) { try { - return new BeamFnDataOutboundAggregator( - options, - processBundleRequestIdSupplier, - connectedClient.get(3, TimeUnit.MINUTES).getOutboundObserver(), - collectElementsIfNoFlushes); + BeamFnDataOutboundAggregator aggregator = + new BeamFnDataOutboundAggregator(options, collectElementsIfNoFlushes); + aggregator.prepareForInstruction( + instructionId, connectedClient.get(3, TimeUnit.MINUTES).getOutboundObserver()); + return aggregator; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); diff --git a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java index f84467077501..363367f1087f 100644 --- a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java +++ b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java @@ -102,7 +102,7 @@ public void testMessageReceivedBySingleClientWhenThereAreMultipleClients() throw for (int i = 0; i < 3; ++i) { final String instructionId = Integer.toString(i); BeamFnDataOutboundAggregator aggregator = - service.createOutboundAggregator(() -> instructionId, false); + service.createOutboundAggregator(instructionId, false); aggregator.start(); FnDataReceiver> consumer = aggregator.registerOutputDataLocation(TRANSFORM_ID, CODER); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataGrpcMultiplexer.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataGrpcMultiplexer.java index 8fec8b455cce..0b9d6adab4f0 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataGrpcMultiplexer.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataGrpcMultiplexer.java @@ -63,7 +63,7 @@ public class BeamFnDataGrpcMultiplexer implements AutoCloseable { private final Cache poisonedInstructionIds; private static class PoisonedException extends RuntimeException { - public PoisonedException() { + private PoisonedException() { super("Instruction poisoned"); } }; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregator.java index 9b9603706b48..04c7e6ef96b4 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregator.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregator.java @@ -17,6 +17,9 @@ */ package org.apache.beam.sdk.fn.data; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + import java.io.IOException; import java.util.Collections; import java.util.HashMap; @@ -28,7 +31,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; @@ -53,10 +55,7 @@ *

        The default time-based buffer threshold can be overridden by specifying the experiment {@code * data_buffer_time_limit_ms=} */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -// The calling thread that invokes sendBufferedDataAndFinishOutboundStreams synchronizes on +// The calling thread that invokes sendOrCollectBufferedDataAndFinishOutboundStreams synchronizes on // flushLock effectively making the periodic flushing no longer read or mutate hasFlushedForBundle // and allowing the calling thread to read and mutate hasFlushedForBundle safely without needing to // create another memory barrier. Also note that flush is always invoked when synchronizing on @@ -72,31 +71,54 @@ public class BeamFnDataOutboundAggregator { private static final Logger LOG = LoggerFactory.getLogger(BeamFnDataOutboundAggregator.class); private final int sizeLimit; private final long timeLimit; - private final Supplier processBundleRequestIdSupplier; - @VisibleForTesting final Map> outputDataReceivers; - @VisibleForTesting final Map> outputTimersReceivers; - private final StreamObserver outboundObserver; + // The instructionId is set between prepareForInstruction and finishInstruction/discard. + private @Nullable String instructionId = null; + @VisibleForTesting final Map> outputDataReceivers = new HashMap<>(); + @VisibleForTesting final Map> outputTimersReceivers = new HashMap<>(); + @Nullable private StreamObserver outboundObserver; @Nullable @VisibleForTesting ScheduledFuture flushFuture; - private long bytesWrittenSinceFlush; - private final Object flushLock; + private long bytesWrittenSinceFlush = 0; + private final Object flushLock = new Object(); private final boolean collectElementsIfNoFlushes; - private boolean hasFlushedForBundle; + private boolean hasFlushedForBundle = false; - public BeamFnDataOutboundAggregator( - PipelineOptions options, - Supplier processBundleRequestIdSupplier, - StreamObserver outboundObserver, - boolean collectElementsIfNoFlushes) { + public BeamFnDataOutboundAggregator(PipelineOptions options, boolean collectElementsIfNoFlushes) { this.sizeLimit = getSizeLimit(options); this.timeLimit = getTimeLimit(options); this.collectElementsIfNoFlushes = collectElementsIfNoFlushes; - this.outputDataReceivers = new HashMap<>(); - this.outputTimersReceivers = new HashMap<>(); - this.outboundObserver = outboundObserver; - this.processBundleRequestIdSupplier = processBundleRequestIdSupplier; - this.bytesWrittenSinceFlush = 0L; - this.flushLock = new Object(); - this.hasFlushedForBundle = false; + } + + public void prepareForInstruction( + String instructionId, StreamObserver outboundObserver) { + if (timeLimit > 0) { + synchronized (flushLock) { + checkState(this.instructionId == null && this.outboundObserver == null); + this.instructionId = instructionId; + this.outboundObserver = outboundObserver; + } + } else { + checkState(this.instructionId == null && this.outboundObserver == null); + this.instructionId = instructionId; + this.outboundObserver = outboundObserver; + } + } + + public void finishInstruction() { + if (flushFuture != null) { + synchronized (flushLock) { + checkState( + this.instructionId != null && this.outboundObserver != null, + "instruction was not started or previously completed"); + checkState(bytesWrittenSinceFlush == 0, "bytes were not flushed for instruction"); + this.instructionId = null; + this.outboundObserver = null; + } + } else { + checkState(this.instructionId != null && this.outboundObserver != null); + checkState(bytesWrittenSinceFlush == 0, "bytes were not flushed for instruction"); + this.instructionId = null; + this.outboundObserver = null; + } } /** Starts the flushing daemon thread if data_buffer_time_limit_ms is set. */ @@ -166,7 +188,7 @@ private void flushInternal() { } Elements.Builder elements = convertBufferForTransmission(); if (elements.getDataCount() > 0 || elements.getTimersCount() > 0) { - outboundObserver.onNext(elements.build()); + checkNotNull(outboundObserver).onNext(elements.build()); } hasFlushedForBundle = true; } @@ -177,10 +199,15 @@ private void flushInternal() { * collectElementsIfNoFlushes=true, and there was no previous flush in this bundle, otherwise * returns null. */ + @Nullable public Elements sendOrCollectBufferedDataAndFinishOutboundStreams() { if (outputTimersReceivers.isEmpty() && outputDataReceivers.isEmpty()) { return null; } + String instructionId = + checkNotNull( + this.instructionId, + "This method should only be called between prepareForInstruction and finishInstruction"); Elements.Builder bufferedElements; if (timeLimit > 0) { synchronized (flushLock) { @@ -191,14 +218,14 @@ public Elements sendOrCollectBufferedDataAndFinishOutboundStreams() { } LOG.debug( "Closing streams for instruction {} and outbound data {} and timers {}.", - processBundleRequestIdSupplier.get(), + instructionId, outputDataReceivers, outputTimersReceivers); for (Map.Entry> entry : outputDataReceivers.entrySet()) { String pTransformId = entry.getKey(); bufferedElements .addDataBuilder() - .setInstructionId(processBundleRequestIdSupplier.get()) + .setInstructionId(instructionId) .setTransformId(pTransformId) .setIsLast(true); entry.getValue().resetStats(); @@ -207,35 +234,60 @@ public Elements sendOrCollectBufferedDataAndFinishOutboundStreams() { TimerEndpoint timerKey = entry.getKey(); bufferedElements .addTimersBuilder() - .setInstructionId(processBundleRequestIdSupplier.get()) + .setInstructionId(instructionId) .setTransformId(timerKey.pTransformId) .setTimerFamilyId(timerKey.timerFamilyId) .setIsLast(true); entry.getValue().resetStats(); } + // This is the end of the bundle so we reset state to prepare for future bundles. if (collectElementsIfNoFlushes && !hasFlushedForBundle) { return bufferedElements.build(); } - outboundObserver.onNext(bufferedElements.build()); - // This is now at the end of a bundle, so we reset hasFlushedForBundle to prepare for new - // bundles. + checkNotNull(outboundObserver).onNext(bufferedElements.build()); hasFlushedForBundle = false; return null; } // Send the elements to the StreamObserver associated with this aggregator. public void sendElements(Elements elements) { - outboundObserver.onNext(elements); + if (timeLimit > 0) { + synchronized (flushLock) { + checkNotNull(outboundObserver).onNext(elements); + } + } else { + checkNotNull(outboundObserver).onNext(elements); + } } + // Prepares for discarding the aggregator without preserving its output or + // preparing it for reuse. public void discard() { - if (flushFuture != null) { - flushFuture.cancel(true); + if (timeLimit > 0) { + // Short-circuit the possibly concurrently running flush. + synchronized (flushLock) { + bytesWrittenSinceFlush = 0L; + finishInstruction(); + } + if (flushFuture != null) { + flushFuture.cancel(false); + } + } else { + bytesWrittenSinceFlush = 0L; + finishInstruction(); } } private Elements.Builder convertBufferForTransmission() { Elements.Builder bufferedElements = Elements.newBuilder(); + if (bytesWrittenSinceFlush == 0) { + return bufferedElements; + } + bytesWrittenSinceFlush = 0L; + String instructionId = + checkNotNull( + this.instructionId, + "This method should only be called between prepareForInstruction and finishInstruction"); for (Map.Entry> entry : outputDataReceivers.entrySet()) { if (!entry.getValue().hasBufferedOutput()) { continue; @@ -243,7 +295,7 @@ private Elements.Builder convertBufferForTransmission() { ByteString bytes = entry.getValue().toByteStringAndResetBuffer(); bufferedElements .addDataBuilder() - .setInstructionId(processBundleRequestIdSupplier.get()) + .setInstructionId(instructionId) .setTransformId(entry.getKey()) .setData(bytes); } @@ -254,12 +306,11 @@ private Elements.Builder convertBufferForTransmission() { ByteString bytes = entry.getValue().toByteStringAndResetBuffer(); bufferedElements .addTimersBuilder() - .setInstructionId(processBundleRequestIdSupplier.get()) + .setInstructionId(instructionId) .setTransformId(entry.getKey().pTransformId) .setTimerFamilyId(entry.getKey().timerFamilyId) .setTimers(bytes); } - bytesWrittenSinceFlush = 0L; return bufferedElements; } @@ -277,7 +328,7 @@ void flush() { /** Check if the flush thread failed with an exception. */ private void checkFlushThreadException() throws IOException { - if (timeLimit > 0 && flushFuture.isDone()) { + if (flushFuture != null && flushFuture.isDone()) { try { flushFuture.get(); throw new IOException("Periodic flushing thread finished unexpectedly."); @@ -353,10 +404,12 @@ public void accept(T input) throws Exception { } } + @VisibleForTesting public long getByteCount() { return perBundleByteCount; } + @VisibleForTesting public long getElementCount() { return perBundleElementCount; } @@ -392,7 +445,7 @@ public TimerEndpoint(String pTransformId, String timerFamilyId) { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/Environments.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/Environments.java index 969bda88d07f..c3b1a7a5235e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/Environments.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/Environments.java @@ -522,6 +522,7 @@ public static Set getJavaCapabilities() { capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.SDK_CONSUMING_RECEIVED_DATA)); capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.ORDERED_LIST_STATE)); capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.MULTIMAP_STATE)); + capabilities.add(BeamUrns.getUrn(StandardProtocols.Enum.NAMED_DATA_STREAMS)); return capabilities.build(); } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregatorTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregatorTest.java index 092ba200c94b..9bcf615d638b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregatorTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregatorTest.java @@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import java.io.IOException; @@ -75,13 +76,12 @@ public void testWithDefaultBuffer() throws Exception { final List values = new ArrayList<>(); final AtomicBoolean onCompletedWasCalled = new AtomicBoolean(); BeamFnDataOutboundAggregator aggregator = - new BeamFnDataOutboundAggregator( - PipelineOptionsFactory.create(), - endpoint::getInstructionId, - TestStreams.withOnNext(values::add) - .withOnCompleted(() -> onCompletedWasCalled.set(true)) - .build(), - false); + new BeamFnDataOutboundAggregator(PipelineOptionsFactory.create(), false); + aggregator.prepareForInstruction( + endpoint.getInstructionId(), + TestStreams.withOnNext(values::add) + .withOnCompleted(() -> onCompletedWasCalled.set(true)) + .build()); // Test that nothing is emitted till the default buffer size is surpassed. FnDataReceiver dataReceiver = registerOutputLocation(aggregator, endpoint, CODER); @@ -124,14 +124,12 @@ public void testConfiguredBufferLimit() throws Exception { options .as(ExperimentalOptions.class) .setExperiments(Arrays.asList("data_buffer_size_limit=100")); - BeamFnDataOutboundAggregator aggregator = - new BeamFnDataOutboundAggregator( - options, - endpoint::getInstructionId, - TestStreams.withOnNext(values::add) - .withOnCompleted(() -> onCompletedWasCalled.set(true)) - .build(), - false); + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + endpoint.getInstructionId(), + TestStreams.withOnNext(values::add) + .withOnCompleted(() -> onCompletedWasCalled.set(true)) + .build()); // Test that nothing is emitted till the default buffer size is surpassed. FnDataReceiver dataReceiver = registerOutputLocation(aggregator, endpoint, CODER); aggregator.start(); @@ -187,18 +185,16 @@ public void testConfiguredTimeLimit() throws Exception { .as(ExperimentalOptions.class) .setExperiments(Arrays.asList("data_buffer_time_limit_ms=1")); final CountDownLatch waitForFlush = new CountDownLatch(1); - BeamFnDataOutboundAggregator aggregator = - new BeamFnDataOutboundAggregator( - options, - endpoint::getInstructionId, - TestStreams.withOnNext( - (Consumer) - e -> { - values.add(e); - waitForFlush.countDown(); - }) - .build(), - false); + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + endpoint.getInstructionId(), + TestStreams.withOnNext( + (Consumer) + e -> { + values.add(e); + waitForFlush.countDown(); + }) + .build()); // Test that it emits when time passed the time limit FnDataReceiver dataReceiver = registerOutputLocation(aggregator, endpoint, CODER); @@ -214,17 +210,15 @@ public void testConfiguredTimeLimitExceptionPropagation() throws Exception { options .as(ExperimentalOptions.class) .setExperiments(Arrays.asList("data_buffer_time_limit_ms=1")); - BeamFnDataOutboundAggregator aggregator = - new BeamFnDataOutboundAggregator( - options, - endpoint::getInstructionId, - TestStreams.withOnNext( - (Consumer) - e -> { - throw new RuntimeException(""); - }) - .build(), - false); + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + endpoint.getInstructionId(), + TestStreams.withOnNext( + (Consumer) + e -> { + throw new RuntimeException(""); + }) + .build()); // Test that it emits when time passed the time limit FnDataReceiver dataReceiver = registerOutputLocation(aggregator, endpoint, CODER); @@ -243,17 +237,15 @@ public void testConfiguredTimeLimitExceptionPropagation() throws Exception { // expected } - aggregator = - new BeamFnDataOutboundAggregator( - options, - endpoint::getInstructionId, - TestStreams.withOnNext( - (Consumer) - e -> { - throw new RuntimeException(""); - }) - .build(), - false); + aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + endpoint.getInstructionId(), + TestStreams.withOnNext( + (Consumer) + e -> { + throw new RuntimeException(""); + }) + .build()); dataReceiver = registerOutputLocation(aggregator, endpoint, CODER); aggregator.start(); dataReceiver.accept(new byte[1]); @@ -279,14 +271,12 @@ public void testConfiguredBufferLimitMultipleEndpoints() throws Exception { options .as(ExperimentalOptions.class) .setExperiments(Arrays.asList("data_buffer_size_limit=100")); - BeamFnDataOutboundAggregator aggregator = - new BeamFnDataOutboundAggregator( - options, - endpoint::getInstructionId, - TestStreams.withOnNext(values::add) - .withOnCompleted(() -> onCompletedWasCalled.set(true)) - .build(), - false); + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + endpoint.getInstructionId(), + TestStreams.withOnNext(values::add) + .withOnCompleted(() -> onCompletedWasCalled.set(true)) + .build()); // Test that nothing is emitted till the default buffer size is surpassed. LogicalEndpoint additionalEndpoint = LogicalEndpoint.data( @@ -334,6 +324,37 @@ public void testConfiguredBufferLimitMultipleEndpoints() throws Exception { checkEqualInAnyOrder(builder.build(), values.get(1)); } + @Test + public void testInstructionLifecycle() { + BeamFnDataOutboundAggregator aggregator = + new BeamFnDataOutboundAggregator(PipelineOptionsFactory.create(), false); + assertThrows( + NullPointerException.class, () -> aggregator.sendElements(Elements.getDefaultInstance())); + aggregator.prepareForInstruction( + "testInstruction", + TestStreams.withOnNext( + (Consumer) + e -> { + throw new RuntimeException(""); + }) + .build()); + assertThrows( + IllegalStateException.class, + () -> + aggregator.prepareForInstruction( + "testInstruction", + TestStreams.withOnNext( + (Consumer) + e -> { + throw new RuntimeException(""); + }) + .build())); + aggregator.finishInstruction(); + assertThrows( + NullPointerException.class, () -> aggregator.sendElements(Elements.getDefaultInstance())); + assertThrows(IllegalStateException.class, aggregator::finishInstruction); + } + private void checkEqualInAnyOrder(Elements first, Elements second) { MatcherAssert.assertThat( first.getDataList(), Matchers.containsInAnyOrder(second.getDataList().toArray())); diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java index 703e726739a0..60e83251f147 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java @@ -17,6 +17,8 @@ */ package org.apache.beam.fn.harness; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; @@ -30,7 +32,6 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import javax.annotation.Nullable; import org.apache.beam.fn.harness.control.BeamFnControlClient; import org.apache.beam.fn.harness.control.ExecutionStateSampler; import org.apache.beam.fn.harness.control.FinalizeBundleHandler; @@ -72,6 +73,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.MoreExecutors; +import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,9 +94,6 @@ * for further details. *

      */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class FnHarness { private static final String HARNESS_ID = "HARNESS_ID"; private static final String CONTROL_API_SERVICE_DESCRIPTOR = "CONTROL_API_SERVICE_DESCRIPTOR"; @@ -138,22 +137,31 @@ private static void removeKeyRecursively(JsonNode node, String keyToRemove) { } public static void main(String[] args) throws Exception { - main(System::getenv); + Function environmentVarGetter = System::getenv; + main(environmentVarGetter); } @VisibleForTesting - public static void main(Function environmentVarGetter) throws Exception { + public static void main(Function environmentVarGetter) + throws Exception { JvmInitializers.runOnStartup(); Endpoints.ApiServiceDescriptor loggingApiServiceDescriptor = - getApiServiceDescriptor(environmentVarGetter.apply(LOGGING_API_SERVICE_DESCRIPTOR)); + getApiServiceDescriptor( + checkNotNull( + environmentVarGetter.apply(LOGGING_API_SERVICE_DESCRIPTOR), + "LOGGING_API_SERVICE_DESCRIPTOR env var must be set.")); Endpoints.ApiServiceDescriptor controlApiServiceDescriptor = - getApiServiceDescriptor(environmentVarGetter.apply(CONTROL_API_SERVICE_DESCRIPTOR)); - Endpoints.ApiServiceDescriptor statusApiServiceDescriptor = - environmentVarGetter.apply(STATUS_API_SERVICE_DESCRIPTOR) == null - ? null - : getApiServiceDescriptor(environmentVarGetter.apply(STATUS_API_SERVICE_DESCRIPTOR)); - String id = environmentVarGetter.apply(HARNESS_ID); + getApiServiceDescriptor( + checkNotNull( + environmentVarGetter.apply(CONTROL_API_SERVICE_DESCRIPTOR), + "CONTROL_API_SERVICE_DESCRIPTOR env var must be set.")); + + @Nullable String envVar = environmentVarGetter.apply(STATUS_API_SERVICE_DESCRIPTOR); + Endpoints.@Nullable ApiServiceDescriptor statusApiServiceDescriptor = + (envVar == null) ? null : getApiServiceDescriptor(envVar); + String id = + checkNotNull(environmentVarGetter.apply(HARNESS_ID), "HARNESS_ID env var must be set."); System.out.format("SDK Fn Harness started%n"); System.out.format("Harness ID %s%n", id); @@ -161,11 +169,11 @@ public static void main(Function environmentVarGetter) throws Ex System.out.format("Control location %s%n", controlApiServiceDescriptor); System.out.format("Status location %s%n", statusApiServiceDescriptor); - String pipelineOptionsJson = environmentVarGetter.apply(PIPELINE_OPTIONS); // Try looking for a file first. If that exists it should override PIPELINE_OPTIONS to avoid // maxing out the kernel's environment space + @Nullable String pipelineOptionsJson = null; try { - String pipelineOptionsPath = environmentVarGetter.apply(PIPELINE_OPTIONS_FILE); + @Nullable String pipelineOptionsPath = environmentVarGetter.apply(PIPELINE_OPTIONS_FILE); System.out.format("Pipeline Options File %s%n", pipelineOptionsPath); if (pipelineOptionsPath != null) { Path filePath = Paths.get(pipelineOptionsPath); @@ -179,11 +187,12 @@ public static void main(Function environmentVarGetter) throws Ex } catch (Exception e) { System.out.format("Problem loading pipeline options from file: %s%n", e.getMessage()); } - + if (pipelineOptionsJson == null) { + pipelineOptionsJson = checkNotNull(environmentVarGetter.apply(PIPELINE_OPTIONS)); + } System.out.format("Pipeline options %s%n", pipelineOptionsJson); // TODO: https://github.com/apache/beam/issues/30301 pipelineOptionsJson = removeNestedKey(pipelineOptionsJson, "impersonateServiceAccount"); - PipelineOptions options = PipelineOptionsTranslation.fromJson(pipelineOptionsJson); String runnerCapabilitesOrNull = environmentVarGetter.apply(RUNNER_CAPABILITIES); @@ -219,7 +228,7 @@ public static void main( Set runnerCapabilities, Endpoints.ApiServiceDescriptor loggingApiServiceDescriptor, Endpoints.ApiServiceDescriptor controlApiServiceDescriptor, - @Nullable Endpoints.ApiServiceDescriptor statusApiServiceDescriptor) + Endpoints.@Nullable ApiServiceDescriptor statusApiServiceDescriptor) throws Exception { ManagedChannelFactory channelFactory; if (ExperimentalOptions.hasExperiment(options, "beam_fn_api_epoll")) { @@ -263,7 +272,7 @@ public static void main( Set runnerCapabilites, Endpoints.ApiServiceDescriptor loggingApiServiceDescriptor, Endpoints.ApiServiceDescriptor controlApiServiceDescriptor, - Endpoints.ApiServiceDescriptor statusApiServiceDescriptor, + Endpoints.@Nullable ApiServiceDescriptor statusApiServiceDescriptor, ManagedChannelFactory channelFactory, OutboundObserverFactory outboundObserverFactory, Cache processWideCache) @@ -318,7 +327,7 @@ public static void main( BeamFnControlGrpc.newBlockingStub(channel); BeamFnDataGrpcClient beamFnDataMultiplexer = - new BeamFnDataGrpcClient(options, channelFactory::forDescriptor, outboundObserverFactory); + new BeamFnDataGrpcClient(channelFactory::forDescriptor, outboundObserverFactory); BeamFnStateGrpcClientCache beamFnStateGrpcClientCache = new BeamFnStateGrpcClientCache(idGenerator, channelFactory, outboundObserverFactory); diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java index 5a57b137bf6b..449afd6a0243 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java @@ -224,7 +224,6 @@ public ProcessBundleHandler( private void addRunnerAndConsumersForPTransformRecursively( BeamFnStateClient beamFnStateClient, - BeamFnDataClient queueingClient, String pTransformId, PTransform pTransform, Supplier processBundleInstructionId, @@ -257,7 +256,6 @@ private void addRunnerAndConsumersForPTransformRecursively( for (String consumingPTransformId : pCollectionIdsToConsumingPTransforms.get(pCollectionId)) { addRunnerAndConsumersForPTransformRecursively( beamFnStateClient, - queueingClient, consumingPTransformId, processBundleDescriptor.getTransformsMap().get(consumingPTransformId), processBundleInstructionId, @@ -315,7 +313,7 @@ public ShortIdMap getShortIdMap() { @Override public BeamFnDataClient getBeamFnDataClient() { - return queueingClient; + return beamFnDataClient; } @Override @@ -378,9 +376,8 @@ public FnDataReceiver addOutgoingDataEndpoint( outboundAggregatorMap.computeIfAbsent( apiServiceDescriptor, asd -> - queueingClient.createOutboundAggregator( - asd, - processBundleInstructionId, + new BeamFnDataOutboundAggregator( + options, runnerCapabilities.contains( BeamUrns.getUrn( StandardRunnerProtocols.Enum @@ -391,21 +388,19 @@ public FnDataReceiver addOutgoingDataEndpoint( @Override public FnDataReceiver> addOutgoingTimersEndpoint( String timerFamilyId, org.apache.beam.sdk.coders.Coder> coder) { - BeamFnDataOutboundAggregator aggregator; if (!processBundleDescriptor.hasTimerApiServiceDescriptor()) { throw new IllegalStateException( String.format( - "Timers are unsupported because the " - + "ProcessBundleRequest %s does not provide a timer ApiServiceDescriptor.", + "Timers are unsupported because the ProcessBundleRequest %s does not" + + " provide a timer ApiServiceDescriptor.", processBundleInstructionId.get())); } - aggregator = + BeamFnDataOutboundAggregator aggregator = outboundAggregatorMap.computeIfAbsent( processBundleDescriptor.getTimerApiServiceDescriptor(), asd -> - queueingClient.createOutboundAggregator( - asd, - processBundleInstructionId, + new BeamFnDataOutboundAggregator( + options, runnerCapabilities.contains( BeamUrns.getUrn( StandardRunnerProtocols.Enum @@ -499,6 +494,8 @@ public BundleFinalizer getBundleFinalizer() { */ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest request) throws Exception { + String instructionId = request.getInstructionId(); + String dataStreamId = request.getProcessBundle().getDataStreamId(); @Nullable BundleProcessor bundleProcessor = null; try { bundleProcessor = @@ -515,13 +512,20 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re } })); + for (Map.Entry entry : + bundleProcessor.getOutboundAggregators().entrySet()) { + BeamFnDataOutboundAggregator aggregator = entry.getValue(); + aggregator.prepareForInstruction( + instructionId, beamFnDataClient.getOutboundObserver(entry.getKey(), dataStreamId)); + } + PTransformFunctionRegistry startFunctionRegistry = bundleProcessor.getStartFunctionRegistry(); PTransformFunctionRegistry finishFunctionRegistry = bundleProcessor.getFinishFunctionRegistry(); ExecutionStateTracker stateTracker = bundleProcessor.getStateTracker(); ProcessBundleResponse.Builder response = ProcessBundleResponse.newBuilder(); try (HandleStateCallsForBundle beamFnStateClient = bundleProcessor.getBeamFnStateClient()) { - stateTracker.start(request.getInstructionId()); + stateTracker.start(instructionId); try { // Already in reverse topological order so we don't need to do anything. for (ThrowingRunnable startFunction : startFunctionRegistry.getFunctions()) { @@ -545,12 +549,14 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re } else if (!bundleProcessor.getInboundEndpointApiServiceDescriptors().isEmpty()) { BeamFnDataInboundObserver observer = bundleProcessor.getInboundObserver(); beamFnDataClient.registerReceiver( - request.getInstructionId(), + instructionId, + dataStreamId, bundleProcessor.getInboundEndpointApiServiceDescriptors(), observer); observer.awaitCompletion(); beamFnDataClient.unregisterReceiver( - request.getInstructionId(), + instructionId, + dataStreamId, bundleProcessor.getInboundEndpointApiServiceDescriptors()); } @@ -581,7 +587,7 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re if (!bundleProcessor.getBundleFinalizationCallbackRegistrations().isEmpty()) { finalizeBundleHandler.registerCallbacks( - bundleProcessor.getInstructionId(), + instructionId, ImmutableList.copyOf(bundleProcessor.getBundleFinalizationCallbackRegistrations())); response.setRequiresFinalization(true); } @@ -599,7 +605,7 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re } catch (Exception e) { LOG.debug( "Error processing bundle {} with bundleProcessor for {} after exception", - request.getInstructionId(), + instructionId, request.getProcessBundle().getProcessBundleDescriptorId(), e); if (bundleProcessor != null) { @@ -607,7 +613,7 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re bundleProcessorCache.discard(bundleProcessor); } // Ensure that if more data arrives for the instruction it is discarded. - beamFnDataClient.poisonInstructionId(request.getInstructionId()); + beamFnDataClient.poisonInstructionId(instructionId); throw e; } } @@ -629,6 +635,10 @@ private void embedOutboundElementsIfApplicable( collectedElements.add(elements); } if (!hasFlushedAggregator) { + for (BeamFnDataOutboundAggregator aggregator : + bundleProcessor.getOutboundAggregators().values()) { + aggregator.finishInstruction(); + } Elements.Builder elementsToEmbed = Elements.newBuilder(); for (Elements collectedElement : collectedElements) { elementsToEmbed.mergeFrom(collectedElement); @@ -645,6 +655,7 @@ private void embedOutboundElementsIfApplicable( if (elements != null) { aggregator.sendElements(elements); } + aggregator.finishInstruction(); } } } @@ -875,7 +886,6 @@ public void afterBundleCommit(Instant callbackExpiry, Callback callback) { addRunnerAndConsumersForPTransformRecursively( beamFnStateClient, - beamFnDataClient, entry.getKey(), entry.getValue(), bundleProcessor::getInstructionId, diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java index 94d59d0fcb62..1a50f5b448c5 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java @@ -18,13 +18,12 @@ package org.apache.beam.fn.harness.data; import java.util.List; -import java.util.function.Supplier; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.model.pipeline.v1.Endpoints; import org.apache.beam.model.pipeline.v1.Endpoints.ApiServiceDescriptor; -import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator; import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver; import org.apache.beam.sdk.fn.data.FnDataReceiver; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.StreamObserver; /** * The {@link BeamFnDataClient} is able to forward inbound elements to a {@link FnDataReceiver} and @@ -47,6 +46,7 @@ public interface BeamFnDataClient { */ void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver); @@ -58,7 +58,8 @@ void registerReceiver( * to the {@link BeamFnDataClient} during a future {@link FnDataReceiver#accept} invocation or via * a call to {@link #poisonInstructionId}. */ - void unregisterReceiver(String instructionId, List apiServiceDescriptors); + void unregisterReceiver( + String instructionId, String dataStreamId, List apiServiceDescriptors); /** * Poisons the instruction id, indicating that future data arriving for it should be discarded. @@ -68,22 +69,7 @@ void registerReceiver( */ void poisonInstructionId(String instructionId); - /** - * Creates a {@link BeamFnDataOutboundAggregator} for buffering and sending outbound data and - * timers over the data plane. It is important that {@link - * BeamFnDataOutboundAggregator#sendOrCollectBufferedDataAndFinishOutboundStreams()} is called on - * the returned BeamFnDataOutboundAggregator at the end of each bundle. If - * collectElementsIfNoFlushes is set to true, {@link - * BeamFnDataOutboundAggregator#sendOrCollectBufferedDataAndFinishOutboundStreams()} returns the - * buffered elements instead of sending it through the outbound StreamObserver if there's no - * previous flush. - * - *

      Closing the returned aggregator signals the end of the streams. - * - *

      The returned aggregator is not thread safe. - */ - BeamFnDataOutboundAggregator createOutboundAggregator( - Endpoints.ApiServiceDescriptor apiServiceDescriptor, - Supplier processBundleRequestIdSupplier, - boolean collectElementsIfNoFlushes); + /** Get the outbound observer for the specified apiServiceDescriptor and dataStreamId. */ + StreamObserver getOutboundObserver( + Endpoints.ApiServiceDescriptor apiServiceDescriptor, String dataStreamId); } diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java index 499d816f8cc0..2f2a6b0fc660 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java @@ -18,20 +18,22 @@ package org.apache.beam.fn.harness.data; import java.util.List; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -import java.util.function.Supplier; +import javax.annotation.Nullable; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.model.fnexecution.v1.BeamFnDataGrpc; import org.apache.beam.model.pipeline.v1.Endpoints; import org.apache.beam.model.pipeline.v1.Endpoints.ApiServiceDescriptor; import org.apache.beam.sdk.fn.data.BeamFnDataGrpcMultiplexer; -import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator; import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver; import org.apache.beam.sdk.fn.stream.OutboundObserverFactory; -import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.ManagedChannel; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.Metadata; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.MetadataUtils; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.StreamObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,17 +46,42 @@ public class BeamFnDataGrpcClient implements BeamFnDataClient { private static final Logger LOG = LoggerFactory.getLogger(BeamFnDataGrpcClient.class); - private final ConcurrentMap - multiplexerCache; + private static class MultiplexerKey { + private final Endpoints.ApiServiceDescriptor apiServiceDescriptor; + private final String dataStreamId; + + private MultiplexerKey( + Endpoints.ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { + this.apiServiceDescriptor = apiServiceDescriptor; + this.dataStreamId = dataStreamId; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MultiplexerKey)) { + return false; + } + MultiplexerKey that = (MultiplexerKey) o; + return Objects.equals(dataStreamId, that.dataStreamId) + && Objects.equals(apiServiceDescriptor, that.apiServiceDescriptor); + } + + @Override + public int hashCode() { + return Objects.hash(apiServiceDescriptor, dataStreamId); + } + } + + private final ConcurrentMap multiplexerCache; private final Function channelFactory; private final OutboundObserverFactory outboundObserverFactory; - private final PipelineOptions options; public BeamFnDataGrpcClient( - PipelineOptions options, Function channelFactory, OutboundObserverFactory outboundObserverFactory) { - this.options = options; this.channelFactory = channelFactory; this.outboundObserverFactory = outboundObserverFactory; this.multiplexerCache = new ConcurrentHashMap<>(); @@ -63,21 +90,22 @@ public BeamFnDataGrpcClient( @Override public void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver) { LOG.debug("Registering consumer for {}", instructionId); for (int i = 0, size = apiServiceDescriptors.size(); i < size; i++) { - BeamFnDataGrpcMultiplexer client = getClientFor(apiServiceDescriptors.get(i)); + BeamFnDataGrpcMultiplexer client = getMultiplexer(apiServiceDescriptors.get(i), dataStreamId); client.registerConsumer(instructionId, receiver); } } @Override public void unregisterReceiver( - String instructionId, List apiServiceDescriptors) { + String instructionId, String dataStreamId, List apiServiceDescriptors) { LOG.debug("Unregistering consumer for {}", instructionId); for (int i = 0, size = apiServiceDescriptors.size(); i < size; i++) { - BeamFnDataGrpcMultiplexer client = getClientFor(apiServiceDescriptors.get(i)); + BeamFnDataGrpcMultiplexer client = getMultiplexer(apiServiceDescriptors.get(i), dataStreamId); client.unregisterConsumer(instructionId); } } @@ -91,25 +119,32 @@ public void poisonInstructionId(String instructionId) { } @Override - public BeamFnDataOutboundAggregator createOutboundAggregator( - ApiServiceDescriptor apiServiceDescriptor, - Supplier processBundleRequestIdSupplier, - boolean collectElementsIfNoFlushes) { - return new BeamFnDataOutboundAggregator( - options, - processBundleRequestIdSupplier, - getClientFor(apiServiceDescriptor).getOutboundObserver(), - collectElementsIfNoFlushes); + public StreamObserver getOutboundObserver( + ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { + return getMultiplexer(apiServiceDescriptor, dataStreamId).getOutboundObserver(); } - private BeamFnDataGrpcMultiplexer getClientFor( - Endpoints.ApiServiceDescriptor apiServiceDescriptor) { + private BeamFnDataGrpcMultiplexer getMultiplexer( + Endpoints.ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { + MultiplexerKey key = new MultiplexerKey(apiServiceDescriptor, dataStreamId); return multiplexerCache.computeIfAbsent( - apiServiceDescriptor, - (Endpoints.ApiServiceDescriptor descriptor) -> - new BeamFnDataGrpcMultiplexer( - descriptor, - outboundObserverFactory, - BeamFnDataGrpc.newStub(channelFactory.apply(apiServiceDescriptor))::data)); + key, + k -> { + OutboundObserverFactory.BasicFactory baseOutboundObserverFactory = + inboundObserver -> { + BeamFnDataGrpc.BeamFnDataStub stub = + BeamFnDataGrpc.newStub(channelFactory.apply(apiServiceDescriptor)); + if (dataStreamId != null && !dataStreamId.isEmpty()) { + Metadata headers = new Metadata(); + headers.put( + Metadata.Key.of("data_stream_id", Metadata.ASCII_STRING_MARSHALLER), + dataStreamId); + stub = stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers)); + } + return stub.data(inboundObserver); + }; + return new BeamFnDataGrpcMultiplexer( + apiServiceDescriptor, outboundObserverFactory, baseOutboundObserverFactory); + }); } } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java index 70a894e7b375..2882b75a2593 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; import org.apache.beam.fn.harness.PTransformRunnerFactory.Registrar; import org.apache.beam.fn.harness.data.BeamFnDataClient; import org.apache.beam.model.fnexecution.v1.BeamFnApi; @@ -107,19 +106,29 @@ public void setUp() { MockitoAnnotations.initMocks(this); } - private BeamFnDataOutboundAggregator createRecordingAggregator( - Map>> output, Supplier bundleId) { + @Test + public void testReuseForMultipleBundles() throws Exception { + AtomicReference bundleId = new AtomicReference<>("0"); + String localInputId = "inputPC"; + RunnerApi.PTransform pTransform = + RemoteGrpcPortWrite.writeToPort(localInputId, PORT_SPEC).toPTransform(); + + List> output0 = new ArrayList<>(); + List> output1 = new ArrayList<>(); + Map aggregators = new HashMap<>(); + PipelineOptions options = PipelineOptionsFactory.create(); options.as(ExperimentalOptions.class).setExperiments(Arrays.asList("data_buffer_size_limit=0")); - return new BeamFnDataOutboundAggregator( - options, - bundleId, + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + + Map>> outputs = ImmutableMap.of("0", output0, "1", output1); + StreamObserver observer = new StreamObserver() { @Override public void onNext(Elements elements) { for (Data data : elements.getDataList()) { try { - output.get(bundleId.get()).add(WIRE_CODER.decode(data.getData().newInput())); + outputs.get(bundleId.get()).add(WIRE_CODER.decode(data.getData().newInput())); } catch (IOException e) { throw new RuntimeException("Failed to decode output."); } @@ -131,22 +140,9 @@ public void onError(Throwable throwable) {} @Override public void onCompleted() {} - }, - false); - } - - @Test - public void testReuseForMultipleBundles() throws Exception { - AtomicReference bundleId = new AtomicReference<>("0"); - String localInputId = "inputPC"; - RunnerApi.PTransform pTransform = - RemoteGrpcPortWrite.writeToPort(localInputId, PORT_SPEC).toPTransform(); + }; - List> output0 = new ArrayList<>(); - List> output1 = new ArrayList<>(); - Map aggregators = new HashMap<>(); - BeamFnDataOutboundAggregator aggregator = - createRecordingAggregator(ImmutableMap.of("0", output0, "1", output1), bundleId::get); + aggregator.prepareForInstruction(bundleId.get(), observer); aggregators.put(PORT_SPEC.getApiServiceDescriptor(), aggregator); PTransformRunnerFactoryTestContext context = @@ -172,18 +168,20 @@ public void testReuseForMultipleBundles() throws Exception { FnDataReceiver pCollectionConsumer = context.getPCollectionConsumer(localInputId); pCollectionConsumer.accept(valueInGlobalWindow("ABC")); pCollectionConsumer.accept(valueInGlobalWindow("DEF")); - assertThat(output0, contains(valueInGlobalWindow("ABC"), valueInGlobalWindow("DEF"))); + aggregator.finishInstruction(); output0.clear(); // Process for bundle id 1 bundleId.set("1"); + aggregator.prepareForInstruction(bundleId.get(), observer); pCollectionConsumer.accept(valueInGlobalWindow("GHI")); pCollectionConsumer.accept(valueInGlobalWindow("JKL")); assertThat(output1, contains(valueInGlobalWindow("GHI"), valueInGlobalWindow("JKL"))); + aggregator.finishInstruction(); verifyNoMoreInteractions(mockBeamFnDataClient); } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java index 50a2fec0b5a2..2aa555e83cd5 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java @@ -839,7 +839,7 @@ private class TestBeamFnDataOutboundAggregator extends BeamFnDataOutboundAggrega private Supplier processBundleRequestIdSupplier; public TestBeamFnDataOutboundAggregator(Supplier bundleIdSupplier) { - super(PipelineOptionsFactory.create(), bundleIdSupplier, null, false); + super(PipelineOptionsFactory.create(), false); this.timers = new HashMap<>(); this.dataOutput = new HashMap<>(); this.processBundleRequestIdSupplier = bundleIdSupplier; diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java index 7b4387738a4c..51e49953b406 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java @@ -54,6 +54,7 @@ import org.apache.beam.sdk.transforms.DoFn.BundleFinalizer; import org.apache.beam.sdk.util.construction.Timer; import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.StreamObserver; import org.joda.time.Instant; /** @@ -74,6 +75,7 @@ public static Builder builder(String pTransformId, RunnerApi.PTransform pTransfo @Override public void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver) { throw new UnsupportedOperationException("Unexpected call during test."); @@ -81,15 +83,15 @@ public void registerReceiver( @Override public void unregisterReceiver( - String instructionId, List apiServiceDescriptors) { + String instructionId, + String dataStreamId, + List apiServiceDescriptors) { throw new UnsupportedOperationException("Unexpected call during test."); } @Override - public BeamFnDataOutboundAggregator createOutboundAggregator( - ApiServiceDescriptor apiServiceDescriptor, - Supplier processBundleRequestIdSupplier, - boolean collectElementsIfNoFlushes) { + public StreamObserver getOutboundObserver( + ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { throw new UnsupportedOperationException("Unexpected call during test."); } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java index 47f85178b0a1..c03f82726740 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java @@ -37,7 +37,6 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; @@ -1071,28 +1070,20 @@ private ProcessBundleHandler setupProcessBundleHandlerForSimpleRecordingDoFn( dataOutput.add(input.getValue()); })); - Mockito.doAnswer( - (invocation) -> - new BeamFnDataOutboundAggregator( - PipelineOptionsFactory.create(), - invocation.getArgument(1), - new StreamObserver() { - @Override - public void onNext(Elements elements) { - for (Timers timer : elements.getTimersList()) { - timerOutput.addAll(elements.getTimersList()); - } - } + Mockito.when(beamFnDataClient.getOutboundObserver(any(), any())) + .thenReturn( + new StreamObserver() { + @Override + public void onNext(Elements elements) { + timerOutput.addAll(elements.getTimersList()); + } - @Override - public void onError(Throwable throwable) {} + @Override + public void onError(Throwable throwable) {} - @Override - public void onCompleted() {} - }, - invocation.getArgument(2))) - .when(beamFnDataClient) - .createOutboundAggregator(any(), any(), anyBoolean()); + @Override + public void onCompleted() {} + }); return new ProcessBundleHandler( PipelineOptionsFactory.create(), @@ -1409,7 +1400,7 @@ public void testInstructionIsUnregisteredFromBeamFnDataClientOnSuccess() throws (invocation) -> { String instructionId = invocation.getArgument(0, String.class); CloseableFnDataReceiver data = - invocation.getArgument(2, CloseableFnDataReceiver.class); + invocation.getArgument(3, CloseableFnDataReceiver.class); data.accept( BeamFnApi.Elements.newBuilder() .addData( @@ -1421,7 +1412,7 @@ public void testInstructionIsUnregisteredFromBeamFnDataClientOnSuccess() throws return null; }) .when(beamFnDataClient) - .registerReceiver(any(), any(), any()); + .registerReceiver(any(), any(), any(), any()); ProcessBundleHandler handler = new ProcessBundleHandler( @@ -1451,8 +1442,8 @@ public void testInstructionIsUnregisteredFromBeamFnDataClientOnSuccess() throws .build()); // Ensure that we unregister during successful processing - verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any()); - verify(beamFnDataClient).unregisterReceiver(eq("instructionId"), any()); + verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any(), any()); + verify(beamFnDataClient).unregisterReceiver(eq("instructionId"), any(), any()); verifyNoMoreInteractions(beamFnDataClient); } @@ -1475,7 +1466,7 @@ public void testDataProcessingExceptionsArePropagated() throws Exception { StringUtf8Coder.of().encode("A", encodedData); String instructionId = invocation.getArgument(0, String.class); CloseableFnDataReceiver data = - invocation.getArgument(2, CloseableFnDataReceiver.class); + invocation.getArgument(3, CloseableFnDataReceiver.class); data.accept( BeamFnApi.Elements.newBuilder() .addData( @@ -1489,7 +1480,7 @@ public void testDataProcessingExceptionsArePropagated() throws Exception { return null; }) .when(beamFnDataClient) - .registerReceiver(any(), any(), any()); + .registerReceiver(any(), any(), any(), any()); ProcessBundleHandler handler = new ProcessBundleHandler( @@ -1526,7 +1517,7 @@ public void testDataProcessingExceptionsArePropagated() throws Exception { .build())); // Ensure that we unregister during successful processing - verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any()); + verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any(), any()); verify(beamFnDataClient).poisonInstructionId(eq("instructionId")); verifyNoMoreInteractions(beamFnDataClient); } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java index 15f83f2582c7..9d9efa0b9c49 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java @@ -23,8 +23,8 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.util.Arrays; import java.util.Collection; @@ -49,6 +49,7 @@ import org.apache.beam.sdk.fn.data.LogicalEndpoint; import org.apache.beam.sdk.fn.stream.OutboundObserverFactory; import org.apache.beam.sdk.fn.test.TestStreams; +import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.values.WindowedValue; @@ -169,7 +170,6 @@ public StreamObserver data( BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); @@ -183,7 +183,7 @@ public StreamObserver data( Collections.emptyList()); clientFactory.registerReceiver( - INSTRUCTION_ID_A, Arrays.asList(apiServiceDescriptor), observerA); + INSTRUCTION_ID_A, "", Arrays.asList(apiServiceDescriptor), observerA); waitForClientToConnect.await(); outboundServerObserver.get().onNext(ELEMENTS_A_1); @@ -193,7 +193,7 @@ public StreamObserver data( Thread.sleep(100); clientFactory.registerReceiver( - INSTRUCTION_ID_B, Arrays.asList(apiServiceDescriptor), observerB); + INSTRUCTION_ID_B, "", Arrays.asList(apiServiceDescriptor), observerB); // Show that out of order stream completion can occur. observerB.awaitCompletion(); @@ -245,7 +245,6 @@ public StreamObserver data( BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); @@ -262,7 +261,7 @@ public StreamObserver data( Collections.emptyList()); clientFactory.registerReceiver( - INSTRUCTION_ID_A, Arrays.asList(apiServiceDescriptor), observer); + INSTRUCTION_ID_A, "", Arrays.asList(apiServiceDescriptor), observer); waitForClientToConnect.await(); @@ -270,12 +269,8 @@ public StreamObserver data( outboundServerObserver.get().onNext(ELEMENTS_A_1); outboundServerObserver.get().onNext(ELEMENTS_A_2); - try { - observer.awaitCompletion(); - fail("Expected channel to fail"); - } catch (Exception e) { - assertEquals(exceptionToThrow, e); - } + Exception e = assertThrows(Exception.class, observer::awaitCompletion); + assertEquals(exceptionToThrow, e); // The server should not have received any values assertThat(inboundServerValues, empty()); // The consumer should have only been invoked once @@ -321,7 +316,6 @@ public StreamObserver data( BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); @@ -347,7 +341,7 @@ public StreamObserver data( }); clientFactory.registerReceiver( - INSTRUCTION_ID_A, Arrays.asList(apiServiceDescriptor), observerA); + INSTRUCTION_ID_A, "", Arrays.asList(apiServiceDescriptor), observerA); waitForClientToConnect.await(); outboundServerObserver.get().onNext(ELEMENTS_B_1); @@ -358,11 +352,9 @@ public StreamObserver data( assertTrue(receivedAElement.await(5, TimeUnit.SECONDS)); clientFactory.poisonInstructionId(INSTRUCTION_ID_A); - try { - future.get(); - fail(); // We expect the awaitCompletion to fail due to closing. - } catch (Exception ignored) { - } + // We expect the awaitCompletion to fail due to closing. + // Expected. + assertThrows(Exception.class, future::get); outboundServerObserver.get().onNext(ELEMENTS_A_2); @@ -404,16 +396,15 @@ public StreamObserver data( ManagedChannel channel = InProcessChannelBuilder.forName(apiServiceDescriptor.getUrl()).build(); + PipelineOptions options = + PipelineOptionsFactory.fromArgs("--experiments=data_buffer_size_limit=20").create(); BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.fromArgs( - new String[] {"--experiments=data_buffer_size_limit=20"}) - .create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); - BeamFnDataOutboundAggregator aggregator = - clientFactory.createOutboundAggregator( - apiServiceDescriptor, () -> INSTRUCTION_ID_A, false); + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + INSTRUCTION_ID_A, clientFactory.getOutboundObserver(apiServiceDescriptor, "")); FnDataReceiver> fnDataReceiver = aggregator.registerOutputDataLocation(TRANSFORM_ID_A, CODER); fnDataReceiver.accept(valueInGlobalWindow("ABC")); From bbde34269bd24dbd588a28936d6d04b84d5cf307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:07:01 -0400 Subject: [PATCH 431/490] Bump github.com/moby/moby/client from 0.4.1 to 0.5.0 in /sdks (#39032) Bumps [github.com/moby/moby/client](https://github.com/moby/moby) from 0.4.1 to 0.5.0. - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.5.0/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.4.1...v0.5.0) --- updated-dependencies: - dependency-name: github.com/moby/moby/client dependency-version: 0.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Derrick Williams --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index b6c261644d06..116f40961052 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -73,7 +73,7 @@ require ( github.com/fsouza/fake-gcs-server v1.52.3 github.com/golang-cz/devslog v0.0.16 github.com/moby/moby/api v1.55.0 - github.com/moby/moby/client v0.4.1 + github.com/moby/moby/client v0.5.0 golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a ) diff --git a/sdks/go.sum b/sdks/go.sum index 6cae761de1a6..1db20e1d6602 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -689,8 +689,8 @@ github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8 github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= -github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= From da4416cd0a65e97015b01fe92dc432f3ce7465df Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:00:29 -0400 Subject: [PATCH 432/490] [Iceberg AddFiles] Add NameMapping when creating table (#39022) * improvement * fix nullness * clean --- .../apache/beam/sdk/io/iceberg/AddFiles.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java index 010d7f558a12..9fad990a4391 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java @@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -96,7 +97,6 @@ import org.apache.iceberg.Table; import org.apache.iceberg.TableProperties; import org.apache.iceberg.avro.Avro; -import org.apache.iceberg.catalog.Catalog; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.AlreadyExistsException; import org.apache.iceberg.exceptions.NoSuchTableException; @@ -531,17 +531,23 @@ private Table getOrCreateTable(String filePath, FileFormat format) throws IOExce org.apache.iceberg.Schema schema = getSchema(filePath, format); PartitionSpec spec = PartitionUtils.toPartitionSpec(partitionFields, schema); SortOrder sortOrder = SortOrderUtils.toSortOrder(sortFields, schema); - - Catalog.TableBuilder builder = - catalogConfig - .catalog() - .buildTable(tableId, schema) - .withPartitionSpec(spec) - .withSortOrder(sortOrder); - if (tableProps != null) { - builder.withProperties(tableProps); + Map properties = + tableProps != null ? new HashMap<>(tableProps) : new HashMap<>(); + if (properties.get(TableProperties.DEFAULT_NAME_MAPPING) == null) { + // Forces Name based resolution instead of position based resolution + NameMapping mapping = MappingUtil.create(schema); + String mappingJson = NameMappingParser.toJson(mapping); + properties.put(TableProperties.DEFAULT_NAME_MAPPING, mappingJson); } - return builder.create(); + + return catalogConfig + .catalog() + .buildTable(tableId, schema) + .withPartitionSpec(spec) + .withSortOrder(sortOrder) + .withProperties(properties) + .create(); + } catch (AlreadyExistsException e2) { // if table already exists, just load it return catalogConfig.catalog().loadTable(TableIdentifier.parse(identifier)); } From 6335c70da4e8c0ae9db563ee5d9cec50b51706ec Mon Sep 17 00:00:00 2001 From: Manan Mangal Date: Fri, 19 Jun 2026 19:24:04 -0700 Subject: [PATCH 433/490] Add Flink 2.2.1 runner support (#38980) --- .../test-properties.json | 2 +- .../beam_PostCommit_Java_Examples_Flink.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Flink.yml | 4 +- .../beam_PostCommit_Java_PVR_Flink_Batch.yml | 2 +- ...am_PostCommit_Java_PVR_Flink_Streaming.yml | 4 +- .../beam_PostCommit_Java_Tpcds_Flink.yml | 2 +- ..._PostCommit_Java_ValidatesRunner_Flink.yml | 4 +- .../workflows/beam_PostCommit_XVR_Flink.yml | 2 +- .../beam_PreCommit_Java_PVR_Flink_Batch.yml | 2 +- .../beam_PreCommit_Java_PVR_Flink_Docker.yml | 2 +- .../beam_Publish_Docker_Snapshots.yml | 4 +- .../run_rc_validation_java_quickstart.yml | 2 +- CHANGES.md | 3 + gradle.properties | 2 +- release/build.gradle.kts | 2 +- runners/flink/2.2/build.gradle | 71 + .../2.2/job-server-container/build.gradle | 26 + runners/flink/2.2/job-server/build.gradle | 44 + .../flink/FlinkExecutionEnvironments.java | 507 +++++ .../wrappers/streaming/DoFnOperator.java | 1786 +++++++++++++++++ .../flink/FlinkPipelineOptionsTest.java | 205 ++ sdks/go/examples/wasm/README.md | 6 +- .../apache_beam/options/pipeline_options.py | 2 +- .../src/apache_beam/runners/flink.ts | 2 +- .../content/en/documentation/runners/flink.md | 10 + 25 files changed, 2675 insertions(+), 23 deletions(-) create mode 100644 runners/flink/2.2/build.gradle create mode 100644 runners/flink/2.2/job-server-container/build.gradle create mode 100644 runners/flink/2.2/job-server/build.gradle create mode 100644 runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java create mode 100644 runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperator.java create mode 100644 runners/flink/2.2/src/test/java/org/apache/beam/runners/flink/FlinkPipelineOptionsTest.java diff --git a/.github/actions/setup-default-test-properties/test-properties.json b/.github/actions/setup-default-test-properties/test-properties.json index ddb4a8b73ffb..5a1057470b68 100644 --- a/.github/actions/setup-default-test-properties/test-properties.json +++ b/.github/actions/setup-default-test-properties/test-properties.json @@ -14,7 +14,7 @@ }, "JavaTestProperties": { "SUPPORTED_VERSIONS": ["8", "11", "17", "21", "25"], - "FLINK_VERSIONS": ["1.19", "1.20", "2.0", "2.1"], + "FLINK_VERSIONS": ["1.19", "1.20", "2.0", "2.1", "2.2"], "SPARK_VERSIONS": ["3"] }, "GoTestProperties": { diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml index 9f60e1529d15..de984a4b2bb7 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml @@ -80,7 +80,7 @@ jobs: - name: run examplesIntegrationTest script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:1.20:examplesIntegrationTest + gradle-command: :runners:flink:2.2:examplesIntegrationTest - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml index 9d7ca2043d74..4a3574906a28 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml @@ -102,7 +102,7 @@ jobs: with: gradle-command: :sdks:java:testing:nexmark:run arguments: | - -Pnexmark.runner=:runners:flink:1.20 \ + -Pnexmark.runner=:runners:flink:2.2 \ "${{ env.GRADLE_COMMAND_ARGUMENTS }} --streaming=${{ matrix.streaming }} --queryLanguage=${{ matrix.queryLanguage }}" \ - name: run PostCommit Java Nexmark Flink (${{ matrix.streaming }}) script if: matrix.queryLanguage == 'none' @@ -110,5 +110,5 @@ jobs: with: gradle-command: :sdks:java:testing:nexmark:run arguments: | - -Pnexmark.runner=:runners:flink:1.19 \ + -Pnexmark.runner=:runners:flink:2.2 \ "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml index 476793bfa8b6..da8c29e4955b 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml @@ -69,7 +69,7 @@ jobs: job_name: ["beam_PostCommit_Java_PVR_Flink_Batch"] job_phrase: ["Run Java_PVR_Flink_Batch PostCommit"] # every major version - flink_version: ['1.20', '2.0'] + flink_version: ['1.20', '2.0', '2.1', '2.2'] timeout-minutes: 240 runs-on: [self-hosted, ubuntu-24.04, highmem] if: | diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml index d5f7edeb11d0..f3c6560f994f 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml @@ -56,11 +56,11 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 120 strategy: - matrix: + matrix: job_name: [beam_PostCommit_Java_PVR_Flink_Streaming] job_phrase: [Run Java Flink PortableValidatesRunner Streaming] # every major version - flink_version: [ '1.20', '2.0' ] + flink_version: [ '1.20', '2.0', '2.1', '2.2'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml index ae3dcb7d6ad2..49416266b487 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml @@ -101,5 +101,5 @@ jobs: with: gradle-command: :sdks:java:testing:tpcds:run arguments: | - -Ptpcds.runner=:runners:flink:1.20 \ + -Ptpcds.runner=:runners:flink:2.2 \ "-Ptpcds.args=${{env.tpcdsBigQueryArgs}} ${{env.tpcdsInfluxDBArgs}} ${{ env.GRADLE_COMMAND_ARGUMENTS }} --queries=${{env.tpcdsQueriesArg}}" \ diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml index febec1afa592..b55d6342cdbb 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml @@ -49,7 +49,7 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PostCommit_Java_ValidatesRunner_Flink: + beam_PostCommit_Java_ValidatesRunner_Flink: name: ${{ matrix.job_name }} (${{ matrix.flink_version }}) runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 100 @@ -58,7 +58,7 @@ jobs: job_name: [beam_PostCommit_Java_ValidatesRunner_Flink] job_phrase: [Run Flink ValidatesRunner] # every major version - flink_version: ['1.20', '2.0'] + flink_version: ['1.20', '2.0', '2.1', '2.2'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml b/.github/workflows/beam_PostCommit_XVR_Flink.yml index 44cb264ad9dd..e93fe82f48ad 100644 --- a/.github/workflows/beam_PostCommit_XVR_Flink.yml +++ b/.github/workflows/beam_PostCommit_XVR_Flink.yml @@ -47,7 +47,7 @@ env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - FlinkVersion: 1.20 + FlinkVersion: 2.2 jobs: beam_PostCommit_XVR_Flink: diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml index 9e04f103a779..43549eb32c52 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml @@ -94,7 +94,7 @@ jobs: - name: run validatesPortableRunnerBatch script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:1.20:job-server:validatesPortableRunnerBatch + gradle-command: :runners:flink:2.2:job-server:validatesPortableRunnerBatch env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH }} - name: Archive JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml index 3ad28549e59e..9d95d830614d 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml @@ -99,7 +99,7 @@ jobs: - name: run PreCommit Java PVR Flink Docker script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:1.20:job-server:validatesPortableRunnerDocker + gradle-command: :runners:flink:2.2:job-server:validatesPortableRunnerDocker env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - name: Archive JUnit Test Results diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml b/.github/workflows/beam_Publish_Docker_Snapshots.yml index 5e4412b2650c..0895c226d479 100644 --- a/.github/workflows/beam_Publish_Docker_Snapshots.yml +++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml @@ -84,10 +84,10 @@ jobs: -Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \ -Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \ -Pdocker-pull-licenses - - name: run Publish Docker Snapshots script for Flink 2.0 + - name: run Publish Docker Snapshots script for Flink 2.2 uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:2.0:job-server-container:dockerPush + gradle-command: :runners:flink:2.2:job-server-container:dockerPush arguments: | -Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \ -Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \ diff --git a/.github/workflows/run_rc_validation_java_quickstart.yml b/.github/workflows/run_rc_validation_java_quickstart.yml index 41a6991d14ee..0675d33c3181 100644 --- a/.github/workflows/run_rc_validation_java_quickstart.yml +++ b/.github/workflows/run_rc_validation_java_quickstart.yml @@ -88,7 +88,7 @@ jobs: - name: Run QuickStart Java Flink Runner uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:2.1:runQuickstartJavaFlinkLocal + gradle-command: :runners:flink:2.2:runQuickstartJavaFlinkLocal arguments: | -Prepourl=${{ env.APACHE_REPO_URL }} \ -Pver=${{ env.RELEASE_VERSION }} diff --git a/CHANGES.md b/CHANGES.md index 0139b8bbd3b0..bd9c6ea246a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -74,6 +74,8 @@ * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). * (Python) Added instrumentation to support off-the-shelf profiling agents when launching Python SDK Harness ([#38853](https://github.com/apache/beam/issues/38853)). * (Java) Added support to the FnApi Data stream protocol allowing runners to isolate bundles slowly processing input from other bundles. ([#39001](https://github.com/apache/beam/issues/39001)). +* (Java) Flink 2.1 support added ([#38947](https://github.com/apache/beam/issues/38947)). +* (Java) Flink 2.2 support added ([#38978](https://github.com/apache/beam/issues/38978)). ## Breaking Changes @@ -99,6 +101,7 @@ [comment]: # ( When updating known issues after release, make sure also update website blog in website/www/site/content/blog.) * ([#X](https://github.com/apache/beam/issues/X)). +* (Java) Projects using the Flink runner with Flink 2.1 or later alongside libraries requiring `org.lz4:lz4-java` (e.g., Kafka clients) may encounter a Gradle capability conflict, because Flink 2.1+ ships `at.yawk.lz4:lz4-java` which declares the same capability. To resolve, add a `capabilitiesResolution` rule to your `build.gradle` that selects `at.yawk.lz4:lz4-java` ([#38947](https://github.com/apache/beam/issues/38947)). # [2.74.0] - 2026-06-02 diff --git a/gradle.properties b/gradle.properties index c20a8373cb60..bc84397c63ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ docker_image_default_repo_root=apache docker_image_default_repo_prefix=beam_ # supported flink versions -flink_versions=1.19,1.20,2.0,2.1 +flink_versions=1.19,1.20,2.0,2.1,2.2 # supported spark versions spark_versions=3,4 # supported python versions diff --git a/release/build.gradle.kts b/release/build.gradle.kts index 5be707428605..54165dc49654 100644 --- a/release/build.gradle.kts +++ b/release/build.gradle.kts @@ -39,7 +39,7 @@ task("runJavaExamplesValidationTask") { dependsOn(":runners:direct-java:runQuickstartJavaDirect") dependsOn(":runners:google-cloud-dataflow-java:runQuickstartJavaDataflow") dependsOn(":runners:spark:3:runQuickstartJavaSpark") - dependsOn(":runners:flink:2.0:runQuickstartJavaFlinkLocal") + dependsOn(":runners:flink:2.2:runQuickstartJavaFlinkLocal") dependsOn(":runners:direct-java:runMobileGamingJavaDirect") if (project.hasProperty("ver") || !project.version.toString().endsWith("SNAPSHOT")) { // only run one variant of MobileGaming on Dataflow for nightly diff --git a/runners/flink/2.2/build.gradle b/runners/flink/2.2/build.gradle new file mode 100644 index 000000000000..3856d20a3512 --- /dev/null +++ b/runners/flink/2.2/build.gradle @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Flink 2.2 API changes addressed in this module's source overrides: +// - CheckpointingMode moved (Flink 2.2): org.apache.flink.streaming.api.CheckpointingMode +// is now @Deprecated; canonical path is org.apache.flink.core.execution.CheckpointingMode. +// FlinkExecutionEnvironments.java, DoFnOperator.java, and FlinkPipelineOptionsTest.java +// are overridden here to use the non-deprecated import. +// +// Flink 2.2 breaking changes that are SQL/cluster-layer only (no runner code change needed): +// - TLS cipher suite default changed (FLINK-39022): JDK 11.0.30+/17.0.18+/21.0.10+/24+ +// disabled TLS_RSA_* ciphers; new default uses ECDHE suites. Verify cipher support +// on TLS-enabled clusters before upgrading. +// - StreamingMultiJoinOperator state format change (FLINK-38209): savepoints from Flink 2.1 +// with table.optimizer.multi-join.enabled=true are NOT compatible with 2.2. +// - SQL row NOT NULL now enforced (FLINK-38181): use table.legacy-nested-row-nullability +// to restore prior (silent-ignore) behavior if needed. + +project.ext { + flink_major = '2.2' + flink_version = '2.2.1' + excluded_files = [ + 'main': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapter.java", + "org/apache/beam/runners/flink/FlinkBatchPipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchPortablePipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchTransformTranslators.java", + "org/apache/beam/runners/flink/translation/functions/FlinkNonMergingReduceFunction.java", + // Moved to org.apache.flink.runtime.state.StateBackendFactory + "org/apache/beam/runners/flink/FlinkStateBackendFactory.java", + ], + 'test': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapterTest.java", + "org/apache/beam/runners/flink/batch/NonMergingGroupByKeyTest.java", + "org/apache/beam/runners/flink/batch/ReshuffleTest.java", + ] + ] +} + +// Load the main build script which contains all build logic. +apply from: "../flink_runner.gradle" + +// Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} diff --git a/runners/flink/2.2/job-server-container/build.gradle b/runners/flink/2.2/job-server-container/build.gradle new file mode 100644 index 000000000000..afdb68a0fc91 --- /dev/null +++ b/runners/flink/2.2/job-server-container/build.gradle @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '../../job-server-container' + +project.ext { + resource_path = basePath +} + +// Load the main build script which contains all build logic. +apply from: "$basePath/flink_job_server_container.gradle" diff --git a/runners/flink/2.2/job-server/build.gradle b/runners/flink/2.2/job-server/build.gradle new file mode 100644 index 000000000000..3f6b184e3fca --- /dev/null +++ b/runners/flink/2.2/job-server/build.gradle @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '../../job-server' + +project.ext { + // Look for the source code in the parent module + main_source_dirs = ["$basePath/src/main/java"] + test_source_dirs = ["$basePath/src/test/java"] + main_resources_dirs = ["$basePath/src/main/resources"] + test_resources_dirs = ["$basePath/src/test/resources"] + archives_base_name = 'beam-runners-flink-2.2-job-server' +} + +// Load the main build script which contains all build logic. +apply from: "$basePath/flink_job_server.gradle" + +// Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} diff --git a/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java b/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java new file mode 100644 index 000000000000..b340632219a2 --- /dev/null +++ b/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java @@ -0,0 +1,507 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.flink; + +import static org.apache.flink.streaming.api.environment.StreamExecutionEnvironment.getDefaultLocalParallelism; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.beam.runners.core.construction.SerializablePipelineOptions; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Streams; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.net.HostAndPort; +import org.apache.flink.api.common.ExecutionConfig; +import org.apache.flink.api.common.RuntimeExecutionMode; +import org.apache.flink.api.common.serialization.SerializerConfigImpl; +import org.apache.flink.configuration.CheckpointingOptions; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.configuration.CoreOptions; +import org.apache.flink.configuration.DeploymentOptions; +import org.apache.flink.configuration.ExternalizedCheckpointRetention; +import org.apache.flink.configuration.GlobalConfiguration; +import org.apache.flink.configuration.RestOptions; +import org.apache.flink.configuration.RestartStrategyOptions; +import org.apache.flink.configuration.StateBackendOptions; +import org.apache.flink.configuration.TaskManagerOptions; +import org.apache.flink.core.execution.CheckpointingMode; +import org.apache.flink.runtime.jobgraph.SavepointRestoreSettings; +import org.apache.flink.runtime.util.EnvironmentInformation; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.RemoteStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Utilities for Flink execution environments. */ +@SuppressWarnings({ + "nullness" // TODO(https://github.com/apache/beam/issues/20497) +}) +public class FlinkExecutionEnvironments { + + private static final Logger LOG = LoggerFactory.getLogger(FlinkExecutionEnvironments.class); + + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * If the submitted job is a batch processing job, this method creates the adequate Flink {@link + * org.apache.flink.streaming.api.environment.StreamExecutionEnvironment} depending on the + * user-specified options. + */ + public static StreamExecutionEnvironment createBatchExecutionEnvironment( + FlinkPipelineOptions options) { + return createBatchExecutionEnvironment( + options, + MoreObjects.firstNonNull(options.getFilesToStage(), Collections.emptyList()), + options.getFlinkConfDir()); + } + + static StreamExecutionEnvironment createBatchExecutionEnvironment( + FlinkPipelineOptions options, List filesToStage, @Nullable String confDir) { + + LOG.info("Creating a Batch Execution Environment."); + + // Although Flink uses Rest, it expects the address not to contain a http scheme + String flinkMasterHostPort = stripHttpSchema(options.getFlinkMaster()); + Configuration flinkConfiguration = getFlinkConfiguration(confDir); + StreamExecutionEnvironment flinkBatchEnv; + + // depending on the master, create the right environment. + if ("[local]".equals(flinkMasterHostPort)) { + setManagedMemoryByFraction(flinkConfiguration); + disableClassLoaderLeakCheck(flinkConfiguration); + flinkBatchEnv = StreamExecutionEnvironment.createLocalEnvironment(flinkConfiguration); + if (!options.getAttachedMode()) { + LOG.warn("Detached mode is only supported in RemoteStreamEnvironment"); + } + } else if ("[collection]".equals(flinkMasterHostPort)) { + throw new UnsupportedOperationException( + "CollectionEnvironment has been removed in Flink 2. Use [local] instead."); + } else if ("[auto]".equals(flinkMasterHostPort)) { + flinkBatchEnv = StreamExecutionEnvironment.getExecutionEnvironment(); + if (flinkBatchEnv instanceof LocalStreamEnvironment) { + disableClassLoaderLeakCheck(flinkConfiguration); + flinkBatchEnv = StreamExecutionEnvironment.createLocalEnvironment(flinkConfiguration); + flinkBatchEnv.setParallelism(getDefaultLocalParallelism()); + } + if (!options.getAttachedMode()) { + LOG.warn("Detached mode is not supported in [auto]."); + } + } else { + int defaultPort = flinkConfiguration.get(RestOptions.PORT); + HostAndPort hostAndPort = + HostAndPort.fromString(flinkMasterHostPort).withDefaultPort(defaultPort); + flinkConfiguration.set(RestOptions.PORT, hostAndPort.getPort()); + if (!options.getAttachedMode()) { + flinkConfiguration.set(DeploymentOptions.ATTACHED, options.getAttachedMode()); + } + flinkBatchEnv = + StreamExecutionEnvironment.createRemoteEnvironment( + hostAndPort.getHost(), + hostAndPort.getPort(), + flinkConfiguration, + filesToStage.toArray(new String[filesToStage.size()])); + LOG.info("Using Flink Master URL {}:{}.", hostAndPort.getHost(), hostAndPort.getPort()); + } + + // Set the execution mode for data exchange. + flinkBatchEnv.setRuntimeMode(RuntimeExecutionMode.BATCH); + + // set the correct parallelism. + if (options.getParallelism() != -1) { + flinkBatchEnv.setParallelism(options.getParallelism()); + } + + // Set the correct parallelism, required by UnboundedSourceWrapper to generate consistent + // splits. + final int parallelism = + determineParallelism( + options.getParallelism(), flinkBatchEnv.getParallelism(), flinkConfiguration); + + flinkBatchEnv.setParallelism(parallelism); + // set parallelism in the options (required by some execution code) + options.setParallelism(parallelism); + + if (options.getObjectReuse()) { + flinkBatchEnv.getConfig().enableObjectReuse(); + } else { + flinkBatchEnv.getConfig().disableObjectReuse(); + } + + applyLatencyTrackingInterval(flinkBatchEnv.getConfig(), options); + + configureWebUIOptions(flinkBatchEnv.getConfig(), options.as(PipelineOptions.class)); + + return flinkBatchEnv; + } + + @VisibleForTesting + static StreamExecutionEnvironment createStreamExecutionEnvironment(FlinkPipelineOptions options) { + return createStreamExecutionEnvironment( + options, + MoreObjects.firstNonNull(options.getFilesToStage(), Collections.emptyList()), + options.getFlinkConfDir()); + } + + /** + * If the submitted job is a stream processing job, this method creates the adequate Flink {@link + * org.apache.flink.streaming.api.environment.StreamExecutionEnvironment} depending on the + * user-specified options. + */ + public static StreamExecutionEnvironment createStreamExecutionEnvironment( + FlinkPipelineOptions options, List filesToStage, @Nullable String confDir) { + + LOG.info("Creating a Streaming Environment."); + + // Although Flink uses Rest, it expects the address not to contain a http scheme + String masterUrl = stripHttpSchema(options.getFlinkMaster()); + Configuration flinkConfiguration = getFlinkConfiguration(confDir); + configureRestartStrategy(options, flinkConfiguration); + configureStateBackend(options, flinkConfiguration); + StreamExecutionEnvironment flinkStreamEnv; + + // depending on the master, create the right environment. + if ("[local]".equals(masterUrl)) { + setManagedMemoryByFraction(flinkConfiguration); + disableClassLoaderLeakCheck(flinkConfiguration); + flinkStreamEnv = + StreamExecutionEnvironment.createLocalEnvironment( + getDefaultLocalParallelism(), flinkConfiguration); + if (!options.getAttachedMode()) { + LOG.warn("Detached mode is only supported in RemoteStreamEnvironment"); + } + } else if ("[auto]".equals(masterUrl)) { + + flinkStreamEnv = StreamExecutionEnvironment.getExecutionEnvironment(flinkConfiguration); + if (flinkStreamEnv instanceof LocalStreamEnvironment) { + disableClassLoaderLeakCheck(flinkConfiguration); + flinkStreamEnv = + StreamExecutionEnvironment.createLocalEnvironment( + getDefaultLocalParallelism(), flinkConfiguration); + } + if (!options.getAttachedMode()) { + LOG.warn("Detached mode is not only supported in [auto]"); + } + } else { + int defaultPort = flinkConfiguration.get(RestOptions.PORT); + HostAndPort hostAndPort = HostAndPort.fromString(masterUrl).withDefaultPort(defaultPort); + flinkConfiguration.set(RestOptions.PORT, hostAndPort.getPort()); + final SavepointRestoreSettings savepointRestoreSettings; + if (options.getSavepointPath() != null) { + savepointRestoreSettings = + SavepointRestoreSettings.forPath( + options.getSavepointPath(), options.getAllowNonRestoredState()); + } else { + savepointRestoreSettings = SavepointRestoreSettings.none(); + } + if (!options.getAttachedMode()) { + flinkConfiguration.set(DeploymentOptions.ATTACHED, options.getAttachedMode()); + } + flinkStreamEnv = + new RemoteStreamEnvironment( + hostAndPort.getHost(), + hostAndPort.getPort(), + flinkConfiguration, + filesToStage.toArray(new String[filesToStage.size()]), + null, + savepointRestoreSettings); + LOG.info("Using Flink Master URL {}:{}.", hostAndPort.getHost(), hostAndPort.getPort()); + } + + // Set the parallelism, required by UnboundedSourceWrapper to generate consistent splits. + final int parallelism = + determineParallelism( + options.getParallelism(), flinkStreamEnv.getParallelism(), flinkConfiguration); + flinkStreamEnv.setParallelism(parallelism); + if (options.getMaxParallelism() > 0) { + flinkStreamEnv.setMaxParallelism(options.getMaxParallelism()); + } else if (!options.isStreaming()) { + // In Flink maxParallelism defines the number of keyGroups. + // (see + // https://github.com/apache/flink/blob/e9dd4683f758b463d0b5ee18e49cecef6a70c5cf/flink-runtime/src/main/java/org/apache/flink/runtime/state/KeyGroupRangeAssignment.java#L76) + // The default value (parallelism * 1.5) + // (see + // https://github.com/apache/flink/blob/e9dd4683f758b463d0b5ee18e49cecef6a70c5cf/flink-runtime/src/main/java/org/apache/flink/runtime/state/KeyGroupRangeAssignment.java#L137-L147) + // create a lot of skew so we force maxParallelism = parallelism in Batch mode. + LOG.info("Setting maxParallelism to {}", parallelism); + flinkStreamEnv.setMaxParallelism(parallelism); + } + // set parallelism in the options (required by some execution code) + options.setParallelism(parallelism); + + if (options.getObjectReuse()) { + flinkStreamEnv.getConfig().enableObjectReuse(); + } else { + flinkStreamEnv.getConfig().disableObjectReuse(); + } + + if (!options.getOperatorChaining()) { + flinkStreamEnv.disableOperatorChaining(); + } + + configureCheckpointing(options, flinkStreamEnv); + + applyLatencyTrackingInterval(flinkStreamEnv.getConfig(), options); + + if (options.getAutoWatermarkInterval() != null) { + flinkStreamEnv.getConfig().setAutoWatermarkInterval(options.getAutoWatermarkInterval()); + } + configureWebUIOptions(flinkStreamEnv.getConfig(), options.as(PipelineOptions.class)); + configureCustomKryoSerializers(flinkStreamEnv.getConfig()); + + return flinkStreamEnv; + } + + private static void configureWebUIOptions( + ExecutionConfig config, org.apache.beam.sdk.options.PipelineOptions options) { + SerializablePipelineOptions serializablePipelineOptions = + new SerializablePipelineOptions(options); + String optionsAsString = serializablePipelineOptions.toString(); + + try { + JsonNode node = mapper.readTree(optionsAsString); + JsonNode optionsNode = node.get("options"); + Map output = + Streams.stream(optionsNode.fields()) + .filter(entry -> !entry.getValue().isNull()) + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().asText())); + + config.setGlobalJobParameters(new GlobalJobParametersImpl(output)); + } catch (Exception e) { + LOG.warn("Unable to configure web ui options", e); + } + } + + private static void configureCustomKryoSerializers(ExecutionConfig config) { + SerializerConfigImpl serializerConfig = (SerializerConfigImpl) config.getSerializerConfig(); + // Force Beam schema to use JavaSerializer to fix serialization involving ImmutableMap + serializerConfig.registerTypeWithKryoSerializer( + org.apache.beam.sdk.schemas.Schema.class, + com.esotericsoftware.kryo.serializers.JavaSerializer.class); + } + + private static class GlobalJobParametersImpl extends ExecutionConfig.GlobalJobParameters { + private final Map jobOptions; + + private GlobalJobParametersImpl(Map jobOptions) { + this.jobOptions = jobOptions; + } + + @Override + public Map toMap() { + return jobOptions; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GlobalJobParametersImpl)) { + return false; + } + + ExecutionConfig.GlobalJobParameters jobParams = (ExecutionConfig.GlobalJobParameters) obj; + return Maps.difference(jobParams.toMap(), this.jobOptions).areEqual(); + } + + @Override + public int hashCode() { + return Objects.hashCode(jobOptions); + } + } + + private static void configureCheckpointing( + FlinkPipelineOptions options, StreamExecutionEnvironment flinkStreamEnv) { + // A value of -1 corresponds to disabled checkpointing (see CheckpointConfig in Flink). + // If the value is not -1, then the validity checks are applied. + // By default, checkpointing is disabled. + long checkpointInterval = options.getCheckpointingInterval(); + if (checkpointInterval != -1) { + if (checkpointInterval < 1) { + throw new IllegalArgumentException("The checkpoint interval must be positive"); + } + flinkStreamEnv.enableCheckpointing( + checkpointInterval, CheckpointingMode.valueOf(options.getCheckpointingMode())); + + if (options.getShutdownSourcesAfterIdleMs() == -1) { + // If not explicitly configured, we never shutdown sources when checkpointing is enabled. + options.setShutdownSourcesAfterIdleMs(Long.MAX_VALUE); + } + + if (options.getCheckpointTimeoutMillis() != -1) { + flinkStreamEnv + .getCheckpointConfig() + .setCheckpointTimeout(options.getCheckpointTimeoutMillis()); + } + + boolean externalizedCheckpoint = options.isExternalizedCheckpointsEnabled(); + boolean retainOnCancellation = options.getRetainExternalizedCheckpointsOnCancellation(); + if (externalizedCheckpoint) { + flinkStreamEnv + .getCheckpointConfig() + .setExternalizedCheckpointRetention( + retainOnCancellation + ? ExternalizedCheckpointRetention.RETAIN_ON_CANCELLATION + : ExternalizedCheckpointRetention.DELETE_ON_CANCELLATION); + } + + if (options.getUnalignedCheckpointEnabled()) { + flinkStreamEnv.getCheckpointConfig().enableUnalignedCheckpoints(); + } + flinkStreamEnv + .getCheckpointConfig() + .setForceUnalignedCheckpoints(options.getForceUnalignedCheckpointEnabled()); + + long minPauseBetweenCheckpoints = options.getMinPauseBetweenCheckpoints(); + if (minPauseBetweenCheckpoints != -1) { + flinkStreamEnv + .getCheckpointConfig() + .setMinPauseBetweenCheckpoints(minPauseBetweenCheckpoints); + } + if (options.getTolerableCheckpointFailureNumber() != null + && options.getTolerableCheckpointFailureNumber() > 0) { + flinkStreamEnv + .getCheckpointConfig() + .setTolerableCheckpointFailureNumber(options.getTolerableCheckpointFailureNumber()); + } + + flinkStreamEnv + .getCheckpointConfig() + .setMaxConcurrentCheckpoints(options.getNumConcurrentCheckpoints()); + } else { + if (options.getShutdownSourcesAfterIdleMs() == -1) { + // If not explicitly configured, we never shutdown sources when checkpointing is disabled. + options.setShutdownSourcesAfterIdleMs(0L); + } + } + } + + private static void configureStateBackend(FlinkPipelineOptions options, Configuration config) { + if (options.getStateBackend() != null) { + final String storagePath = options.getStateBackendStoragePath(); + Preconditions.checkArgument( + storagePath != null, + "State backend was set to '%s' but no storage path was provided.", + options.getStateBackend()); + + if (options.getStateBackend().equalsIgnoreCase("rocksdb")) { + config.set(StateBackendOptions.STATE_BACKEND, "rocksdb"); + } else if (options.getStateBackend().equalsIgnoreCase("filesystem") + || options.getStateBackend().equalsIgnoreCase("hashmap")) { + config.set(StateBackendOptions.STATE_BACKEND, "hashmap"); + } else { + throw new IllegalArgumentException( + String.format( + "Unknown state backend '%s'. Use 'rocksdb' or 'filesystem' or configure via Flink config file.", + options.getStateBackend())); + } + config.set(CheckpointingOptions.CHECKPOINTS_DIRECTORY, storagePath); + } else if (options.getStateBackendFactory() != null) { + // Legacy way of setting the state backend + config.set(StateBackendOptions.STATE_BACKEND, options.getStateBackendFactory().getName()); + } + } + + private static void configureRestartStrategy(FlinkPipelineOptions options, Configuration config) { + // for the following 2 parameters, a value of -1 means that Flink will use + // the default values as specified in the configuration. + int numRetries = options.getNumberOfExecutionRetries(); + if (numRetries != -1) { + // setNumberOfExecutionRetries + config.set(RestartStrategyOptions.RESTART_STRATEGY, "fixed-delay"); + config.set(RestartStrategyOptions.RESTART_STRATEGY_FIXED_DELAY_ATTEMPTS, numRetries); + } + long retryDelay = options.getExecutionRetryDelay(); + if (retryDelay != -1) { + config.set( + RestartStrategyOptions.RESTART_STRATEGY_FIXED_DELAY_DELAY, + java.time.Duration.ofMillis(retryDelay)); + } + } + + /** + * Removes the http:// or https:// schema from a url string. This is commonly used with the + * flink_master address which is expected to be of form host:port but users may specify a URL; + * Python code also assumes a URL which may be passed here. + */ + private static String stripHttpSchema(String url) { + return url.trim().replaceFirst("^http[s]?://", ""); + } + + private static int determineParallelism( + final int pipelineOptionsParallelism, + final int envParallelism, + final Configuration configuration) { + if (pipelineOptionsParallelism > 0) { + return pipelineOptionsParallelism; + } + if (envParallelism > 0) { + // If the user supplies a parallelism on the command-line, this is set on the execution + // environment during creation + return envParallelism; + } + + final int flinkConfigParallelism = + configuration.getOptional(CoreOptions.DEFAULT_PARALLELISM).orElse(-1); + if (flinkConfigParallelism > 0) { + return flinkConfigParallelism; + } + LOG.warn( + "No default parallelism could be found. Defaulting to parallelism 1. " + + "Please set an explicit parallelism with --parallelism"); + return 1; + } + + private static Configuration getFlinkConfiguration(@Nullable String flinkConfDir) { + return flinkConfDir == null || flinkConfDir.isEmpty() + ? GlobalConfiguration.loadConfiguration() + : GlobalConfiguration.loadConfiguration(flinkConfDir); + } + + private static void applyLatencyTrackingInterval( + ExecutionConfig config, FlinkPipelineOptions options) { + long latencyTrackingInterval = options.getLatencyTrackingInterval(); + config.setLatencyTrackingInterval(latencyTrackingInterval); + } + + private static void setManagedMemoryByFraction(final Configuration config) { + if (!config.containsKey("taskmanager.memory.managed.size")) { + float managedMemoryFraction = config.get(TaskManagerOptions.MANAGED_MEMORY_FRACTION); + long freeHeapMemory = EnvironmentInformation.getSizeOfFreeHeapMemoryWithDefrag(); + long managedMemorySize = (long) (freeHeapMemory * managedMemoryFraction); + config.setString("taskmanager.memory.managed.size", String.valueOf(managedMemorySize)); + } + } + + /** + * Disables classloader.check-leaked-classloader unless set by the user. See + * https://github.com/apache/beam/issues/20783. + */ + private static void disableClassLoaderLeakCheck(final Configuration config) { + if (!config.containsKey(CoreOptions.CHECK_LEAKED_CLASSLOADER.key())) { + config.set(CoreOptions.CHECK_LEAKED_CLASSLOADER, false); + } + } +} diff --git a/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperator.java b/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperator.java new file mode 100644 index 000000000000..409797625db4 --- /dev/null +++ b/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperator.java @@ -0,0 +1,1786 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.flink.translation.wrappers.streaming; + +import static org.apache.flink.util.Preconditions.checkArgument; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.Lock; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.beam.runners.core.DoFnRunner; +import org.apache.beam.runners.core.DoFnRunners; +import org.apache.beam.runners.core.InMemoryBundleFinalizer; +import org.apache.beam.runners.core.NullSideInputReader; +import org.apache.beam.runners.core.ProcessFnRunner; +import org.apache.beam.runners.core.PushbackSideInputDoFnRunner; +import org.apache.beam.runners.core.SideInputHandler; +import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.core.SimplePushbackSideInputDoFnRunner; +import org.apache.beam.runners.core.SplittableParDoViaKeyedWorkItems; +import org.apache.beam.runners.core.StateInternals; +import org.apache.beam.runners.core.StateNamespace; +import org.apache.beam.runners.core.StateNamespaces.WindowNamespace; +import org.apache.beam.runners.core.StatefulDoFnRunner; +import org.apache.beam.runners.core.StepContext; +import org.apache.beam.runners.core.TimerInternals; +import org.apache.beam.runners.core.TimerInternals.TimerData; +import org.apache.beam.runners.core.construction.SerializablePipelineOptions; +import org.apache.beam.runners.flink.FlinkPipelineOptions; +import org.apache.beam.runners.flink.adapter.FlinkKey; +import org.apache.beam.runners.flink.metrics.DoFnRunnerWithMetricsUpdate; +import org.apache.beam.runners.flink.metrics.FlinkMetricContainer; +import org.apache.beam.runners.flink.translation.types.CoderTypeSerializer; +import org.apache.beam.runners.flink.translation.utils.CheckpointStats; +import org.apache.beam.runners.flink.translation.utils.Workarounds; +import org.apache.beam.runners.flink.translation.wrappers.streaming.stableinput.BufferingDoFnRunner; +import org.apache.beam.runners.flink.translation.wrappers.streaming.state.FlinkBroadcastStateInternals; +import org.apache.beam.runners.flink.translation.wrappers.streaming.state.FlinkStateInternals; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.StructuredCoder; +import org.apache.beam.sdk.coders.VarIntCoder; +import org.apache.beam.sdk.io.FileSystems; +import org.apache.beam.sdk.metrics.MetricName; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.state.StateSpec; +import org.apache.beam.sdk.state.TimeDomain; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFn.BundleFinalizer; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.join.RawUnionValue; +import org.apache.beam.sdk.transforms.reflect.DoFnInvoker; +import org.apache.beam.sdk.transforms.reflect.DoFnInvokers; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature; +import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.util.NoopLock; +import org.apache.beam.sdk.util.WindowedValueMultiReceiver; +import org.apache.beam.sdk.util.WindowedValueReceiver; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollectionView; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Joiner; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; +import org.apache.flink.api.common.operators.ProcessingTimeService.ProcessingTimeCallback; +import org.apache.flink.api.common.state.ListState; +import org.apache.flink.api.common.state.ListStateDescriptor; +import org.apache.flink.api.common.state.MapState; +import org.apache.flink.api.common.state.MapStateDescriptor; +import org.apache.flink.api.common.typeutils.base.StringSerializer; +import org.apache.flink.api.java.functions.KeySelector; +import org.apache.flink.core.execution.CheckpointingMode; +import org.apache.flink.runtime.state.InternalPriorityQueue; +import org.apache.flink.runtime.state.KeyedStateBackend; +import org.apache.flink.runtime.state.OperatorStateBackend; +import org.apache.flink.runtime.state.StateInitializationContext; +import org.apache.flink.runtime.state.StateSnapshotContext; +import org.apache.flink.streaming.api.graph.StreamConfig; +import org.apache.flink.streaming.api.operators.AbstractStreamOperator; +import org.apache.flink.streaming.api.operators.InternalTimeServiceManagerImpl; +import org.apache.flink.streaming.api.operators.InternalTimer; +import org.apache.flink.streaming.api.operators.InternalTimerService; +import org.apache.flink.streaming.api.operators.InternalTimerServiceImpl; +import org.apache.flink.streaming.api.operators.OneInputStreamOperator; +import org.apache.flink.streaming.api.operators.Output; +import org.apache.flink.streaming.api.operators.Triggerable; +import org.apache.flink.streaming.api.operators.TwoInputStreamOperator; +import org.apache.flink.streaming.api.operators.sorted.state.BatchExecutionInternalTimeService; +import org.apache.flink.streaming.api.operators.sorted.state.BatchExecutionInternalTimeServiceManager; +import org.apache.flink.streaming.api.watermark.Watermark; +import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; +import org.apache.flink.streaming.runtime.tasks.ProcessingTimeService; +import org.apache.flink.streaming.runtime.tasks.StreamTask; +import org.apache.flink.util.OutputTag; +import org.apache.flink.util.function.BiConsumerWithException; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Flink operator for executing {@link DoFn DoFns}. + * + * @param the input type of the {@link DoFn} + * @param the output type of the {@link DoFn} + */ +// We use Flink's lifecycle methods to initialize transient fields +@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") +@SuppressWarnings({ + "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) + "keyfor", + "nullness" +}) // TODO(https://github.com/apache/beam/issues/20497) +public class DoFnOperator + extends AbstractStreamOperator> + implements OneInputStreamOperator, WindowedValue>, + TwoInputStreamOperator, RawUnionValue, WindowedValue>, + Triggerable { + + private static final Logger LOG = LoggerFactory.getLogger(DoFnOperator.class); + private final boolean isStreaming; + + protected DoFn doFn; + + protected final SerializablePipelineOptions serializedOptions; + + protected final TupleTag mainOutputTag; + protected final List> additionalOutputTags; + + protected final Collection> sideInputs; + protected final Map> sideInputTagMapping; + + protected final WindowingStrategy windowingStrategy; + + protected final OutputManagerFactory outputManagerFactory; + + protected transient DoFnRunner doFnRunner; + protected transient PushbackSideInputDoFnRunner pushbackDoFnRunner; + protected transient BufferingDoFnRunner bufferingDoFnRunner; + + protected transient SideInputHandler sideInputHandler; + + protected transient SideInputReader sideInputReader; + + protected transient BufferedOutputManager outputManager; + + private transient DoFnInvoker doFnInvoker; + + protected transient FlinkStateInternals keyedStateInternals; + protected transient FlinkTimerInternals timerInternals; + + protected final String stepName; + + final Coder> windowedInputCoder; + + final Map, Coder> outputCoders; + + final Coder keyCoder; + + final KeySelector, ?> keySelector; + + final TimerInternals.TimerDataCoderV2 timerCoder; + + /** Max number of elements to include in a bundle. */ + private final long maxBundleSize; + /** Max duration of a bundle. */ + private final long maxBundleTimeMills; + + private final DoFnSchemaInformation doFnSchemaInformation; + + private final Map> sideInputMapping; + + /** If true, we must process elements only after a checkpoint is finished. */ + final boolean requiresStableInput; + + /** + * If both requiresStableInput and this parameter are true, we must flush the buffer during drain + * operation. + */ + final boolean enableStableInputDrain; + + final int numConcurrentCheckpoints; + + private final boolean usesOnWindowExpiration; + + private final boolean finishBundleBeforeCheckpointing; + + /** Stores new finalizations being gathered. */ + private transient InMemoryBundleFinalizer bundleFinalizer; + /** Pending bundle finalizations which have not been acknowledged yet. */ + private transient LinkedHashMap> + pendingFinalizations; + /** + * Keep a maximum of 32 bundle finalizations for {@link + * BundleFinalizer.Callback#onBundleSuccess()}. + */ + private static final int MAX_NUMBER_PENDING_BUNDLE_FINALIZATIONS = 32; + + protected transient InternalTimerService timerService; + // Flink 1.20 moved timeServiceManager to protected scope. No longer need delegate + // private transient InternalTimeServiceManager timeServiceManager; + + private transient PushedBackElementsHandler> pushedBackElementsHandler; + + /** Metrics container for reporting Beam metrics to Flink (null if metrics are disabled). */ + transient @Nullable FlinkMetricContainer flinkMetricContainer; + + /** Helper class to report the checkpoint duration. */ + private transient @Nullable CheckpointStats checkpointStats; + + /** A timer that finishes the current bundle after a fixed amount of time. */ + private transient ScheduledFuture checkFinishBundleTimer; + + /** + * This and the below fields need to be volatile because we use multiple threads to access these. + * (a) the main processing thread (b) a timer thread to finish bundles by a timeout instead of the + * number of element However, we do not need a lock because Flink makes sure to acquire the + * "checkpointing" lock for the main processing but also for timer set via its {@code + * timerService}. + * + *

      The volatile flag can be removed once https://issues.apache.org/jira/browse/FLINK-12481 has + * been addressed. + */ + private transient volatile boolean bundleStarted; + /** Number of processed elements in the current bundle. */ + private transient volatile long elementCount; + /** Time that the last bundle was finished (to set the timer). */ + private transient volatile long lastFinishBundleTime; + /** Callback to be executed before the current bundle is started. */ + private transient volatile Runnable preBundleCallback; + /** Callback to be executed after the current bundle was finished. */ + private transient volatile Runnable bundleFinishedCallback; + + // Watermark state. + // Volatile because these can be set in two mutually exclusive threads (see above). + private transient volatile long currentInputWatermark; + private transient volatile long currentSideInputWatermark; + private transient volatile long currentOutputWatermark; + private transient volatile long pushedBackWatermark; + + /** Constructor for DoFnOperator. */ + public DoFnOperator( + @Nullable DoFn doFn, + String stepName, + Coder> inputWindowedCoder, + Map, Coder> outputCoders, + TupleTag mainOutputTag, + List> additionalOutputTags, + OutputManagerFactory outputManagerFactory, + WindowingStrategy windowingStrategy, + Map> sideInputTagMapping, + Collection> sideInputs, + PipelineOptions options, + @Nullable Coder keyCoder, + @Nullable KeySelector, ?> keySelector, + DoFnSchemaInformation doFnSchemaInformation, + Map> sideInputMapping) { + this.doFn = doFn; + this.stepName = stepName; + this.windowedInputCoder = inputWindowedCoder; + this.outputCoders = outputCoders; + this.mainOutputTag = mainOutputTag; + this.additionalOutputTags = additionalOutputTags; + this.sideInputTagMapping = sideInputTagMapping; + this.sideInputs = sideInputs; + this.serializedOptions = new SerializablePipelineOptions(options); + this.isStreaming = serializedOptions.get().as(FlinkPipelineOptions.class).isStreaming(); + this.windowingStrategy = windowingStrategy; + this.outputManagerFactory = outputManagerFactory; + + // API removed in Flink 2.0. setChainingStrategy is now set internally. + // setChainingStrategy(ChainingStrategy.ALWAYS); + + this.keyCoder = keyCoder; + this.keySelector = keySelector; + + this.timerCoder = + TimerInternals.TimerDataCoderV2.of(windowingStrategy.getWindowFn().windowCoder()); + + FlinkPipelineOptions flinkOptions = options.as(FlinkPipelineOptions.class); + + this.maxBundleSize = flinkOptions.getMaxBundleSize(); + Preconditions.checkArgument(maxBundleSize > 0, "Bundle size must be at least 1"); + this.maxBundleTimeMills = flinkOptions.getMaxBundleTimeMills(); + Preconditions.checkArgument(maxBundleTimeMills > 0, "Bundle time must be at least 1"); + this.doFnSchemaInformation = doFnSchemaInformation; + this.sideInputMapping = sideInputMapping; + + this.requiresStableInput = isRequiresStableInput(doFn); + + this.usesOnWindowExpiration = + doFn != null && DoFnSignatures.getSignature(doFn.getClass()).onWindowExpiration() != null; + + if (requiresStableInput) { + Preconditions.checkState( + CheckpointingMode.valueOf(flinkOptions.getCheckpointingMode()) + == CheckpointingMode.EXACTLY_ONCE, + "Checkpointing mode is not set to exactly once but @RequiresStableInput is used."); + Preconditions.checkState( + flinkOptions.getCheckpointingInterval() > 0, + "No checkpointing configured but pipeline uses @RequiresStableInput"); + LOG.warn( + "Enabling stable input for transform {}. Will only process elements at most every {} milliseconds.", + stepName, + flinkOptions.getCheckpointingInterval() + + Math.max(0, flinkOptions.getMinPauseBetweenCheckpoints())); + } + + this.enableStableInputDrain = flinkOptions.getEnableStableInputDrain(); + + this.numConcurrentCheckpoints = flinkOptions.getNumConcurrentCheckpoints(); + + this.finishBundleBeforeCheckpointing = flinkOptions.getFinishBundleBeforeCheckpointing(); + } + + private boolean isRequiresStableInput(DoFn doFn) { + // WindowDoFnOperator does not use a DoFn + return doFn != null + && DoFnSignatures.getSignature(doFn.getClass()).processElement().requiresStableInput(); + } + + @VisibleForTesting + boolean getRequiresStableInput() { + return requiresStableInput; + } + + // allow overriding this in WindowDoFnOperator because this one dynamically creates + // the DoFn + protected DoFn getDoFn() { + return doFn; + } + + protected Iterable> preProcess(WindowedValue input) { + // Assume Input is PreInputT + return Collections.singletonList((WindowedValue) input); + } + + // allow overriding this, for example SplittableDoFnOperator will not create a + // stateful DoFn runner because ProcessFn, which is used for executing a Splittable DoFn + // doesn't play by the normal DoFn rules and WindowDoFnOperator uses LateDataDroppingDoFnRunner + protected DoFnRunner createWrappingDoFnRunner( + DoFnRunner wrappedRunner, StepContext stepContext) { + + if (keyCoder != null) { + StatefulDoFnRunner.CleanupTimer cleanupTimer = + new StatefulDoFnRunner.TimeInternalsCleanupTimer( + timerInternals, windowingStrategy) { + @Override + public void setForWindow(InputT input, BoundedWindow window) { + if (!window.equals(GlobalWindow.INSTANCE) || usesOnWindowExpiration) { + // Skip setting a cleanup timer for the global window as these timers + // lead to potentially unbounded state growth in the runner, depending on key + // cardinality. Cleanup for global window will be performed upon arrival of the + // final watermark. + // In the case of OnWindowExpiration, we still set the timer. + super.setForWindow(input, window); + } + } + }; + + // we don't know the window type + // @SuppressWarnings({"unchecked", "rawtypes"}) + Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); + + @SuppressWarnings({"unchecked"}) + StatefulDoFnRunner.StateCleaner stateCleaner = + new StatefulDoFnRunner.StateInternalsStateCleaner<>( + doFn, keyedStateInternals, windowCoder); + + return DoFnRunners.defaultStatefulDoFnRunner( + doFn, + getInputCoder(), + wrappedRunner, + stepContext, + windowingStrategy, + cleanupTimer, + stateCleaner, + true /* requiresTimeSortedInput is supported */); + + } else { + return doFnRunner; + } + } + + @Override + public void setup( + StreamTask containingTask, + StreamConfig config, + Output>> output) { + + // make sure that FileSystems is initialized correctly + FileSystems.setDefaultPipelineOptions(serializedOptions.get()); + + super.setup(containingTask, config, output); + } + + protected boolean shoudBundleElements() { + return isStreaming; + } + + @Override + public void initializeState(StateInitializationContext context) throws Exception { + super.initializeState(context); + + ListStateDescriptor> pushedBackStateDescriptor = + new ListStateDescriptor<>( + "pushed-back-elements", + new CoderTypeSerializer<>(windowedInputCoder, serializedOptions)); + + if (keySelector != null) { + pushedBackElementsHandler = + KeyedPushedBackElementsHandler.create( + keySelector, getKeyedStateBackend(), pushedBackStateDescriptor); + } else { + ListState> listState = + getOperatorStateBackend().getListState(pushedBackStateDescriptor); + pushedBackElementsHandler = NonKeyedPushedBackElementsHandler.create(listState); + } + + currentInputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE.getMillis(); + currentSideInputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE.getMillis(); + currentOutputWatermark = BoundedWindow.TIMESTAMP_MIN_VALUE.getMillis(); + + sideInputReader = NullSideInputReader.of(sideInputs); + + if (!sideInputs.isEmpty()) { + + FlinkBroadcastStateInternals sideInputStateInternals = + new FlinkBroadcastStateInternals<>( + getContainingTask().getIndexInSubtaskGroup(), + getOperatorStateBackend(), + serializedOptions); + + sideInputHandler = new SideInputHandler(sideInputs, sideInputStateInternals); + sideInputReader = sideInputHandler; + + Stream> pushedBack = pushedBackElementsHandler.getElements(); + long min = + pushedBack.map(v -> v.getTimestamp().getMillis()).reduce(Long.MAX_VALUE, Math::min); + pushedBackWatermark = min; + } else { + pushedBackWatermark = Long.MAX_VALUE; + } + + // StatefulPardo or WindowDoFn + if (keyCoder != null) { + keyedStateInternals = + new FlinkStateInternals<>( + (KeyedStateBackend) getKeyedStateBackend(), + keyCoder, + windowingStrategy.getWindowFn().windowCoder(), + serializedOptions); + + if (timerService == null) { + timerService = + getInternalTimerService( + "beam-timer", new CoderTypeSerializer<>(timerCoder, serializedOptions), this); + } + + timerInternals = new FlinkTimerInternals(timerService); + Preconditions.checkNotNull(getTimeServiceManager(), "Time service manager is not set."); + } + + outputManager = + outputManagerFactory.create( + output, getLockToAcquireForStateAccessDuringBundles(), getOperatorStateBackend()); + } + + /** + * Subclasses may provide a lock to ensure that the state backend is not accessed concurrently + * during bundle execution. + */ + protected Lock getLockToAcquireForStateAccessDuringBundles() { + return NoopLock.get(); + } + + @Override + public void open() throws Exception { + // WindowDoFnOperator need use state and timer to get DoFn. + // So must wait StateInternals and TimerInternals ready. + // This will be called after initializeState() + this.doFn = getDoFn(); + + FlinkPipelineOptions options = serializedOptions.get().as(FlinkPipelineOptions.class); + doFnInvoker = DoFnInvokers.tryInvokeSetupFor(doFn, options); + + StepContext stepContext = new FlinkStepContext(); + doFnRunner = + DoFnRunners.simpleRunner( + options, + doFn, + sideInputReader, + outputManager, + mainOutputTag, + additionalOutputTags, + stepContext, + getInputCoder(), + outputCoders, + windowingStrategy, + doFnSchemaInformation, + sideInputMapping); + + doFnRunner = + createBufferingDoFnRunnerIfNeeded(createWrappingDoFnRunner(doFnRunner, stepContext)); + earlyBindStateIfNeeded(); + + if (!options.getDisableMetrics()) { + flinkMetricContainer = new FlinkMetricContainer(getRuntimeContext()); + doFnRunner = new DoFnRunnerWithMetricsUpdate<>(stepName, doFnRunner, flinkMetricContainer); + String checkpointMetricNamespace = options.getReportCheckpointDuration(); + if (checkpointMetricNamespace != null) { + MetricName checkpointMetric = + MetricName.named(checkpointMetricNamespace, "checkpoint_duration"); + checkpointStats = + new CheckpointStats( + () -> + flinkMetricContainer + .getMetricsContainer(stepName) + .getDistribution(checkpointMetric)); + } + } + + elementCount = 0L; + lastFinishBundleTime = getProcessingTimeService().getCurrentProcessingTime(); + + // Schedule timer to check timeout of finish bundle. + long bundleCheckPeriod = Math.max(maxBundleTimeMills / 2, 1); + checkFinishBundleTimer = + getProcessingTimeService() + .scheduleAtFixedRate( + timestamp -> checkInvokeFinishBundleByTime(), bundleCheckPeriod, bundleCheckPeriod); + + if (doFn instanceof SplittableParDoViaKeyedWorkItems.ProcessFn) { + pushbackDoFnRunner = + new ProcessFnRunner<>((DoFnRunner) doFnRunner, sideInputs, sideInputHandler); + } else { + pushbackDoFnRunner = + SimplePushbackSideInputDoFnRunner.create(doFnRunner, sideInputs, sideInputHandler); + } + + bundleFinalizer = new InMemoryBundleFinalizer(); + pendingFinalizations = new LinkedHashMap<>(); + } + + DoFnRunner createBufferingDoFnRunnerIfNeeded( + DoFnRunner wrappedRunner) throws Exception { + + if (requiresStableInput) { + // put this in front of the root FnRunner before any additional wrappers + return this.bufferingDoFnRunner = + BufferingDoFnRunner.create( + wrappedRunner, + "stable-input-buffer", + windowedInputCoder, + windowingStrategy.getWindowFn().windowCoder(), + getOperatorStateBackend(), + getBufferingKeyedStateBackend(), + numConcurrentCheckpoints, + serializedOptions); + } + return wrappedRunner; + } + + /** + * Retrieve a keyed state backend that should be used to buffer elements for + * {@code @RequiresStableInput} functionality. By default this is the default keyed backend, but + * can be override in {@link ExecutableStageDoFnOperator}. + * + * @return the keyed backend to use for element buffering + */ + @Nullable KeyedStateBackend getBufferingKeyedStateBackend() { + return getKeyedStateBackend(); + } + + private void earlyBindStateIfNeeded() throws IllegalArgumentException, IllegalAccessException { + if (keyCoder != null) { + if (doFn != null) { + DoFnSignature signature = DoFnSignatures.getSignature(doFn.getClass()); + FlinkStateInternals.EarlyBinder earlyBinder = + new FlinkStateInternals.EarlyBinder( + getKeyedStateBackend(), + serializedOptions, + windowingStrategy.getWindowFn().windowCoder()); + for (DoFnSignature.StateDeclaration value : signature.stateDeclarations().values()) { + StateSpec spec = + (StateSpec) signature.stateDeclarations().get(value.id()).field().get(doFn); + spec.bind(value.id(), earlyBinder); + } + if (doFnRunner instanceof StatefulDoFnRunner) { + ((StatefulDoFnRunner) doFnRunner) + .getSystemStateTags() + .forEach(tag -> tag.getSpec().bind(tag.getId(), earlyBinder)); + } + } + } + } + + void cleanUp() throws Exception { + Optional.ofNullable(flinkMetricContainer) + .ifPresent(FlinkMetricContainer::registerMetricsForPipelineResult); + Optional.ofNullable(checkFinishBundleTimer).ifPresent(timer -> timer.cancel(true)); + Workarounds.deleteStaticCaches(); + Optional.ofNullable(doFnInvoker).ifPresent(DoFnInvoker::invokeTeardown); + } + + void flushData() throws Exception { + // This is our last change to block shutdown of this operator while + // there are still remaining processing-time timers. Flink will ignore pending + // processing-time timers when upstream operators have shut down and will also + // shut down this operator with pending processing-time timers. + if (numProcessingTimeTimers() > 0) { + timerInternals.processPendingProcessingTimeTimers(); + } + if (numProcessingTimeTimers() > 0) { + throw new RuntimeException( + "There are still " + + numProcessingTimeTimers() + + " processing-time timers left, this indicates a bug"); + } + // make sure we send a +Inf watermark downstream. It can happen that we receive +Inf + // in processWatermark*() but have holds, so we have to re-evaluate here. + processWatermark(new Watermark(Long.MAX_VALUE)); + // Make sure to finish the current bundle + while (bundleStarted) { + invokeFinishBundle(); + } + if (requiresStableInput && enableStableInputDrain) { + // Flush any buffered events here before draining the pipeline. Note that this is best-effort + // and requiresStableInput contract might be violated in cases where buffer processing fails. + bufferingDoFnRunner.checkpointCompleted(Long.MAX_VALUE); + updateOutputWatermark(); + } + if (currentOutputWatermark < Long.MAX_VALUE) { + throw new RuntimeException( + String.format( + "There are still watermark holds left when terminating operator %s Watermark held %d", + getOperatorName(), currentOutputWatermark)); + } + + // sanity check: these should have been flushed out by +Inf watermarks + if (!sideInputs.isEmpty()) { + + List> pushedBackElements = + pushedBackElementsHandler.getElements().collect(Collectors.toList()); + + if (pushedBackElements.size() > 0) { + String pushedBackString = Joiner.on(",").join(pushedBackElements); + throw new RuntimeException( + "Leftover pushed-back data: " + pushedBackString + ". This indicates a bug."); + } + } + } + + @Override + public void finish() throws Exception { + try { + flushData(); + } finally { + super.finish(); + } + } + + @Override + public void close() throws Exception { + try { + cleanUp(); + } finally { + super.close(); + } + } + + protected int numProcessingTimeTimers() { + return getTimeServiceManager() + .map( + manager -> { + if (timeServiceManager instanceof InternalTimeServiceManagerImpl) { + final InternalTimeServiceManagerImpl cast = + (InternalTimeServiceManagerImpl) timeServiceManager; + return cast.numProcessingTimeTimers(); + } else if (timeServiceManager instanceof BatchExecutionInternalTimeServiceManager) { + return 0; + } else { + throw new IllegalStateException( + String.format( + "Unknown implementation of InternalTimerServiceManager. %s", + timeServiceManager)); + } + }) + .orElse(0); + } + + public long getEffectiveInputWatermark() { + // hold back by the pushed back values waiting for side inputs + long combinedPushedBackWatermark = pushedBackWatermark; + if (requiresStableInput) { + combinedPushedBackWatermark = + Math.min(combinedPushedBackWatermark, bufferingDoFnRunner.getOutputWatermarkHold()); + } + return Math.min(combinedPushedBackWatermark, currentInputWatermark); + } + + public long getCurrentOutputWatermark() { + return currentOutputWatermark; + } + + protected final void setPreBundleCallback(Runnable callback) { + this.preBundleCallback = callback; + } + + protected final void setBundleFinishedCallback(Runnable callback) { + this.bundleFinishedCallback = callback; + } + + @Override + public final void processElement(StreamRecord> streamRecord) { + for (WindowedValue e : preProcess(streamRecord.getValue())) { + checkInvokeStartBundle(); + LOG.trace("Processing element {} in {}", streamRecord.getValue().getValue(), doFn.getClass()); + long oldHold = keyCoder != null ? keyedStateInternals.minWatermarkHoldMs() : -1L; + doFnRunner.processElement(e); + checkInvokeFinishBundleByCount(); + emitWatermarkIfHoldChanged(oldHold); + } + } + + @Override + public final void processElement1(StreamRecord> streamRecord) + throws Exception { + for (WindowedValue e : preProcess(streamRecord.getValue())) { + checkInvokeStartBundle(); + Iterable> justPushedBack = + pushbackDoFnRunner.processElementInReadyWindows(e); + + long min = pushedBackWatermark; + for (WindowedValue pushedBackValue : justPushedBack) { + min = Math.min(min, pushedBackValue.getTimestamp().getMillis()); + pushedBackElementsHandler.pushBack(pushedBackValue); + } + pushedBackWatermark = min; + + checkInvokeFinishBundleByCount(); + } + } + + /** + * Add the side input value. Here we are assuming that views have already been materialized and + * are sent over the wire as {@link Iterable}. Subclasses may elect to perform materialization in + * state and receive side input incrementally instead. + * + * @param streamRecord + */ + protected void addSideInputValue(StreamRecord streamRecord) { + @SuppressWarnings("unchecked") + WindowedValue> value = + (WindowedValue>) streamRecord.getValue().getValue(); + + PCollectionView sideInput = sideInputTagMapping.get(streamRecord.getValue().getUnionTag()); + sideInputHandler.addSideInputValue(sideInput, value); + } + + @Override + public final void processElement2(StreamRecord streamRecord) throws Exception { + // we finish the bundle because the newly arrived side-input might + // make a view available that was previously not ready. + // The PushbackSideInputRunner will only reset its cache of non-ready windows when + // finishing a bundle. + invokeFinishBundle(); + checkInvokeStartBundle(); + + // add the side input, which may cause pushed back elements become eligible for processing + addSideInputValue(streamRecord); + + List> newPushedBack = new ArrayList<>(); + + Iterator> it = pushedBackElementsHandler.getElements().iterator(); + + while (it.hasNext()) { + WindowedValue element = it.next(); + // we need to set the correct key in case the operator is + // a (keyed) window operator + if (keySelector != null) { + setCurrentKey(keySelector.getKey(element)); + } + + Iterable> justPushedBack = + pushbackDoFnRunner.processElementInReadyWindows(element); + Iterables.addAll(newPushedBack, justPushedBack); + } + + pushedBackElementsHandler.clear(); + long min = Long.MAX_VALUE; + for (WindowedValue pushedBackValue : newPushedBack) { + min = Math.min(min, pushedBackValue.getTimestamp().getMillis()); + pushedBackElementsHandler.pushBack(pushedBackValue); + } + pushedBackWatermark = min; + + checkInvokeFinishBundleByCount(); + + // maybe output a new watermark + processWatermark1(new Watermark(currentInputWatermark)); + } + + @Override + public final void processWatermark(Watermark mark) throws Exception { + LOG.trace("Processing watermark {} in {}", mark.getTimestamp(), doFn.getClass()); + processWatermark1(mark); + } + + @Override + public final void processWatermark1(Watermark mark) throws Exception { + // Flush any data buffered during snapshotState(). + outputManager.flushBuffer(); + + // We do the check here because we are guaranteed to at least get the +Inf watermark on the + // main input when the job finishes. + if (currentSideInputWatermark >= BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()) { + // this means we will never see any more side input + // we also do the check here because we might have received the side-input MAX watermark + // before receiving any main-input data + emitAllPushedBackData(); + } + + currentInputWatermark = mark.getTimestamp(); + processInputWatermark(true); + } + + private void processInputWatermark(boolean advanceInputWatermark) throws Exception { + long inputWatermarkHold = applyInputWatermarkHold(getEffectiveInputWatermark()); + if (keyCoder != null && advanceInputWatermark) { + timeServiceManager.advanceWatermark(new Watermark(inputWatermarkHold)); + } + + long potentialOutputWatermark = + applyOutputWatermarkHold( + currentOutputWatermark, computeOutputWatermark(inputWatermarkHold)); + + maybeEmitWatermark(potentialOutputWatermark); + } + + /** + * Allows to apply a hold to the input watermark. By default, just passes the input watermark + * through. + */ + public long applyInputWatermarkHold(long inputWatermark) { + return inputWatermark; + } + + /** + * Allows to apply a hold to the output watermark before it is sent out. Used to apply hold on + * output watermark for delayed (asynchronous or buffered) processing. + * + * @param currentOutputWatermark the current output watermark + * @param potentialOutputWatermark The potential new output watermark which can be adjusted, if + * needed. The input watermark hold has already been applied. + * @return The new output watermark which will be emitted. + */ + public long applyOutputWatermarkHold(long currentOutputWatermark, long potentialOutputWatermark) { + return potentialOutputWatermark; + } + + private long computeOutputWatermark(long inputWatermarkHold) { + final long potentialOutputWatermark; + if (keyCoder == null) { + potentialOutputWatermark = inputWatermarkHold; + } else { + potentialOutputWatermark = + Math.min(keyedStateInternals.minWatermarkHoldMs(), inputWatermarkHold); + } + return potentialOutputWatermark; + } + + private void maybeEmitWatermark(long watermark) { + if (watermark > currentOutputWatermark) { + // Must invoke finishBatch before emit the +Inf watermark otherwise there are some late + // events. + if (watermark >= BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()) { + invokeFinishBundle(); + } + + if (bundleStarted) { + // do not update watermark in the middle of bundle, because it might cause + // user-buffered data to be emitted past watermark + return; + } + + LOG.debug("Emitting watermark {} from {}", watermark, getOperatorName()); + currentOutputWatermark = watermark; + output.emitWatermark(new Watermark(watermark)); + + // Check if the final watermark was triggered to perform state cleanup for global window + // TODO: Do we need to do this when OnWindowExpiration is set, since in that case we have a + // cleanup timer? + if (keyedStateInternals != null + && currentOutputWatermark + > adjustTimestampForFlink(GlobalWindow.INSTANCE.maxTimestamp().getMillis())) { + keyedStateInternals.clearGlobalState(); + } + } + } + + @Override + public final void processWatermark2(Watermark mark) throws Exception { + currentSideInputWatermark = mark.getTimestamp(); + if (mark.getTimestamp() >= BoundedWindow.TIMESTAMP_MAX_VALUE.getMillis()) { + // this means we will never see any more side input + emitAllPushedBackData(); + + // maybe output a new watermark + processWatermark1(new Watermark(currentInputWatermark)); + } + } + + /** + * Emits all pushed-back data. This should be used once we know that there will not be any future + * side input, i.e. that there is no point in waiting. + */ + private void emitAllPushedBackData() throws Exception { + + Iterator> it = pushedBackElementsHandler.getElements().iterator(); + + while (it.hasNext()) { + checkInvokeStartBundle(); + WindowedValue element = it.next(); + // we need to set the correct key in case the operator is + // a (keyed) window operator + setKeyContextElement1(new StreamRecord<>(element)); + + doFnRunner.processElement(element); + } + + pushedBackElementsHandler.clear(); + pushedBackWatermark = Long.MAX_VALUE; + } + + /** + * Check whether invoke startBundle, if it is, need to output elements that were buffered as part + * of finishing a bundle in snapshot() first. + * + *

      In order to avoid having {@link DoFnRunner#processElement(WindowedValue)} or {@link + * DoFnRunner#onTimer(String, String, Object, BoundedWindow, Instant, Instant, TimeDomain)} not + * between StartBundle and FinishBundle, this method needs to be called in each processElement and + * each processWatermark and onProcessingTime. Do not need to call in onEventTime, because it has + * been guaranteed in the processWatermark. + */ + private void checkInvokeStartBundle() { + if (!bundleStarted) { + // Flush any data buffered during snapshotState(). + outputManager.flushBuffer(); + LOG.debug("Starting bundle."); + if (preBundleCallback != null) { + preBundleCallback.run(); + } + pushbackDoFnRunner.startBundle(); + bundleStarted = true; + } + } + + /** Check whether invoke finishBundle by elements count. Called in processElement. */ + @SuppressWarnings("NonAtomicVolatileUpdate") + @SuppressFBWarnings("VO_VOLATILE_INCREMENT") + private void checkInvokeFinishBundleByCount() { + if (!shoudBundleElements()) { + return; + } + // We do not access this statement concurrently, but we want to make sure that each thread + // sees the latest value, which is why we use volatile. See the class field section above + // for more information. + //noinspection NonAtomicOperationOnVolatileField + elementCount++; + if (elementCount >= maxBundleSize) { + invokeFinishBundle(); + updateOutputWatermark(); + } + } + + /** Check whether invoke finishBundle by timeout. */ + private void checkInvokeFinishBundleByTime() { + if (!shoudBundleElements()) { + return; + } + long now = getProcessingTimeService().getCurrentProcessingTime(); + if (now - lastFinishBundleTime >= maxBundleTimeMills) { + invokeFinishBundle(); + scheduleForCurrentProcessingTime(ts -> updateOutputWatermark()); + } + } + + @SuppressWarnings("FutureReturnValueIgnored") + protected void scheduleForCurrentProcessingTime(ProcessingTimeCallback callback) { + // We are scheduling a timer for advancing the watermark, to not delay finishing the bundle + // and temporarily release the checkpoint lock. Otherwise, we could potentially loop when a + // timer keeps scheduling a timer for the same timestamp. + ProcessingTimeService timeService = getProcessingTimeService(); + timeService.registerTimer(timeService.getCurrentProcessingTime(), callback); + } + + void updateOutputWatermark() { + try { + processInputWatermark(false); + } catch (Exception ex) { + failBundleFinalization(ex); + } + } + + protected final void invokeFinishBundle() { + long previousBundleFinishTime = lastFinishBundleTime; + if (bundleStarted) { + LOG.debug("Finishing bundle."); + pushbackDoFnRunner.finishBundle(); + LOG.debug("Finished bundle. Element count: {}", elementCount); + elementCount = 0L; + lastFinishBundleTime = getProcessingTimeService().getCurrentProcessingTime(); + bundleStarted = false; + // callback only after current bundle was fully finalized + // it could start a new bundle, for example resulting from timer processing + if (bundleFinishedCallback != null) { + LOG.debug("Invoking bundle finish callback."); + bundleFinishedCallback.run(); + } + } + try { + if (previousBundleFinishTime - getProcessingTimeService().getCurrentProcessingTime() + > maxBundleTimeMills) { + processInputWatermark(false); + } + } catch (Exception ex) { + LOG.warn("Failed to update downstream watermark", ex); + } + } + + @Override + public void prepareSnapshotPreBarrier(long checkpointId) { + if (finishBundleBeforeCheckpointing) { + // We finish the bundle and flush any pending data. + // This avoids buffering any data as part of snapshotState() below. + while (bundleStarted) { + invokeFinishBundle(); + } + updateOutputWatermark(); + } + } + + @Override + public void snapshotState(StateSnapshotContext context) throws Exception { + if (checkpointStats != null) { + checkpointStats.snapshotStart(context.getCheckpointId()); + } + + if (requiresStableInput) { + // We notify the BufferingDoFnRunner to associate buffered state with this + // snapshot id and start a new buffer for elements arriving after this snapshot. + bufferingDoFnRunner.checkpoint(context.getCheckpointId()); + } + + int diff = pendingFinalizations.size() - MAX_NUMBER_PENDING_BUNDLE_FINALIZATIONS; + if (diff >= 0) { + for (Iterator iterator = pendingFinalizations.keySet().iterator(); diff >= 0; diff--) { + iterator.next(); + iterator.remove(); + } + } + pendingFinalizations.put(context.getCheckpointId(), bundleFinalizer.getAndClearFinalizations()); + + try { + outputManager.openBuffer(); + // Ensure that no new bundle gets started as part of finishing a bundle + while (bundleStarted) { + invokeFinishBundle(); + } + outputManager.closeBuffer(); + } catch (Exception e) { + failBundleFinalization(e); + } + + super.snapshotState(context); + } + + private void failBundleFinalization(Exception e) { + // https://jira.apache.org/jira/browse/FLINK-14653 + // Any regular exception during checkpointing will be tolerated by Flink because those + // typically do not affect the execution flow. We need to fail hard here because errors + // in bundle execution are application errors which are not related to checkpointing. + throw new Error("Checkpointing failed because bundle failed to finalize.", e); + } + + public BundleFinalizer getBundleFinalizer() { + return bundleFinalizer; + } + + @Override + public void notifyCheckpointComplete(long checkpointId) throws Exception { + if (checkpointStats != null) { + checkpointStats.reportCheckpointDuration(checkpointId); + } + + if (requiresStableInput) { + // We can now release all buffered data which was held back for + // @RequiresStableInput guarantees. + bufferingDoFnRunner.checkpointCompleted(checkpointId); + updateOutputWatermark(); + } + + List finalizations = + pendingFinalizations.remove(checkpointId); + if (finalizations != null) { + // confirm all finalizations that were associated with the checkpoint + for (InMemoryBundleFinalizer.Finalization finalization : finalizations) { + finalization.getCallback().onBundleSuccess(); + } + } + + super.notifyCheckpointComplete(checkpointId); + } + + @Override + public void onEventTime(InternalTimer timer) { + checkInvokeStartBundle(); + fireTimerInternal(timer.getKey(), timer.getNamespace()); + } + + @Override + public void onProcessingTime(InternalTimer timer) { + checkInvokeStartBundle(); + fireTimerInternal(timer.getKey(), timer.getNamespace()); + } + + // allow overriding this in ExecutableStageDoFnOperator to set the key context + protected void fireTimerInternal(FlinkKey key, TimerData timerData) { + long oldHold = keyCoder != null ? keyedStateInternals.minWatermarkHoldMs() : -1L; + fireTimer(timerData); + emitWatermarkIfHoldChanged(oldHold); + } + + void emitWatermarkIfHoldChanged(long currentWatermarkHold) { + if (keyCoder != null) { + long newWatermarkHold = keyedStateInternals.minWatermarkHoldMs(); + if (newWatermarkHold > currentWatermarkHold) { + try { + processInputWatermark(false); + } catch (Exception ex) { + // should not happen + throw new IllegalStateException(ex); + } + } + } + } + + // allow overriding this in WindowDoFnOperator + protected void fireTimer(TimerData timerData) { + LOG.debug( + "Firing timer: {} at {} with output time {}", + timerData.getTimerId(), + timerData.getTimestamp().getMillis(), + timerData.getOutputTimestamp().getMillis()); + StateNamespace namespace = timerData.getNamespace(); + // This is a user timer, so namespace must be WindowNamespace + checkArgument(namespace instanceof WindowNamespace); + BoundedWindow window = ((WindowNamespace) namespace).getWindow(); + timerInternals.onFiredOrDeletedTimer(timerData); + + pushbackDoFnRunner.onTimer( + timerData.getTimerId(), + timerData.getTimerFamilyId(), + keyedStateInternals.getKey(), + window, + timerData.getTimestamp(), + timerData.getOutputTimestamp(), + timerData.getDomain(), + timerData.causedByDrain()); + } + + @SuppressWarnings("unchecked") + Coder getInputCoder() { + return (Coder) Iterables.getOnlyElement(windowedInputCoder.getCoderArguments()); + } + + /** Factory for creating an {@link BufferedOutputManager} from a Flink {@link Output}. */ + interface OutputManagerFactory extends Serializable { + BufferedOutputManager create( + Output>> output, + Lock bufferLock, + OperatorStateBackend operatorStateBackend) + throws Exception; + } + + /** + * A {@link WindowedValueReceiver} that can buffer its outputs. Uses {@link + * PushedBackElementsHandler} to buffer the data. Buffering data is necessary because no elements + * can be emitted during {@code snapshotState} which is called when the checkpoint barrier already + * has been sent downstream. Emitting elements would break the flow of checkpoint barrier and + * violate exactly-once semantics. + * + *

      This buffering can be deactived using {@code + * FlinkPipelineOptions#setFinishBundleBeforeCheckpointing(true)}. If activated, we flush out + * bundle data before the barrier is sent downstream. This is done via {@code + * prepareSnapshotPreBarrier}. When Flink supports unaligned checkpoints, this should become the + * default and this class should be removed as in https://github.com/apache/beam/pull/9652. + */ + public static class BufferedOutputManager implements WindowedValueMultiReceiver { + + private final TupleTag mainTag; + private final Map, OutputTag>> tagsToOutputTags; + private final Map, Integer> tagsToIds; + /** + * A lock to be acquired before writing to the buffer. This lock will only be acquired during + * buffering. It will not be acquired during flushing the buffer. + */ + private final Lock bufferLock; + + private final boolean isStreaming; + + private Map> idsToTags; + /** Elements buffered during a snapshot, by output id. */ + @VisibleForTesting + final PushedBackElementsHandler>> pushedBackElementsHandler; + + protected final Output>> output; + + /** Indicates whether we are buffering data as part of snapshotState(). */ + private boolean openBuffer = false; + /** For performance, to avoid having to access the state backend when the buffer is empty. */ + private boolean bufferIsEmpty = false; + + BufferedOutputManager( + Output>> output, + TupleTag mainTag, + Map, OutputTag>> tagsToOutputTags, + Map, Integer> tagsToIds, + Lock bufferLock, + PushedBackElementsHandler>> pushedBackElementsHandler, + boolean isStreaming) { + this.output = output; + this.mainTag = mainTag; + this.tagsToOutputTags = tagsToOutputTags; + this.tagsToIds = tagsToIds; + this.bufferLock = bufferLock; + this.idsToTags = new HashMap<>(); + for (Map.Entry, Integer> entry : tagsToIds.entrySet()) { + idsToTags.put(entry.getValue(), entry.getKey()); + } + this.pushedBackElementsHandler = pushedBackElementsHandler; + this.isStreaming = isStreaming; + } + + void openBuffer() { + this.openBuffer = true; + } + + void closeBuffer() { + this.openBuffer = false; + } + + @Override + public void output(TupleTag tag, WindowedValue value) { + // Don't buffer elements in Batch mode + if (!openBuffer || !isStreaming) { + emit(tag, value); + } else { + buffer(KV.of(tagsToIds.get(tag), value)); + } + } + + private void buffer(KV> taggedValue) { + bufferLock.lock(); + try { + pushedBackElementsHandler.pushBack(taggedValue); + } catch (Exception e) { + throw new RuntimeException("Couldn't pushback element.", e); + } finally { + bufferLock.unlock(); + bufferIsEmpty = false; + } + } + + /** + * Flush elements of bufferState to Flink Output. This method should not be invoked in {@link + * #snapshotState(StateSnapshotContext)} because the checkpoint barrier has already been sent + * downstream; emitting elements at this point would violate the checkpoint barrier alignment. + * + *

      The buffer should be flushed before starting a new bundle when the buffer cannot be + * concurrently accessed and thus does not need to be guarded by a lock. + */ + void flushBuffer() { + if (openBuffer || bufferIsEmpty) { + // Checkpoint currently in progress or nothing buffered, do not proceed + return; + } + try { + pushedBackElementsHandler + .getElements() + .forEach( + element -> + emit(idsToTags.get(element.getKey()), (WindowedValue) element.getValue())); + pushedBackElementsHandler.clear(); + bufferIsEmpty = true; + } catch (Exception e) { + throw new RuntimeException("Couldn't flush pushed back elements.", e); + } + } + + private void emit(TupleTag tag, WindowedValue value) { + if (tag.equals(mainTag)) { + // with tagged outputs we can't get around this because we don't + // know our own output type... + @SuppressWarnings("unchecked") + WindowedValue castValue = (WindowedValue) value; + output.collect(new StreamRecord<>(castValue)); + } else { + @SuppressWarnings("unchecked") + OutputTag> outputTag = (OutputTag) tagsToOutputTags.get(tag); + output.collect(outputTag, new StreamRecord<>(value)); + } + } + } + + /** Coder for KV of id and value. It will be serialized in Flink checkpoint. */ + private static class TaggedKvCoder extends StructuredCoder>> { + + private final Map>> idsToCoders; + + TaggedKvCoder(Map>> idsToCoders) { + this.idsToCoders = idsToCoders; + } + + @Override + public void encode(KV> kv, OutputStream out) throws IOException { + Coder> coder = idsToCoders.get(kv.getKey()); + VarIntCoder.of().encode(kv.getKey(), out); + coder.encode(kv.getValue(), out); + } + + @Override + public KV> decode(InputStream in) throws IOException { + Integer id = VarIntCoder.of().decode(in); + Coder> coder = idsToCoders.get(id); + WindowedValue value = coder.decode(in); + return KV.of(id, value); + } + + @Override + public List> getCoderArguments() { + return new ArrayList<>(idsToCoders.values()); + } + + @Override + public void verifyDeterministic() throws NonDeterministicException { + for (Coder coder : idsToCoders.values()) { + verifyDeterministic(this, "Coder must be deterministic", coder); + } + } + } + + /** + * Implementation of {@link OutputManagerFactory} that creates an {@link BufferedOutputManager} + * that can write to multiple logical outputs by Flink side output. + */ + public static class MultiOutputOutputManagerFactory + implements OutputManagerFactory { + + private final TupleTag mainTag; + private final Map, Integer> tagsToIds; + private final Map, OutputTag>> tagsToOutputTags; + private final Map, Coder>> tagsToCoders; + private final SerializablePipelineOptions pipelineOptions; + private final boolean isStreaming; + + // There is no side output. + @SuppressWarnings("unchecked") + public MultiOutputOutputManagerFactory( + TupleTag mainTag, + Coder> mainCoder, + SerializablePipelineOptions pipelineOptions) { + this( + mainTag, + new HashMap<>(), + ImmutableMap., Coder>>builder() + .put(mainTag, (Coder) mainCoder) + .build(), + ImmutableMap., Integer>builder().put(mainTag, 0).build(), + pipelineOptions); + } + + public MultiOutputOutputManagerFactory( + TupleTag mainTag, + Map, OutputTag>> tagsToOutputTags, + Map, Coder>> tagsToCoders, + Map, Integer> tagsToIds, + SerializablePipelineOptions pipelineOptions) { + this.mainTag = mainTag; + this.tagsToOutputTags = tagsToOutputTags; + this.tagsToCoders = tagsToCoders; + this.tagsToIds = tagsToIds; + this.pipelineOptions = pipelineOptions; + this.isStreaming = pipelineOptions.get().as(FlinkPipelineOptions.class).isStreaming(); + } + + @Override + public BufferedOutputManager create( + Output>> output, + Lock bufferLock, + OperatorStateBackend operatorStateBackend) + throws Exception { + Preconditions.checkNotNull(output); + Preconditions.checkNotNull(bufferLock); + Preconditions.checkNotNull(operatorStateBackend); + + TaggedKvCoder taggedKvCoder = buildTaggedKvCoder(); + ListStateDescriptor>> taggedOutputPushbackStateDescriptor = + new ListStateDescriptor<>( + "bundle-buffer-tag", new CoderTypeSerializer<>(taggedKvCoder, pipelineOptions)); + ListState>> listStateBuffer = + operatorStateBackend.getListState(taggedOutputPushbackStateDescriptor); + PushedBackElementsHandler>> pushedBackElementsHandler = + NonKeyedPushedBackElementsHandler.create(listStateBuffer); + + return new BufferedOutputManager<>( + output, + mainTag, + tagsToOutputTags, + tagsToIds, + bufferLock, + pushedBackElementsHandler, + isStreaming); + } + + private TaggedKvCoder buildTaggedKvCoder() { + ImmutableMap.Builder>> idsToCodersBuilder = + ImmutableMap.builder(); + for (Map.Entry, Integer> entry : tagsToIds.entrySet()) { + idsToCodersBuilder.put(entry.getValue(), tagsToCoders.get(entry.getKey())); + } + return new TaggedKvCoder(idsToCodersBuilder.build()); + } + } + + /** + * {@link StepContext} for running {@link DoFn DoFns} on Flink. This does not allow accessing + * state or timer internals. + */ + protected class FlinkStepContext implements StepContext { + + @Override + public StateInternals stateInternals() { + return keyedStateInternals; + } + + @Override + public TimerInternals timerInternals() { + return timerInternals; + } + + @Override + public BundleFinalizer bundleFinalizer() { + return bundleFinalizer; + } + } + + class FlinkTimerInternals implements TimerInternals { + + private static final String PENDING_TIMERS_STATE_NAME = "pending-timers"; + + /** + * Pending Timers (=not been fired yet) by context id. The id is generated from the state + * namespace of the timer and the timer's id. Necessary for supporting removal of existing + * timers. In Flink removal of timers can only be done by providing id and time of the timer. + * + *

      CAUTION: This map is scoped by the current active key. Do not attempt to perform any + * calculations which span across keys. + */ + @VisibleForTesting final MapState pendingTimersById; + + private final InternalTimerService timerService; + + private FlinkTimerInternals(InternalTimerService timerService) throws Exception { + MapStateDescriptor pendingTimersByIdStateDescriptor = + new MapStateDescriptor<>( + PENDING_TIMERS_STATE_NAME, + new StringSerializer(), + new CoderTypeSerializer<>(timerCoder, serializedOptions)); + + this.pendingTimersById = getKeyedStateStore().getMapState(pendingTimersByIdStateDescriptor); + this.timerService = timerService; + populateOutputTimestampQueue(timerService); + } + + /** + * Processes all pending processing timers. This is intended for use during shutdown. From Flink + * 1.10 on, processing timer execution is stopped when the operator is closed. This leads to + * problems for applications which assume all pending timers will be completed. Although Flink + * does drain the remaining timers after close(), this is not sufficient because no new timers + * are allowed to be scheduled anymore. This breaks Beam pipelines which rely on all processing + * timers to be scheduled and executed. + */ + void processPendingProcessingTimeTimers() { + final KeyedStateBackend keyedStateBackend = getKeyedStateBackend(); + final InternalPriorityQueue> processingTimeTimersQueue = + Workarounds.retrieveInternalProcessingTimerQueue(timerService); + + InternalTimer internalTimer; + while ((internalTimer = processingTimeTimersQueue.poll()) != null) { + keyedStateBackend.setCurrentKey(internalTimer.getKey()); + TimerData timer = internalTimer.getNamespace(); + checkInvokeStartBundle(); + fireTimerInternal((FlinkKey) internalTimer.getKey(), timer); + } + } + + private void populateOutputTimestampQueue(InternalTimerService timerService) + throws Exception { + + BiConsumerWithException consumer = + (timerData, stamp) -> + keyedStateInternals.addWatermarkHoldUsage(timerData.getOutputTimestamp()); + if (timerService instanceof InternalTimerServiceImpl) { + timerService.forEachEventTimeTimer(consumer); + timerService.forEachProcessingTimeTimer(consumer); + } + } + + private String constructTimerId(String timerFamilyId, String timerId) { + return timerFamilyId + "+" + timerId; + } + + @Override + public void setTimer( + StateNamespace namespace, + String timerId, + String timerFamilyId, + Instant target, + Instant outputTimestamp, + TimeDomain timeDomain) { + setTimer( + TimerData.of(timerId, timerFamilyId, namespace, target, outputTimestamp, timeDomain)); + } + + /** + * @deprecated use {@link #setTimer(StateNamespace, String, String, Instant, Instant, + * TimeDomain)}. + */ + @Deprecated + @Override + public void setTimer(TimerData timer) { + try { + LOG.debug( + "Setting timer: {} at {} with output time {}", + timer.getTimerId(), + timer.getTimestamp().getMillis(), + timer.getOutputTimestamp().getMillis()); + String contextTimerId = + getContextTimerId( + constructTimerId(timer.getTimerFamilyId(), timer.getTimerId()), + timer.getNamespace()); + @Nullable final TimerData oldTimer = pendingTimersById.get(contextTimerId); + if (!timer.equals(oldTimer)) { + // Only one timer can exist at a time for a given timer id and context. + // If a timer gets set twice in the same context, the second must + // override the first. Thus, we must cancel any pending timers + // before we set the new one. + cancelPendingTimer(oldTimer); + registerTimer(timer, contextTimerId); + } + } catch (Exception e) { + throw new RuntimeException("Failed to set timer", e); + } + } + + private void registerTimer(TimerData timer, String contextTimerId) throws Exception { + LOG.debug("Registering timer {}", timer); + pendingTimersById.put(contextTimerId, timer); + long time = timer.getTimestamp().getMillis(); + switch (timer.getDomain()) { + case EVENT_TIME: + timerService.registerEventTimeTimer(timer, adjustTimestampForFlink(time)); + break; + case PROCESSING_TIME: + case SYNCHRONIZED_PROCESSING_TIME: + timerService.registerProcessingTimeTimer(timer, adjustTimestampForFlink(time)); + break; + default: + throw new UnsupportedOperationException("Unsupported time domain: " + timer.getDomain()); + } + keyedStateInternals.addWatermarkHoldUsage(timer.getOutputTimestamp()); + } + + /** + * Looks up a timer by its id. This is necessary to support canceling existing timers with the + * same id. Flink does not provide this functionality. + * + * @param contextTimerId Timer ID o cancel. + */ + private void cancelPendingTimerById(String contextTimerId) throws Exception { + cancelPendingTimer(pendingTimersById.get(contextTimerId)); + } + + /** + * Cancels a pending timer. + * + * @param timer Timer to cancel. + */ + private void cancelPendingTimer(@Nullable TimerData timer) { + if (timer != null) { + deleteTimerInternal(timer); + } + } + + /** + * Hook which must be called when a timer is fired or deleted to perform cleanup. Note: Make + * sure that the state backend key is set correctly. It is best to run this in the fireTimer() + * method. + */ + void onFiredOrDeletedTimer(TimerData timer) { + try { + pendingTimersById.remove( + getContextTimerId( + constructTimerId(timer.getTimerFamilyId(), timer.getTimerId()), + timer.getNamespace())); + keyedStateInternals.removeWatermarkHoldUsage(timer.getOutputTimestamp()); + } catch (Exception e) { + throw new RuntimeException("Failed to cleanup pending timers state.", e); + } + } + + /** @deprecated use {@link #deleteTimer(StateNamespace, String, String, TimeDomain)}. */ + @Deprecated + @Override + public void deleteTimer(StateNamespace namespace, String timerId, String timerFamilyId) { + throw new UnsupportedOperationException("Canceling of a timer by ID is not yet supported."); + } + + @Override + public void deleteTimer( + StateNamespace namespace, String timerId, String timerFamilyId, TimeDomain timeDomain) { + try { + cancelPendingTimerById(getContextTimerId(timerId, namespace)); + } catch (Exception e) { + throw new RuntimeException("Failed to cancel timer", e); + } + } + + /** @deprecated use {@link #deleteTimer(StateNamespace, String, String, TimeDomain)}. */ + @Override + @Deprecated + public void deleteTimer(TimerData timer) { + deleteTimer( + timer.getNamespace(), + constructTimerId(timer.getTimerFamilyId(), timer.getTimerId()), + timer.getTimerFamilyId(), + timer.getDomain()); + } + + void deleteTimerInternal(TimerData timer) { + long time = timer.getTimestamp().getMillis(); + switch (timer.getDomain()) { + case EVENT_TIME: + timerService.deleteEventTimeTimer(timer, adjustTimestampForFlink(time)); + break; + case PROCESSING_TIME: + case SYNCHRONIZED_PROCESSING_TIME: + timerService.deleteProcessingTimeTimer(timer, adjustTimestampForFlink(time)); + break; + default: + throw new UnsupportedOperationException("Unsupported time domain: " + timer.getDomain()); + } + onFiredOrDeletedTimer(timer); + } + + @Override + public Instant currentProcessingTime() { + return new Instant(timerService.currentProcessingTime()); + } + + @Override + public @Nullable Instant currentSynchronizedProcessingTime() { + return new Instant(timerService.currentProcessingTime()); + } + + @Override + public Instant currentInputWatermarkTime() { + if (timerService instanceof BatchExecutionInternalTimeService) { + // In batch mode, this method will only either return BoundedWindow.TIMESTAMP_MIN_VALUE, + // or BoundedWindow.TIMESTAMP_MAX_VALUE. + // + // For batch execution mode, the currentInputWatermark variable will never be updated + // until all the records are processed. However, every time when a record with a new + // key arrives, the Flink timer service watermark will be set to + // MAX_WATERMARK(LONG.MAX_VALUE) so that all the timers associated with the current + // key can fire. After that the Flink timer service watermark will be reset to + // LONG.MIN_VALUE, so the next key will start from a fresh env as if the previous + // records of a different key never existed. So the watermark is either Long.MIN_VALUE + // or long MAX_VALUE. So we should just use the Flink time service watermark in batch mode. + // + // In Flink the watermark ranges from + // [LONG.MIN_VALUE (-9223372036854775808), LONG.MAX_VALUE (9223372036854775807)] while the + // beam + // watermark range is [BoundedWindow.TIMESTAMP_MIN_VALUE (-9223372036854775), + // BoundedWindow.TIMESTAMP_MAX_VALUE (9223372036854775)]. To ensure the timestamp visible to + // the users follow the Beam convention, we just use the Beam range instead. + return timerService.currentWatermark() == Long.MAX_VALUE + ? new Instant(Long.MAX_VALUE) + : BoundedWindow.TIMESTAMP_MIN_VALUE; + } else { + return new Instant(getEffectiveInputWatermark()); + } + } + + @Override + public @Nullable Instant currentOutputWatermarkTime() { + return new Instant(currentOutputWatermark); + } + + /** + * Check whether event time timers lower or equal to the given timestamp exist. Caution: This is + * scoped by the current key. + */ + public boolean hasPendingEventTimeTimers(long maxTimestamp) throws Exception { + for (TimerData timer : pendingTimersById.values()) { + if (timer.getDomain() == TimeDomain.EVENT_TIME + && timer.getTimestamp().getMillis() <= maxTimestamp) { + return true; + } + } + return false; + } + + /** Unique contextual id of a timer. Used to look up any existing timers in a context. */ + private String getContextTimerId(String timerId, StateNamespace namespace) { + return timerId + namespace.stringKey(); + } + } + + /** + * In Beam, a timer with timestamp {@code T} is only illegible for firing when the time has moved + * past this time stamp, i.e. {@code T < current_time}. In the case of event time, current_time is + * the watermark, in the case of processing time it is the system time. + * + *

      Flink's TimerService has different semantics because it only ensures {@code T <= + * current_time}. + * + *

      To make up for this, we need to add one millisecond to Flink's internal timer timestamp. + * Note that we do not modify Beam's timestamp and we are not exposing Flink's timestamp. + * + *

      See also https://jira.apache.org/jira/browse/BEAM-3863 + */ + static long adjustTimestampForFlink(long beamTimerTimestamp) { + if (beamTimerTimestamp == Long.MAX_VALUE) { + // We would overflow, do not adjust timestamp + return Long.MAX_VALUE; + } + return beamTimerTimestamp + 1; + } +} diff --git a/runners/flink/2.2/src/test/java/org/apache/beam/runners/flink/FlinkPipelineOptionsTest.java b/runners/flink/2.2/src/test/java/org/apache/beam/runners/flink/FlinkPipelineOptionsTest.java new file mode 100644 index 000000000000..6cebadc49d5c --- /dev/null +++ b/runners/flink/2.2/src/test/java/org/apache/beam/runners/flink/FlinkPipelineOptionsTest.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.flink; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import org.apache.beam.repackaged.core.org.apache.commons.lang3.SerializationUtils; +import org.apache.beam.runners.core.construction.SerializablePipelineOptions; +import org.apache.beam.runners.flink.translation.wrappers.streaming.DoFnOperator; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.options.Default; +import org.apache.beam.sdk.options.Description; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.flink.api.common.serialization.SerializerConfigImpl; +import org.apache.flink.api.common.typeinfo.TypeHint; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.core.execution.CheckpointingMode; +import org.apache.flink.runtime.util.EnvironmentInformation; +import org.apache.flink.streaming.runtime.streamrecord.StreamRecord; +import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness; +import org.joda.time.Instant; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for serialization and deserialization of {@link PipelineOptions} in {@link DoFnOperator}. + * + *

      This is the Flink 2.2 override. It uses {@code + * org.apache.flink.core.execution.CheckpointingMode} (the non-deprecated path) instead of the + * deprecated {@code org.apache.flink.streaming.api.CheckpointingMode}. + */ +public class FlinkPipelineOptionsTest { + + /** Pipeline options. */ + public interface MyOptions extends FlinkPipelineOptions { + @Description("Bla bla bla") + @Default.String("Hello") + String getTestOption(); + + void setTestOption(String value); + } + + private static MyOptions options = + PipelineOptionsFactory.fromArgs("--testOption=nothing").as(MyOptions.class); + + /** Verifies that Flink 2.2.x is on the classpath. */ + @Test + public void testFlinkVersion() { + String version = EnvironmentInformation.getVersion(); + assertTrue("Expected Flink 2.2.x but got: " + version, version.startsWith("2.2")); + } + + /** These defaults should only be changed with a very good reason. */ + @Test + public void testDefaults() { + FlinkPipelineOptions options = FlinkPipelineOptions.defaults(); + assertThat(options.getParallelism(), is(-1)); + assertThat(options.getMaxParallelism(), is(-1)); + assertThat(options.getFlinkMaster(), is("[auto]")); + assertThat(options.getFilesToStage(), is(nullValue())); + assertThat(options.getLatencyTrackingInterval(), is(0L)); + assertThat(options.getShutdownSourcesAfterIdleMs(), is(-1L)); + assertThat(options.getObjectReuse(), is(false)); + // Uses the non-deprecated CheckpointingMode from org.apache.flink.core.execution + assertThat(options.getCheckpointingMode(), is(CheckpointingMode.EXACTLY_ONCE.name())); + assertThat(options.getMinPauseBetweenCheckpoints(), is(-1L)); + assertThat(options.getCheckpointingInterval(), is(-1L)); + assertThat(options.getCheckpointTimeoutMillis(), is(-1L)); + assertThat(options.getNumConcurrentCheckpoints(), is(1)); + assertThat(options.getTolerableCheckpointFailureNumber(), is(0)); + assertThat(options.getFinishBundleBeforeCheckpointing(), is(false)); + assertThat(options.getNumberOfExecutionRetries(), is(-1)); + assertThat(options.getExecutionRetryDelay(), is(-1L)); + assertThat(options.getRetainExternalizedCheckpointsOnCancellation(), is(false)); + assertThat(options.getStateBackendFactory(), is(nullValue())); + assertThat(options.getStateBackend(), is(nullValue())); + assertThat(options.getStateBackendStoragePath(), is(nullValue())); + assertThat(options.getExecutionModeForBatch(), is(FlinkPipelineOptions.PIPELINED)); + assertThat(options.getUseDataStreamForBatch(), is(false)); + assertThat(options.getSavepointPath(), is(nullValue())); + assertThat(options.getAllowNonRestoredState(), is(false)); + assertThat(options.getDisableMetrics(), is(false)); + assertThat(options.getFasterCopy(), is(false)); + + assertThat(options.isStreaming(), is(false)); + assertThat(options.getMaxBundleSize(), is(5000L)); + assertThat(options.getMaxBundleTimeMills(), is(10000L)); + + // In streaming mode bundle size and bundle time are shorter + FlinkPipelineOptions optionsStreaming = FlinkPipelineOptions.defaults(); + optionsStreaming.setStreaming(true); + assertThat(optionsStreaming.getMaxBundleSize(), is(1000L)); + assertThat(optionsStreaming.getMaxBundleTimeMills(), is(1000L)); + } + + @Test(expected = Exception.class) + public void parDoBaseClassPipelineOptionsNullTest() { + TupleTag mainTag = new TupleTag<>("main-output"); + Coder> coder = WindowedValues.getValueOnlyCoder(StringUtf8Coder.of()); + new DoFnOperator<>( + new TestDoFn(), + "stepName", + coder, + Collections.emptyMap(), + mainTag, + Collections.emptyList(), + new DoFnOperator.MultiOutputOutputManagerFactory<>( + mainTag, coder, new SerializablePipelineOptions(FlinkPipelineOptions.defaults())), + WindowingStrategy.globalDefault(), + new HashMap<>(), + Collections.emptyList(), + null, + null, /* key coder */ + null /* key selector */, + DoFnSchemaInformation.create(), + Collections.emptyMap()); + } + + /** Tests that PipelineOptions are present after serialization. */ + @Test + public void parDoBaseClassPipelineOptionsSerializationTest() throws Exception { + + TupleTag mainTag = new TupleTag<>("main-output"); + + Coder> coder = WindowedValues.getValueOnlyCoder(StringUtf8Coder.of()); + DoFnOperator doFnOperator = + new DoFnOperator<>( + new TestDoFn(), + "stepName", + coder, + Collections.emptyMap(), + mainTag, + Collections.emptyList(), + new DoFnOperator.MultiOutputOutputManagerFactory<>( + mainTag, coder, new SerializablePipelineOptions(FlinkPipelineOptions.defaults())), + WindowingStrategy.globalDefault(), + new HashMap<>(), + Collections.emptyList(), + options, + null, /* key coder */ + null /* key selector */, + DoFnSchemaInformation.create(), + Collections.emptyMap()); + + final byte[] serialized = SerializationUtils.serialize(doFnOperator); + + @SuppressWarnings("unchecked") + DoFnOperator deserialized = SerializationUtils.deserialize(serialized); + + TypeInformation> typeInformation = + TypeInformation.of(new TypeHint>() {}); + + OneInputStreamOperatorTestHarness, WindowedValue> testHarness = + new OneInputStreamOperatorTestHarness<>( + deserialized, typeInformation.createSerializer(new SerializerConfigImpl())); + testHarness.open(); + + // execute once to access options + testHarness.processElement( + new StreamRecord<>( + WindowedValues.of( + new Object(), Instant.now(), GlobalWindow.INSTANCE, PaneInfo.NO_FIRING))); + + testHarness.close(); + } + + private static class TestDoFn extends DoFn { + @ProcessElement + public void processElement(ProcessContext c) throws Exception { + Assert.assertNotNull(c.getPipelineOptions()); + Assert.assertEquals( + options.getTestOption(), c.getPipelineOptions().as(MyOptions.class).getTestOption()); + } + } +} diff --git a/sdks/go/examples/wasm/README.md b/sdks/go/examples/wasm/README.md index deb2e9196f41..7a7b54befee5 100644 --- a/sdks/go/examples/wasm/README.md +++ b/sdks/go/examples/wasm/README.md @@ -68,13 +68,13 @@ cd $BEAM_HOME Expected output should include the following, from which you acquire the latest flink runner version. ```shell -'flink_versions: 1.19,1.20,2.0,2.1' +'flink_versions: 1.19,1.20,2.0,2.1,2.2' ``` -#### 2. Set to the latest flink runner version i.e. 2.1 +#### 2. Set to the latest flink runner version i.e. 2.2 ```shell -FLINK_VERSION=2.1 +FLINK_VERSION=2.2 ``` #### 3. In a separate terminal, start the flink runner (It should take a few minutes on the first execution) diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index 8bd220776312..4b06b8eda613 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -2106,7 +2106,7 @@ def _add_argparse_args(cls, parser): class FlinkRunnerOptions(PipelineOptions): # These should stay in sync with gradle.properties. - PUBLISHED_FLINK_VERSIONS = ['1.19', '1.20', '2.0', '2.1'] + PUBLISHED_FLINK_VERSIONS = ['1.19', '1.20', '2.0', '2.1', '2.2'] @classmethod def _add_argparse_args(cls, parser): diff --git a/sdks/typescript/src/apache_beam/runners/flink.ts b/sdks/typescript/src/apache_beam/runners/flink.ts index bb5137b8f9b8..3fa386cb58a7 100644 --- a/sdks/typescript/src/apache_beam/runners/flink.ts +++ b/sdks/typescript/src/apache_beam/runners/flink.ts @@ -28,7 +28,7 @@ import { JavaJarService } from "../utils/service"; const MAGIC_HOST_NAMES = ["[local]", "[auto]"]; // These should stay in sync with gradle.properties. -const PUBLISHED_FLINK_VERSIONS = ["1.19", "1.20", "2.0", "2.1"]; +const PUBLISHED_FLINK_VERSIONS = ["1.19", "1.20", "2.0", "2.1", "2.2"]; const defaultOptions = { flinkMaster: "[local]", diff --git a/website/www/site/content/en/documentation/runners/flink.md b/website/www/site/content/en/documentation/runners/flink.md index 1aafa41ecb9c..d201d2546f8c 100644 --- a/website/www/site/content/en/documentation/runners/flink.md +++ b/website/www/site/content/en/documentation/runners/flink.md @@ -330,6 +330,16 @@ To find out which version of Flink is compatible with Beam please see the table Artifact Id Supported Beam Versions + + 2.2.x + beam-runners-flink-2.2 + ≥ 2.75.0 + + + 2.1.x + beam-runners-flink-2.1 + ≥ 2.75.0 + 2.0.x beam-runners-flink-2.0 From e3e10c01817852ee67717212076ce251a4dcc141 Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:29:26 -0400 Subject: [PATCH 434/490] use proper table naming in iceberg sql (#39035) --- .../sql/meta/provider/iceberg/IcebergMetastore.java | 8 ++++++-- .../meta/provider/iceberg/IcebergMetastoreTest.java | 13 +++++++++++++ .../beam/sdk/io/iceberg/IcebergCatalogConfig.java | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastore.java b/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastore.java index 5604b7c0837f..e6518f7192a5 100644 --- a/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastore.java +++ b/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastore.java @@ -94,7 +94,7 @@ public void dropTable(String tableName) { @Override public Map getTables() { for (String id : catalogConfig.listTables(database)) { - String name = TableName.create(id).getTableName(); + String name = tableName(id); @Nullable Table cachedTable = cachedTables.get(name); if (cachedTable == null) { Table table = checkStateNotNull(loadTable(id)); @@ -116,6 +116,10 @@ public Map getTables() { return table; } + private String tableName(String tableId) { + return TableName.create(tableId).getTableName(); + } + private String getIdentifier(String name) { return database + "." + name; } @@ -133,7 +137,7 @@ private String getIdentifier(Table table) { } return Table.builder() .type(getTableType()) - .name(identifier) + .name(tableName(identifier)) .schema(tableInfo.getSchema()) .properties(TableUtils.parseProperties(tableInfo.getProperties())) .build(); diff --git a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastoreTest.java b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastoreTest.java index a7baf1191d15..5e76b536134d 100644 --- a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastoreTest.java +++ b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergMetastoreTest.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.IOException; +import java.util.Map; import java.util.UUID; import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTable; import org.apache.beam.sdk.extensions.sql.meta.Table; @@ -89,6 +90,18 @@ public void testGetTables() { assertEquals(ImmutableSet.of("my_table_1", "my_table_2"), metastore().getTables().keySet()); } + @Test + public void testGetTablesLoadsUncachedTablesWithSimpleNames() { + String tableName = "uncached_table"; + catalog.catalogConfig.createTable( + catalog.currentDatabase() + "." + tableName, Schema.of(), null, null); + + Map tables = metastore().getTables(); + + assertEquals(ImmutableSet.of(tableName), tables.keySet()); + assertEquals(tableName, tables.get(tableName).getName()); + } + @Test public void testSupportsPartitioning() { Table table = Table.builder().name("my_table_1").schema(Schema.of()).type("iceberg").build(); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java index 6fb0a3480a0f..454fe40269d6 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java @@ -276,7 +276,7 @@ public boolean dropTable(String tableIdentifier) { public Set listTables(String namespace) { return catalog().listTables(Namespace.of(namespace)).stream() - .map(TableIdentifier::name) + .map(TableIdentifier::toString) .collect(Collectors.toSet()); } From b41fd6ff43b203e522fb3761041a5f3b423e298d Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Sat, 20 Jun 2026 22:57:53 -0400 Subject: [PATCH 435/490] Roll forward io expansion service to Java17 (#38974) * Second try moving io expansion service to Java17 * fixing xlang issue * Fix cross-language CI by auto-discovering JDK homes and setting Java 17 as default * invert java 11 / 17 order installation * fix gemini * fix gemini * decouple BeamModulePlugin and setup-environment-action from GHA infra --- .github/actions/setup-environment-action/action.yml | 2 +- .../beam_PerformanceTests_xlang_KafkaIO_Python.yml | 5 +++++ .../beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml | 4 ++++ .../beam_PostCommit_Python_Xlang_Gcp_Direct.yml | 5 +++++ .../beam_PostCommit_Python_Xlang_IO_Dataflow.yml | 4 ++++ .../beam_PostCommit_Python_Xlang_IO_Direct.yml | 7 ++++++- .../beam_PostCommit_TransformService_Direct.yml | 7 +++++-- .github/workflows/beam_PostCommit_XVR_Direct.yml | 4 ++++ .../workflows/beam_PostCommit_Yaml_Xlang_Direct.yml | 6 +++++- .../beam_PreCommit_Xlang_Generated_Transforms.yml | 6 +++++- .github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml | 5 +++++ .github/workflows/beam_Publish_Beam_SDK_Snapshots.yml | 5 ++++- .github/workflows/build_release_candidate.yml | 10 ++++++---- .github/workflows/typescript_tests.yml | 10 ++++++++++ .../org/apache/beam/gradle/BeamModulePlugin.groovy | 6 ++++++ sdks/java/expansion-service/container/Dockerfile | 2 +- sdks/java/io/expansion-service/build.gradle | 4 ++-- 17 files changed, 78 insertions(+), 14 deletions(-) diff --git a/.github/actions/setup-environment-action/action.yml b/.github/actions/setup-environment-action/action.yml index 06633c6c7279..c82ad087fe46 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -71,7 +71,7 @@ runs: - name: Install Java if: ${{ inputs.java-version != '' }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} diff --git a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml index 7ed3fce19a0e..564f1be187d7 100644 --- a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml +++ b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml @@ -73,6 +73,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: default - name: Set k8s access uses: ./.github/actions/setup-k8s-access @@ -179,6 +182,8 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:java:io:expansion-service:shadowJar + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 # The env variable is created and populated in the test-arguments-action as "_test_arguments_" - name: run integrationTest uses: ./.github/actions/gradle-command-self-hosted-action diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml index 01997b22515d..2c442af725e6 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -84,6 +87,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ -PuseWheelDistribution \ + -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml index e4a71ee8edd3..5dc1ed0dbaf2 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -85,6 +88,8 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:gcpCrossLanguagePostCommit + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml index 832db23a73fc..7bfe29e4b509 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml @@ -73,6 +73,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -84,6 +87,7 @@ jobs: -PpythonVersion=3.14 \ -PuseWheelDistribution \ -PkafkaBootstrapServer=10.128.0.40:9094,10.128.0.28:9094,10.128.0.165:9094 \ + -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml index bb3198f83d2c..ebb891dae457 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml @@ -73,6 +73,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -80,7 +83,9 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:ioCrossLanguagePostCommit - arguments: -PuseWheelDistribution + arguments: | + -PuseWheelDistribution \ + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_TransformService_Direct.yml b/.github/workflows/beam_PostCommit_TransformService_Direct.yml index a127cb5b6046..2111afa48015 100644 --- a/.github/workflows/beam_PostCommit_TransformService_Direct.yml +++ b/.github/workflows/beam_PostCommit_TransformService_Direct.yml @@ -74,7 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: 11 + java-version: | + 17 + 11 python-version: | 3.10 ${{ matrix.python_version }} @@ -83,7 +85,8 @@ jobs: with: gradle-command: :sdks:python:test-suites:direct:xlang:transformServicePythonUsingJava arguments: | - -PtestJavaVersion=11 \ + -PtestJavaVersion=17 \ + -Pjava17Home=$JAVA_HOME_17_X64 \ -Pjava11Home=$JAVA_HOME_11_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_Direct.yml b/.github/workflows/beam_PostCommit_XVR_Direct.yml index f2174aa3ec98..d95fe3bec3d0 100644 --- a/.github/workflows/beam_PostCommit_XVR_Direct.yml +++ b/.github/workflows/beam_PostCommit_XVR_Direct.yml @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Direct script env: @@ -83,6 +86,7 @@ jobs: gradle-command: :sdks:python:test-suites:direct:xlang:validatesCrossLanguageRunner arguments: | -PpythonVersion=${{ matrix.python_version }} \ + -Pjava17Home=$JAVA_HOME_17_X64 \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index 9598b19ebbe0..e1887ce7864e 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -74,13 +74,17 @@ jobs: uses: ./.github/actions/setup-environment-action with: python-version: default - java-version: '11' + java-version: | + 17 + 11 - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=p310_ml_test,yaml + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml index 959f36234d70..2f9741c01a01 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -102,7 +102,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: default + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: Set PY_VER_CLEAN id: set_py_ver_clean @@ -114,3 +116,5 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:crossLanguageWrapperValidationPreCommit --info + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 21281b8cd3e5..10693af1bf4e 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -87,11 +87,16 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: default - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=p310_ml_test,yaml + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index 526abfd4e30b..ebf2ec703f61 100644 --- a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml +++ b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml @@ -109,7 +109,9 @@ jobs: if: ${{ startsWith(matrix.container_task, 'java') }} uses: ./.github/actions/setup-environment-action with: - java-version: 11 + java-version: | + 17 + 11 - name: Setup Python environment if: ${{ startsWith(matrix.container_task, 'python') }} uses: ./.github/actions/setup-environment-action @@ -120,6 +122,7 @@ jobs: with: gradle-command: :sdks:${{ matrix.container_task }} arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -Pdocker-repository-root=gcr.io/apache-beam-testing/beam-sdk \ -Pdocker-tag-list=${{ github.sha }},${BEAM_VERSION}${LATEST_TAG} \ -Pcontainer-architecture-list=arm64,amd64 \ diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index c53c0712a600..9e2a0c820178 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -276,11 +276,13 @@ jobs: repository: apache/beam - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - - name: Install Java 11 + - name: Install Java uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: | + 17 + 11 - name: Install Python 3.10 uses: actions/setup-python@v5 with: @@ -570,11 +572,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Install Java 11 + - name: Install Java 17 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Remove default github maven configuration # This step is a workaround to avoid a decryption issue of Beam's # net.linguica.gradle.maven.settings plugin and github's provided maven diff --git a/.github/workflows/typescript_tests.yml b/.github/workflows/typescript_tests.yml index 9bc352379913..17947290d4c3 100644 --- a/.github/workflows/typescript_tests.yml +++ b/.github/workflows/typescript_tests.yml @@ -111,6 +111,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' + - name: Install Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' - name: Setup Beam Python working-directory: ./sdks/python run: | @@ -174,6 +179,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' + - name: Install Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' - name: Setup Beam Python working-directory: ./sdks/python run: | diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index e8c7a8791935..344cf66e01fc 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -1646,6 +1646,12 @@ class BeamModulePlugin implements Plugin { useJUnit() executable = "${testJavaHome}/bin/java" } + // redirect java exec tasks (expansion service, run, shadowJar execs) to specified JDK + if (testJavaHome) { + project.tasks.withType(JavaExec).configureEach { + executable = "${testJavaHome}/bin/java" + } + } } if (configuration.shadowClosure) { diff --git a/sdks/java/expansion-service/container/Dockerfile b/sdks/java/expansion-service/container/Dockerfile index 968f5cd2ac25..513dd6b75b88 100644 --- a/sdks/java/expansion-service/container/Dockerfile +++ b/sdks/java/expansion-service/container/Dockerfile @@ -16,7 +16,7 @@ # limitations under the License. ############################################################################### -FROM eclipse-temurin:11 +FROM eclipse-temurin:17 LABEL Author "Apache Beam " ARG TARGETOS ARG TARGETARCH diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 70a3fce538b6..f7b241a75944 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -25,8 +25,8 @@ applyJavaNature( exportJavadoc: false, validateShadowJar: false, shadowClosure: {}, - // iceberg requires Java11+ - requireJavaVersion: JavaVersion.VERSION_11 + // iceberg requires Java11+ and delta lake requires Java17+ + requireJavaVersion: JavaVersion.VERSION_17 ) // We don't want to use the latest version for the entire beam sdk since beam Java users can override it themselves. From 22c7dcbfa31510c59251446fc73f7d9e7b902c3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:31:32 -0400 Subject: [PATCH 436/490] Bump github.com/golang-cz/devslog from 0.0.16 to 0.0.17 in /sdks (#39054) --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 116f40961052..5d0d2ec141c7 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -71,7 +71,7 @@ require ( require ( github.com/avast/retry-go/v4 v4.7.0 github.com/fsouza/fake-gcs-server v1.52.3 - github.com/golang-cz/devslog v0.0.16 + github.com/golang-cz/devslog v0.0.17 github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.0 golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a diff --git a/sdks/go.sum b/sdks/go.sum index 1db20e1d6602..11bb3dca7582 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -415,8 +415,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang-cz/devslog v0.0.16 h1:3tmUAvVRzX3+lzaLdpuEpfnc72e2pLGrIyok+FHjLP8= -github.com/golang-cz/devslog v0.0.16/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= +github.com/golang-cz/devslog v0.0.17 h1:FAAqtWwomNWqWaoiitKfKWwSU8la0SQ9XhwXF7x2QF4= +github.com/golang-cz/devslog v0.0.17/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= From e882d33f719e612819a4c8255dafe9a10cc9985f Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Mon, 22 Jun 2026 15:49:25 -0400 Subject: [PATCH 437/490] try to fix forward tests for java 17 (#39058) * try to fix forward with 17 only * address just two workflows * Revert XVR Spark3 workflow java-version override * Add Java 17 and 11 setup and -Pjava17Home across all XVR workflows * Revert redundant java-version override in TransformService Direct workflow --- .../workflows/beam_PostCommit_XVR_Flink.yml | 4 ++++ ...am_PostCommit_XVR_GoUsingJava_Dataflow.yml | 4 ++++ ...ostCommit_XVR_JavaUsingPython_Dataflow.yml | 4 ++++ ...Commit_XVR_PythonUsingJavaSQL_Dataflow.yml | 4 ++++ ...ostCommit_XVR_PythonUsingJava_Dataflow.yml | 4 ++++ .../workflows/beam_PostCommit_XVR_Spark3.yml | 4 ++++ .../beam/gradle/BeamModulePlugin.groovy | 24 ++++++++++++------- .../controller-container/Dockerfile | 2 +- 8 files changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml b/.github/workflows/beam_PostCommit_XVR_Flink.yml index e93fe82f48ad..ac52e88f529c 100644 --- a/.github/workflows/beam_PostCommit_XVR_Flink.yml +++ b/.github/workflows/beam_PostCommit_XVR_Flink.yml @@ -75,6 +75,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Flink script env: @@ -83,6 +86,7 @@ jobs: with: gradle-command: :runners:flink:${{ env.FlinkVersion }}:job-server:validatesCrossLanguageRunner arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml index 90d1eb786522..78e5c8afc3f0 100644 --- a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml @@ -76,6 +76,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: default - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 @@ -86,6 +89,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerGoUsingJava arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PuseDockerBuildx - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 diff --git a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml index c282f6c47069..32dbbee7842a 100644 --- a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 ${{ matrix.python_version }} @@ -82,6 +85,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerJavaUsingPython arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml index 95496621f9ce..8ec8a941ed48 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml @@ -73,12 +73,16 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: 3.14 - name: run PostCommit XVR PythonUsingJavaSQL Dataflow script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerPythonUsingSql arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=3.14 \ - name: Archive Python Test Results uses: actions/upload-artifact@v7 diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml index 0cd9790e6793..acf4c47ad2c1 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 ${{ matrix.python_version }} @@ -82,6 +85,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerPythonUsingJava arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results uses: actions/upload-artifact@v7 diff --git a/.github/workflows/beam_PostCommit_XVR_Spark3.yml b/.github/workflows/beam_PostCommit_XVR_Spark3.yml index 2c7104df6115..2db33583a317 100644 --- a/.github/workflows/beam_PostCommit_XVR_Spark3.yml +++ b/.github/workflows/beam_PostCommit_XVR_Spark3.yml @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Spark3 script env: @@ -82,6 +85,7 @@ jobs: with: gradle-command: :runners:spark:3:job-server:validatesCrossLanguageRunner arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 344cf66e01fc..b5aef5d23ad4 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -978,6 +978,9 @@ class BeamModulePlugin implements Plugin { // set compiler options for java version overrides to compile with a different java version project.ext.setJavaVerOptions = { CompileOptions options, String ver -> + if (!project.findProperty("java${ver}Home")) { + return + } if (ver == '8') { def java8Home = project.findProperty("java8Home") options.fork = true @@ -1161,16 +1164,16 @@ class BeamModulePlugin implements Plugin { // If compiled on older SDK, compile with JDK configured with compatible javaXXHome // The order is intended here if (requireJavaVersion.compareTo(JavaVersion.VERSION_11) <= 0 && - project.hasProperty('java11Home')) { + project.findProperty('java11Home')) { forkJavaVersion = '11' } else if (requireJavaVersion.compareTo(JavaVersion.VERSION_17) <= 0 && - project.hasProperty('java17Home')) { + project.findProperty('java17Home')) { forkJavaVersion = '17' } else if (requireJavaVersion.compareTo(JavaVersion.VERSION_21) <= 0 && - project.hasProperty('java21Home')) { + project.findProperty('java21Home')) { forkJavaVersion = '21' } else if (requireJavaVersion.compareTo(JavaVersion.VERSION_25) <= 0 && - project.hasProperty('java25Home')) { + project.findProperty('java25Home')) { forkJavaVersion = '25' } else { logger.config("Module ${project.name} disabled. To enable, either " + @@ -1628,7 +1631,7 @@ class BeamModulePlugin implements Plugin { } // if specified test java version, modify the compile and runtime versions accordingly if (['11', '17', '21', '25'].contains(testJavaVersion)) { - def testJavaHome = project.getProperty("java${testJavaVersion}Home") + def testJavaHome = project.findProperty("java${testJavaVersion}Home") // redirect java compiler to specified version for compileTestJava only project.tasks.compileTestJava { @@ -2710,7 +2713,8 @@ class BeamModulePlugin implements Plugin { def pythonDir = project.project(":sdks:python").projectDir def usesDataflowRunner = config.pythonPipelineOptions.contains("--runner=TestDataflowRunner") || config.pythonPipelineOptions.contains("--runner=DataflowRunner") - def javaContainerSuffix = getSupportedJavaVersion() + String ver = project.findProperty('testJavaVersion') + def javaContainerSuffix = ver ? getSupportedJavaVersion(ver) : getSupportedJavaVersion() // Sets up, collects, and runs Python pipeline tests project.tasks.register(config.name+"PythonUsingJava") { @@ -2790,7 +2794,8 @@ class BeamModulePlugin implements Plugin { ] def serviceArgs = project.project(':sdks:python').mapToArgString(expansionServiceOpts) def pythonContainerSuffix = project.project(':sdks:python').pythonVersion.replace('.', '') - def javaContainerSuffix = getSupportedJavaVersion() + String ver = project.findProperty('testJavaVersion') + def javaContainerSuffix = ver ? getSupportedJavaVersion(ver) : getSupportedJavaVersion() def setupTask = project.tasks.register(config.name+"Setup", Exec) { dependsOn ':sdks:java:container:'+javaContainerSuffix+':docker' dependsOn ':sdks:python:container:py'+pythonContainerSuffix+':docker' @@ -2979,10 +2984,11 @@ class BeamModulePlugin implements Plugin { ] def serviceArgs = project.project(':sdks:python').mapToArgString(transformServiceOpts) def pythonContainerSuffix = project.project(':sdks:python').pythonVersion.replace('.', '') - def javaContainerSuffix = getSupportedJavaVersion() + String ver = project.findProperty('testJavaVersion') + def javaContainerSuffix = ver ? getSupportedJavaVersion(ver) : getSupportedJavaVersion() // Transform service delivers transforms that refer to SDK harness containers with following sufixes. - def transformServiceJavaContainerSuffix = 'java11' + def transformServiceJavaContainerSuffix = 'java17' def transformServicePythonContainerSuffix = pythonContainerSuffix def setupTask = project.tasks.register(config.name+"Setup", Exec) { diff --git a/sdks/java/transform-service/controller-container/Dockerfile b/sdks/java/transform-service/controller-container/Dockerfile index 8aca2d7fb7c8..4efa7bdc42c3 100644 --- a/sdks/java/transform-service/controller-container/Dockerfile +++ b/sdks/java/transform-service/controller-container/Dockerfile @@ -16,7 +16,7 @@ # limitations under the License. ############################################################################### -FROM eclipse-temurin:11 +FROM eclipse-temurin:17 LABEL Author "Apache Beam " ARG TARGETOS ARG TARGETARCH From ba11d772f8b40421919bb0b9b9e64fbaa0dfe5ad Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 22 Jun 2026 16:08:55 -0400 Subject: [PATCH 438/490] Update pandas version upper bound to support python 3.14 (#39056) * Update pandas version upper bound to support python 3.14 * Fix the failed tests causing by updating pandas * Skip the test as the pandas bug is unfixed at 2.3.3 * Refactor get_dummies NaN column lookup to use global static constant --- sdks/python/apache_beam/dataframe/frames.py | 7 ++++++- sdks/python/apache_beam/dataframe/frames_test.py | 4 +++- sdks/python/setup.py | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/sdks/python/apache_beam/dataframe/frames.py b/sdks/python/apache_beam/dataframe/frames.py index b839d5504cea..310791d2b58f 100644 --- a/sdks/python/apache_beam/dataframe/frames.py +++ b/sdks/python/apache_beam/dataframe/frames.py @@ -63,6 +63,10 @@ # Get major, minor version PD_VERSION = tuple(map(int, pd.__version__.split('.')[0:2])) +# Find the name pandas uses for the NaN column in get_dummies(). +# Older pandas versions (<2.3.0) use 'nan', whereas newer versions (>=2.3.0) use 'NaN'. +_DUMMY_NAN_COLUMN = 'NaN' if PD_VERSION >= (2, 3) else 'nan' + def populate_not_implemented(pd_type): def wrapper(deferred_type): @@ -5054,7 +5058,8 @@ def get_dummies(self, **kwargs): # the data includes NaNs, which is not valid to be casted as a Category, # but nevertheless would be broadcasted as a column in get_dummies() columns = sorted(set().union(*split_cats)) - columns = columns + ['nan'] if 'nan' not in columns else columns + if _DUMMY_NAN_COLUMN not in columns: + columns = columns + [_DUMMY_NAN_COLUMN] proxy = pd.DataFrame(columns=columns).astype(int) diff --git a/sdks/python/apache_beam/dataframe/frames_test.py b/sdks/python/apache_beam/dataframe/frames_test.py index bb46f0e29be1..7a03af6220b8 100644 --- a/sdks/python/apache_beam/dataframe/frames_test.py +++ b/sdks/python/apache_beam/dataframe/frames_test.py @@ -37,6 +37,8 @@ # Get major, minor version PD_VERSION = tuple(map(int, pd.__version__.split('.')[0:2])) +# Get major, minor, patch version +PD_FULL_VERSION = tuple(int(x) for x in re.findall(r'\d+', pd.__version__)[0:3]) GROUPBY_DF = pd.DataFrame({ 'group': ['a' if i % 5 == 0 or i % 3 == 0 else 'b' for i in range(100)], @@ -1437,7 +1439,7 @@ def test_unstack_pandas_example2(self): self._run_test(lambda s: s.unstack(level=0), s) @unittest.skipIf( - sys.version_info >= (3, 12) and PD_VERSION < (2, 3), + sys.version_info >= (3, 12) and PD_FULL_VERSION < (2, 3, 4), 'https://github.com/pandas-dev/pandas/issues/58604') def test_unstack_pandas_example3(self): index = self._unstack_get_categorical_index() diff --git a/sdks/python/setup.py b/sdks/python/setup.py index c3543995a85b..b746fb1d180a 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -162,7 +162,7 @@ def cythonize(*args, **kwargs): # https://github.com/pandas-dev/pandas/issues/45725 # must update the below "docs" and "test" for extras_require dataframe_dependency = [ - 'pandas>=1.4.3,!=1.5.0,!=1.5.1,<2.3', + 'pandas>=1.4.3,!=1.5.0,!=1.5.1,<2.4', ] milvus_dependency = ['pymilvus>=2.5.10,<3.0.0'] @@ -485,7 +485,7 @@ def get_portability_package_data(): 'docstring-parser>=0.15,<1.0', 'docutils>=0.18.1', 'markdown', - 'pandas<2.3.0', + 'pandas<2.4.0', 'openai', 'virtualenv-clone>=0.5,<1.0', ], @@ -496,7 +496,7 @@ def get_portability_package_data(): 'jinja2>=3.0,<3.2', 'joblib>=1.0.1', 'mock>=1.0.1,<6.0.0', - 'pandas<2.3.0', + 'pandas<2.4.0', 'parameterized>=0.7.1,<0.10.0', 'pydot>=1.2.0,<2', 'pyhamcrest>=1.9,!=1.10.0,<3.0.0', From 5df98af220e80f3cdf4e80f9e25a2bdcd8a744d1 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Mon, 22 Jun 2026 17:24:07 -0400 Subject: [PATCH 439/490] Fix playground precommit test failure (#39062) * Debug playground failure * Restore the error message and update java version. --- .github/workflows/beam_Playground_Precommit.yml | 2 +- .../internal/setup_tools/life_cycle/life_cycle_setuper_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index 379184fbd8a8..d04863743dd7 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -45,7 +45,7 @@ jobs: env: DATASTORE_EMULATOR_VERSION: '423.0.0' PYTHON_VERSION: '3.10' - JAVA_VERSION: '11' + JAVA_VERSION: '17' steps: - uses: actions/checkout@v6 - name: Setup repository diff --git a/playground/backend/internal/setup_tools/life_cycle/life_cycle_setuper_test.go b/playground/backend/internal/setup_tools/life_cycle/life_cycle_setuper_test.go index e709e87ea7cd..e72c649b904e 100644 --- a/playground/backend/internal/setup_tools/life_cycle/life_cycle_setuper_test.go +++ b/playground/backend/internal/setup_tools/life_cycle/life_cycle_setuper_test.go @@ -16,13 +16,14 @@ package life_cycle import ( - "beam.apache.org/playground/backend/internal/emulators" "fmt" "io/fs" "os" "path/filepath" "testing" + "beam.apache.org/playground/backend/internal/emulators" + "github.com/google/uuid" playground "beam.apache.org/playground/backend/internal/api/v1" From 8430ac18d74848ac274493d1a48ad926e66b7e8a Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Mon, 22 Jun 2026 17:46:00 -0400 Subject: [PATCH 440/490] [yaml] - expand jinja method (#38547) * expand jinja * minor tweaks * fix lint * fix gemini comments * add more documentation --- .../apache_beam/yaml/examples/README.md | 4 + sdks/python/apache_beam/yaml/main.py | 7 +- sdks/python/apache_beam/yaml/yaml_provider.py | 11 +- .../python/apache_beam/yaml/yaml_transform.py | 56 ++++++++- .../apache_beam/yaml/yaml_transform_test.py | 119 ++++++++++++++++++ .../content/en/documentation/sdks/yaml.md | 34 +++++ 6 files changed, 222 insertions(+), 9 deletions(-) diff --git a/sdks/python/apache_beam/yaml/examples/README.md b/sdks/python/apache_beam/yaml/examples/README.md index 55fd19bd8c40..5282951c696e 100644 --- a/sdks/python/apache_beam/yaml/examples/README.md +++ b/sdks/python/apache_beam/yaml/examples/README.md @@ -256,6 +256,10 @@ Jinja `% include` directive: - [wordCountInclude.yaml](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/yaml/examples/transforms/jinja/include/wordCountInclude.yaml) - [Instructions](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/yaml/examples/transforms/jinja/include/README.md) on how to run the pipeline. +Jinja template inheritance (`{% extends %}`): +- [wordCountInheritance.yaml](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/yaml/examples/transforms/jinja/inheritance/wordCountInheritance.yaml) +- [Instructions](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/yaml/examples/transforms/jinja/inheritance/README.md) on how to run the pipeline. + ### ML diff --git a/sdks/python/apache_beam/yaml/main.py b/sdks/python/apache_beam/yaml/main.py index dc928dec7941..804798b82e02 100644 --- a/sdks/python/apache_beam/yaml/main.py +++ b/sdks/python/apache_beam/yaml/main.py @@ -235,8 +235,13 @@ def _build_pipeline_yaml_from_argv(argv): argv = _preparse_jinja_flags(argv) known_args, pipeline_args = _parse_arguments(argv) pipeline_template = _pipeline_spec_from_args(known_args) + + search_paths = [] + if known_args.yaml_pipeline_file: + search_paths.append(FileSystems.split(known_args.yaml_pipeline_file)[0]) + pipeline_yaml = yaml_transform.expand_jinja( - pipeline_template, known_args.jinja_variables or {}) + pipeline_template, known_args.jinja_variables or {}, search_paths) return known_args, pipeline_args, pipeline_template, pipeline_yaml diff --git a/sdks/python/apache_beam/yaml/yaml_provider.py b/sdks/python/apache_beam/yaml/yaml_provider.py index ce5a6e320589..324ae0c2e734 100755 --- a/sdks/python/apache_beam/yaml/yaml_provider.py +++ b/sdks/python/apache_beam/yaml/yaml_provider.py @@ -469,10 +469,14 @@ def _with_extra_dependencies(self, dependencies: Iterable[str]): @ExternalProvider.register_provider_type('yaml') class YamlProvider(Provider): - def __init__(self, transforms: Mapping[str, Mapping[str, Any]]): + def __init__( + self, + transforms: Mapping[str, Mapping[str, Any]], + provider_base_path: Optional[str] = None): if not isinstance(transforms, dict): raise ValueError('Transform mapping must be a dict.') self._transforms = transforms + self._provider_base_path = provider_base_path def available(self): return True @@ -524,7 +528,10 @@ def create_transform( else: body_str = yaml.safe_dump(SafeLineLoader.strip_metadata(body)) # Now re-parse resolved templatization. - body = yaml.load(expand_jinja(body_str, args), Loader=SafeLineLoader) + search_paths = [FileSystems.split(self._provider_base_path)[0] + ] if self._provider_base_path else [] + body = yaml.load( + expand_jinja(body_str, args, search_paths), Loader=SafeLineLoader) if (body.get('type') == 'chain' and 'input' not in body and spec.get('requires_inputs', True)): body['input'] = 'input' diff --git a/sdks/python/apache_beam/yaml/yaml_transform.py b/sdks/python/apache_beam/yaml/yaml_transform.py index 2b745babad02..a4bb9144f8e6 100644 --- a/sdks/python/apache_beam/yaml/yaml_transform.py +++ b/sdks/python/apache_beam/yaml/yaml_transform.py @@ -1391,19 +1391,63 @@ def validate_transform_references(spec): return spec +def strip_leading_comments(source: str) -> str: + lines = source.splitlines(keepends=True) + stripped_lines = [] + in_leading_comments = True + for line in lines: + stripped_line = line.lstrip() + if in_leading_comments: + if stripped_line.startswith('#') or not stripped_line: + continue + else: + in_leading_comments = False + stripped_lines.append(line) + return "".join(stripped_lines) + + class _BeamFileIOLoader(jinja2.BaseLoader): + def __init__(self, search_paths=()): + self.search_paths = list(search_paths) + def get_source(self, environment, path): - with FileSystems.open(path) as fin: - source = fin.read().decode() - return source, path, lambda: True + candidates = [path] + if FileSystems.get_scheme(path) is None and not os.path.isabs(path): + for search_path in self.search_paths: + candidates.append(FileSystems.join(search_path, path)) + + for candidate in candidates: + try: + exists = FileSystems.exists(candidate) + except Exception: + exists = False + + if exists: + with FileSystems.open(candidate) as fin: + source = fin.read().decode() + return strip_leading_comments(source), candidate, lambda: True + + raise jinja2.TemplateNotFound(path) def expand_jinja( - jinja_template: str, jinja_variables: Mapping[str, Any]) -> str: + jinja_template: str, + jinja_variables: Mapping[str, Any], + search_paths: Iterable[str] = ()) -> str: + beam_root_dir = os.path.dirname( + os.path.dirname(os.path.abspath(beam.__file__))) + + all_search_paths = list(search_paths) + if beam_root_dir not in all_search_paths: + all_search_paths.append(beam_root_dir) + if '.' not in all_search_paths: + all_search_paths.append('.') + return ( # keep formatting jinja2.Environment( - undefined=jinja2.StrictUndefined, loader=_BeamFileIOLoader()) - .from_string(jinja_template) + undefined=jinja2.StrictUndefined, + loader=_BeamFileIOLoader(all_search_paths)) + .from_string(strip_leading_comments(jinja_template)) .render(datetime=datetime, **jinja_variables)) diff --git a/sdks/python/apache_beam/yaml/yaml_transform_test.py b/sdks/python/apache_beam/yaml/yaml_transform_test.py index 192af63a9871..4cfe625fc9cd 100644 --- a/sdks/python/apache_beam/yaml/yaml_transform_test.py +++ b/sdks/python/apache_beam/yaml/yaml_transform_test.py @@ -23,12 +23,16 @@ import tempfile import unittest +import yaml + import apache_beam as beam from apache_beam.testing.util import assert_that from apache_beam.testing.util import equal_to from apache_beam.utils import python_callable from apache_beam.yaml import yaml_provider +from apache_beam.yaml.yaml_transform import SafeLineLoader from apache_beam.yaml.yaml_transform import YamlTransform +from apache_beam.yaml.yaml_transform import expand_jinja try: import jsonschema @@ -1467,6 +1471,65 @@ def test_must_consume_error_output(self): ''', providers=merged_providers) + def test_provider_with_jinja_imports(self): + # Create a macro file in the same temp directory as the provider + macro_path = os.path.join(self.temp_dir, 'my_macros.yaml') + with open(macro_path, 'w') as f: + f.write( + """ +{%- macro power_expr(var, n) -%} +{{ var }} ** {{ n }} +{%- endmacro -%} +""") + + # Create a provider that imports and uses the macro + templated_provider_path = os.path.join( + self.temp_dir, 'templated_provider.yaml') + with open(templated_provider_path, 'w') as f: + f.write( + """ +- type: yaml + transforms: + CustomPower: + config_schema: + properties: + n: {type: integer} + body: | + type: MapToFields + config: + language: python + append: true + fields: + power: "{% import 'my_macros.yaml' as m %}{{ m.power_expr('element', n) }}" +""") + + loaded_providers = yaml_provider.load_providers(templated_provider_path) + test_providers = yaml_provider.InlineProvider(TEST_PROVIDERS) + merged_providers = yaml_provider.merge_providers( + loaded_providers, [test_providers]) + + with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions( + pickle_library='cloudpickle')) as p: + results = p | YamlTransform( + ''' + type: composite + transforms: + - type: Create + config: + elements: [2, 3] + - type: CustomPower + input: Create + config: + n: 3 + output: CustomPower + ''', + providers=merged_providers) + + assert_that( + results, + equal_to( + [beam.Row(element=2, power=8), beam.Row(element=3, power=27)])) + @beam.transforms.ptransform.annotate_yaml class LinearTransform(beam.PTransform): @@ -1481,6 +1544,62 @@ def expand(self, pcoll): return pcoll | beam.Map(lambda x: a * x.element + b) +class TestYamlExpandJinja(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + # Create a macro file with leading comments (license header) + self.macro_path = os.path.join(self.temp_dir, 'my_macros.yaml') + with open(self.macro_path, 'w') as f: + f.write( + """# coding=utf-8 +# Licensed to the Apache Software Foundation... +# Some leading comment line + +{%- macro add_n(val, n) -%} +{{ val }} + {{ n }} +{%- endmacro -%} +""") + + # Create a pipeline template that includes/imports the macro + self.pipeline_path = os.path.join(self.temp_dir, 'my_pipeline.yaml') + with open(self.pipeline_path, 'w') as f: + f.write( + """# coding=utf-8 +# Licensed to the Apache Software Foundation... + +{% import 'my_macros.yaml' as macros %} +type: composite +transforms: + - type: Create + config: + elements: [1, 2, 3] + - type: MapToFields + config: + language: python + fields: + result: {{ macros.add_n('element', 10) }} +""") + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_expand_jinja_with_leading_comments_and_imports(self): + # Read the pipeline template + with open(self.pipeline_path, 'r') as f: + template_content = f.read() + + # Expand the jinja using our temp_dir as a search path + expanded = expand_jinja(template_content, {}, [self.temp_dir]) + + # Parse the expanded YAML + parsed = yaml.load(expanded, Loader=SafeLineLoader) + + # Verify the comment-stripping and import resolution was successful + self.assertEqual(parsed['type'], 'composite') + self.assertEqual( + parsed['transforms'][1]['config']['fields']['result'], 'element + 10') + + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) unittest.main() diff --git a/website/www/site/content/en/documentation/sdks/yaml.md b/website/www/site/content/en/documentation/sdks/yaml.md index 589679d3e502..a1fda30ddb68 100644 --- a/website/www/site/content/en/documentation/sdks/yaml.md +++ b/website/www/site/content/en/documentation/sdks/yaml.md @@ -812,6 +812,40 @@ pipeline: This pipeline can be run with the same command as in the `% include` example above. +You can also use template inheritance (`{% extends %}` and `{% block %}`) to define a base pipeline layout and allow child pipelines to override specific blocks of transforms: + +/base_pipeline.yaml +```yaml +pipeline: + type: chain + transforms: + - type: ReadFromText + config: + path: {{ input_path }} + +# Injection point for child templates +{% block custom_steps %} +{% endblock %} + + - type: WriteToText + config: + path: {{ output_path }} +``` + +/child_pipeline.yaml +```yaml +{% extends '/base_pipeline.yaml' %} + +{% block custom_steps %} + - type: Filter + config: + language: python + keep: 'row.count > 10' +{% endblock %} +``` + +This pipeline can be run using the exact same command structure as above. + There are many more ways to import and even use template inheritance using Jinja as seen [here](https://jinja.palletsprojects.com/en/stable/templates/#import) and [here](https://jinja.palletsprojects.com/en/stable/templates/#inheritance). From 4a6c433ac7a90ffabd67f669912875384843c5b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:46:29 -0400 Subject: [PATCH 441/490] Bump actions/checkout from 6 to 7 (#39033) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/IO_Iceberg_Integration_Tests.yml | 2 +- .../IO_Iceberg_Integration_Tests_Dataflow.yml | 2 +- ..._Iceberg_Managed_Integration_Tests_Dataflow.yml | 2 +- .github/workflows/IO_Iceberg_Performance_Tests.yml | 2 +- .github/workflows/IO_Iceberg_Unit_Tests.yml | 2 +- .github/workflows/assign_milestone.yml | 2 +- .github/workflows/beam_CancelStaleDataflowJobs.yml | 2 +- .../workflows/beam_CleanUpDataprocResources.yml | 2 +- .github/workflows/beam_CleanUpGCPResources.yml | 2 +- .../workflows/beam_CleanUpPrebuiltSDKImages.yml | 2 +- .../workflows/beam_CloudML_Benchmarks_Dataflow.yml | 2 +- .../beam_IODatastoresCredentialsRotation.yml | 2 +- .../beam_Inference_Python_Benchmarks_Dataflow.yml | 2 +- .../beam_Infrastructure_PolicyEnforcer.yml | 2 +- .../beam_Infrastructure_SecurityLogging.yml | 2 +- .../beam_Infrastructure_ServiceAccountKeys.yml | 2 +- .../beam_Infrastructure_UsersPermissions.yml | 2 +- .github/workflows/beam_Java_JMH.yml | 2 +- .../beam_Java_LoadTests_Combine_Smoke.yml | 2 +- .../beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_CoGBK_Flink_batch.yml | 2 +- .../beam_LoadTests_Go_Combine_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_Combine_Flink_Batch.yml | 2 +- .../beam_LoadTests_Go_GBK_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_GBK_Flink_Batch.yml | 2 +- .../beam_LoadTests_Go_ParDo_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_ParDo_Flink_Batch.yml | 2 +- .../beam_LoadTests_Go_SideInput_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Go_SideInput_Flink_Batch.yml | 2 +- .../beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml | 2 +- ...eam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml | 2 +- ...s_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml | 2 +- ...va_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml | 2 +- ...s_Java_CoGBK_SparkStructuredStreaming_Batch.yml | 2 +- .../beam_LoadTests_Java_Combine_Dataflow_Batch.yml | 2 +- ...m_LoadTests_Java_Combine_Dataflow_Streaming.yml | 2 +- ...Java_Combine_SparkStructuredStreaming_Batch.yml | 2 +- .../beam_LoadTests_Java_GBK_Dataflow_Batch.yml | 2 +- .../beam_LoadTests_Java_GBK_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml | 2 +- ...LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml | 2 +- ...am_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml | 2 +- ...Tests_Java_GBK_Dataflow_V2_Streaming_Java17.yml | 2 +- .../workflows/beam_LoadTests_Java_GBK_Smoke.yml | 2 +- ...sts_Java_GBK_SparkStructuredStreaming_Batch.yml | 2 +- .../beam_LoadTests_Java_ParDo_Dataflow_Batch.yml | 2 +- ...eam_LoadTests_Java_ParDo_Dataflow_Streaming.yml | 2 +- ...s_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml | 2 +- ...va_ParDo_Dataflow_V2_Streaming_JavaVersions.yml | 2 +- ...s_Java_ParDo_SparkStructuredStreaming_Batch.yml | 2 +- .github/workflows/beam_LoadTests_Java_PubsubIO.yml | 2 +- .../beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml | 2 +- ...m_LoadTests_Python_CoGBK_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Python_CoGBK_Flink_Batch.yml | 2 +- ...eam_LoadTests_Python_Combine_Dataflow_Batch.yml | 2 +- ...LoadTests_Python_Combine_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Python_Combine_Flink_Batch.yml | 2 +- ...am_LoadTests_Python_Combine_Flink_Streaming.yml | 2 +- ...LoadTests_Python_FnApiRunner_Microbenchmark.yml | 2 +- .../beam_LoadTests_Python_GBK_Dataflow_Batch.yml | 2 +- ...eam_LoadTests_Python_GBK_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Python_GBK_Flink_Batch.yml | 2 +- ...adTests_Python_GBK_reiterate_Dataflow_Batch.yml | 2 +- ...sts_Python_GBK_reiterate_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Python_ParDo_Dataflow_Batch.yml | 2 +- ...m_LoadTests_Python_ParDo_Dataflow_Streaming.yml | 2 +- .../beam_LoadTests_Python_ParDo_Flink_Batch.yml | 2 +- ...beam_LoadTests_Python_ParDo_Flink_Streaming.yml | 2 +- ...m_LoadTests_Python_SideInput_Dataflow_Batch.yml | 2 +- .github/workflows/beam_LoadTests_Python_Smoke.yml | 2 +- .../workflows/beam_MetricsCredentialsRotation.yml | 2 +- .github/workflows/beam_Metrics_Report.yml | 2 +- .../workflows/beam_PerformanceTests_AvroIOIT.yml | 2 +- .../beam_PerformanceTests_AvroIOIT_HDFS.yml | 2 +- ...PerformanceTests_BigQueryIO_Batch_Java_Avro.yml | 2 +- ...PerformanceTests_BigQueryIO_Batch_Java_Json.yml | 2 +- ..._PerformanceTests_BigQueryIO_Streaming_Java.yml | 2 +- ...eam_PerformanceTests_BiqQueryIO_Read_Python.yml | 2 +- ...formanceTests_BiqQueryIO_Write_Python_Batch.yml | 2 +- .github/workflows/beam_PerformanceTests_Cdap.yml | 2 +- .../beam_PerformanceTests_Compressed_TextIOIT.yml | 2 +- ...m_PerformanceTests_Compressed_TextIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_HadoopFormat.yml | 2 +- .github/workflows/beam_PerformanceTests_JDBC.yml | 2 +- .../workflows/beam_PerformanceTests_Kafka_IO.yml | 2 +- .../beam_PerformanceTests_ManyFiles_TextIOIT.yml | 2 +- ...am_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_MongoDBIO_IT.yml | 2 +- .../beam_PerformanceTests_ParquetIOIT.yml | 2 +- .../beam_PerformanceTests_ParquetIOIT_HDFS.yml | 2 +- ...erformanceTests_PubsubIOIT_Python_Streaming.yml | 2 +- ...m_PerformanceTests_SQLBigQueryIO_Batch_Java.yml | 2 +- .../beam_PerformanceTests_SingleStoreIO.yml | 2 +- ..._PerformanceTests_SpannerIO_Read_2GB_Python.yml | 2 +- ...manceTests_SpannerIO_Write_2GB_Python_Batch.yml | 2 +- .../beam_PerformanceTests_SparkReceiver_IO.yml | 2 +- .../beam_PerformanceTests_TFRecordIOIT.yml | 2 +- .../beam_PerformanceTests_TFRecordIOIT_HDFS.yml | 2 +- .../workflows/beam_PerformanceTests_TextIOIT.yml | 2 +- .../beam_PerformanceTests_TextIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_TextIOIT_Python.yml | 2 +- ...PerformanceTests_WordCountIT_PythonVersions.yml | 2 +- .../workflows/beam_PerformanceTests_XmlIOIT.yml | 2 +- .../beam_PerformanceTests_XmlIOIT_HDFS.yml | 2 +- .../beam_PerformanceTests_xlang_KafkaIO_Python.yml | 2 +- .github/workflows/beam_Playground_CI_Nightly.yml | 2 +- .github/workflows/beam_Playground_Precommit.yml | 2 +- .github/workflows/beam_PostCommit_Go.yml | 2 +- .../workflows/beam_PostCommit_Go_Dataflow_ARM.yml | 2 +- .github/workflows/beam_PostCommit_Go_VR_Flink.yml | 2 +- .github/workflows/beam_PostCommit_Go_VR_Spark.yml | 2 +- .github/workflows/beam_PostCommit_Java.yml | 2 +- .../beam_PostCommit_Java_Avro_Versions.yml | 2 +- .../beam_PostCommit_Java_BigQueryEarlyRollout.yml | 2 +- .../workflows/beam_PostCommit_Java_DataflowV1.yml | 2 +- .../workflows/beam_PostCommit_Java_DataflowV2.yml | 2 +- .../beam_PostCommit_Java_Examples_Dataflow.yml | 2 +- .../beam_PostCommit_Java_Examples_Dataflow_ARM.yml | 2 +- ...beam_PostCommit_Java_Examples_Dataflow_Java.yml | 2 +- .../beam_PostCommit_Java_Examples_Dataflow_V2.yml | 2 +- ...m_PostCommit_Java_Examples_Dataflow_V2_Java.yml | 2 +- .../beam_PostCommit_Java_Examples_Direct.yml | 2 +- .../beam_PostCommit_Java_Examples_Flink.yml | 2 +- .../beam_PostCommit_Java_Examples_Spark.yml | 2 +- .../beam_PostCommit_Java_Hadoop_Versions.yml | 2 +- .../beam_PostCommit_Java_IO_Performance_Tests.yml | 4 ++-- .../beam_PostCommit_Java_InfluxDbIO_IT.yml | 2 +- .../beam_PostCommit_Java_Jpms_Dataflow.yml | 2 +- ...beam_PostCommit_Java_Jpms_Dataflow_Versions.yml | 2 +- .../workflows/beam_PostCommit_Java_Jpms_Direct.yml | 2 +- .../beam_PostCommit_Java_Jpms_Direct_Versions.yml | 2 +- .../beam_PostCommit_Java_Jpms_Flink_Java11.yml | 2 +- .../beam_PostCommit_Java_Jpms_Spark_Java11.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Dataflow.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Dataflow_V2.yml | 2 +- ...am_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Direct.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Flink.yml | 2 +- .../beam_PostCommit_Java_Nexmark_Spark.yml | 2 +- .../beam_PostCommit_Java_PVR_Flink_Batch.yml | 2 +- .../beam_PostCommit_Java_PVR_Flink_Streaming.yml | 2 +- .../beam_PostCommit_Java_PVR_Spark3_Streaming.yml | 2 +- .../beam_PostCommit_Java_PVR_Spark4_Batch.yml | 2 +- .../beam_PostCommit_Java_PVR_Spark4_Streaming.yml | 2 +- .../beam_PostCommit_Java_PVR_Spark_Batch.yml | 2 +- .../beam_PostCommit_Java_SingleStoreIO_IT.yml | 2 +- .../beam_PostCommit_Java_Tpcds_Dataflow.yml | 2 +- .../workflows/beam_PostCommit_Java_Tpcds_Flink.yml | 2 +- .../workflows/beam_PostCommit_Java_Tpcds_Spark.yml | 2 +- ...am_PostCommit_Java_ValidatesRunner_Dataflow.yml | 2 +- ..._Java_ValidatesRunner_Dataflow_JavaVersions.yml | 2 +- ...mit_Java_ValidatesRunner_Dataflow_Streaming.yml | 2 +- ...a_ValidatesRunner_Dataflow_Streaming_Engine.yml | 2 +- ...atesRunner_Dataflow_Streaming_TagEncodingV2.yml | 2 +- ...PostCommit_Java_ValidatesRunner_Dataflow_V2.yml | 2 +- ..._Java_ValidatesRunner_Dataflow_V2_Streaming.yml | 2 +- ...beam_PostCommit_Java_ValidatesRunner_Direct.yml | 2 +- ...it_Java_ValidatesRunner_Direct_JavaVersions.yml | 2 +- .../beam_PostCommit_Java_ValidatesRunner_Flink.yml | 2 +- .../beam_PostCommit_Java_ValidatesRunner_Spark.yml | 2 +- ...beam_PostCommit_Java_ValidatesRunner_Spark4.yml | 2 +- ...va_ValidatesRunner_SparkStructuredStreaming.yml | 2 +- ...am_PostCommit_Java_ValidatesRunner_Twister2.yml | 2 +- .../beam_PostCommit_Java_ValidatesRunner_ULR.yml | 2 +- .github/workflows/beam_PostCommit_Javadoc.yml | 2 +- .../beam_PostCommit_PortableJar_Flink.yml | 2 +- .../beam_PostCommit_PortableJar_Spark.yml | 2 +- .github/workflows/beam_PostCommit_Python.yml | 2 +- .github/workflows/beam_PostCommit_Python_Arm.yml | 2 +- .../beam_PostCommit_Python_Dependency.yml | 2 +- .../beam_PostCommit_Python_Examples_Dataflow.yml | 2 +- .../beam_PostCommit_Python_Examples_Direct.yml | 2 +- .../beam_PostCommit_Python_Examples_Flink.yml | 2 +- .../beam_PostCommit_Python_Examples_Spark.yml | 2 +- .../beam_PostCommit_Python_MongoDBIO_IT.yml | 2 +- .../beam_PostCommit_Python_Nexmark_Direct.yml | 2 +- .../beam_PostCommit_Python_Portable_Flink.yml | 2 +- ...stCommit_Python_ValidatesContainer_Dataflow.yml | 2 +- ..._Python_ValidatesContainer_Dataflow_With_RC.yml | 2 +- ..._PostCommit_Python_ValidatesRunner_Dataflow.yml | 2 +- ...eam_PostCommit_Python_ValidatesRunner_Flink.yml | 2 +- ...eam_PostCommit_Python_ValidatesRunner_Spark.yml | 2 +- .../workflows/beam_PostCommit_Python_Versions.yml | 2 +- .../beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml | 2 +- .../beam_PostCommit_Python_Xlang_Gcp_Direct.yml | 2 +- .../beam_PostCommit_Python_Xlang_IO_Dataflow.yml | 2 +- .../beam_PostCommit_Python_Xlang_IO_Direct.yml | 2 +- .github/workflows/beam_PostCommit_SQL.yml | 2 +- .../beam_PostCommit_TransformService_Direct.yml | 2 +- .github/workflows/beam_PostCommit_Website_Test.yml | 2 +- .github/workflows/beam_PostCommit_XVR_Direct.yml | 2 +- .github/workflows/beam_PostCommit_XVR_Flink.yml | 2 +- .../beam_PostCommit_XVR_GoUsingJava_Dataflow.yml | 2 +- ...eam_PostCommit_XVR_JavaUsingPython_Dataflow.yml | 2 +- ..._PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml | 2 +- ...eam_PostCommit_XVR_PythonUsingJava_Dataflow.yml | 2 +- .github/workflows/beam_PostCommit_XVR_Spark3.yml | 2 +- .../beam_PostCommit_Yaml_Xlang_Direct.yml | 2 +- .../workflows/beam_PostRelease_NightlySnapshot.yml | 2 +- .../workflows/beam_PreCommit_CommunityMetrics.yml | 2 +- .../workflows/beam_PreCommit_Flink_Container.yml | 2 +- .github/workflows/beam_PreCommit_GHA.yml | 2 +- .github/workflows/beam_PreCommit_Go.yml | 2 +- .github/workflows/beam_PreCommit_GoPortable.yml | 2 +- .github/workflows/beam_PreCommit_GoPrism.yml | 2 +- .github/workflows/beam_PreCommit_ItFramework.yml | 2 +- .github/workflows/beam_PreCommit_Java.yml | 2 +- ...eCommit_Java_Amazon-Web-Services2_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Amqp_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Azure_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Cassandra_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Cdap_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Clickhouse_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Csv_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Datadog_IO_Direct.yml | 2 +- .github/workflows/beam_PreCommit_Java_Dataflow.yml | 2 +- .../beam_PreCommit_Java_Debezium_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Delta_IO_Direct.yml | 2 +- ...beam_PreCommit_Java_ElasticSearch_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Examples_Dataflow.yml | 2 +- ...eam_PreCommit_Java_Examples_Dataflow_Java21.yml | 2 +- ...Commit_Java_File-schema-transform_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Flink_Versions.yml | 2 +- .../beam_PreCommit_Java_GCP_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Google-ads_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_HBase_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_HCatalog_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Hadoop_IO_Direct.yml | 2 +- .../workflows/beam_PreCommit_Java_IOs_Direct.yml | 2 +- .../beam_PreCommit_Java_InfluxDb_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_JDBC_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Jms_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Kafka_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Kudu_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_MongoDb_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Mqtt_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Neo4j_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_PVR_Flink_Batch.yml | 2 +- .../beam_PreCommit_Java_PVR_Flink_Docker.yml | 2 +- .../beam_PreCommit_Java_PVR_Prism_Loopback.yml | 2 +- .../beam_PreCommit_Java_Parquet_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Pulsar_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_RabbitMq_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Redis_IO_Direct.yml | 2 +- ...am_PreCommit_Java_RequestResponse_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_SingleStore_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Snowflake_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Solace_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Solr_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Spark_Versions.yml | 2 +- .../beam_PreCommit_Java_Splunk_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Thrift_IO_Direct.yml | 2 +- .../beam_PreCommit_Java_Tika_IO_Direct.yml | 2 +- .../workflows/beam_PreCommit_Kotlin_Examples.yml | 2 +- .../workflows/beam_PreCommit_Portable_Python.yml | 2 +- .github/workflows/beam_PreCommit_Prism_Python.yml | 2 +- .github/workflows/beam_PreCommit_Python.yml | 2 +- .github/workflows/beam_PreCommit_PythonDocker.yml | 2 +- .github/workflows/beam_PreCommit_PythonDocs.yml | 2 +- .../workflows/beam_PreCommit_PythonFormatter.yml | 2 +- .github/workflows/beam_PreCommit_PythonLint.yml | 2 +- .../workflows/beam_PreCommit_Python_Coverage.yml | 2 +- .../workflows/beam_PreCommit_Python_Dataframes.yml | 2 +- .github/workflows/beam_PreCommit_Python_Dill.yml | 2 +- .../workflows/beam_PreCommit_Python_Examples.yml | 2 +- .../beam_PreCommit_Python_Integration.yml | 2 +- .github/workflows/beam_PreCommit_Python_ML.yml | 2 +- .../workflows/beam_PreCommit_Python_PVR_Flink.yml | 2 +- .../workflows/beam_PreCommit_Python_Runners.yml | 2 +- .../workflows/beam_PreCommit_Python_Transforms.yml | 2 +- .github/workflows/beam_PreCommit_RAT.yml | 2 +- .github/workflows/beam_PreCommit_SQL.yml | 2 +- .github/workflows/beam_PreCommit_SQL_Java17.yml | 2 +- .github/workflows/beam_PreCommit_Spotless.yml | 2 +- .github/workflows/beam_PreCommit_Typescript.yml | 2 +- .github/workflows/beam_PreCommit_Website.yml | 2 +- .../workflows/beam_PreCommit_Website_Stage_GCS.yml | 2 +- .github/workflows/beam_PreCommit_Whitespace.yml | 2 +- .../beam_PreCommit_Xlang_Generated_Transforms.yml | 2 +- .../workflows/beam_PreCommit_Yaml_Xlang_Direct.yml | 2 +- .github/workflows/beam_Prober_CommunityMetrics.yml | 2 +- .github/workflows/beam_Publish_BeamMetrics.yml | 2 +- .../workflows/beam_Publish_Beam_SDK_Snapshots.yml | 2 +- .../workflows/beam_Publish_Docker_Snapshots.yml | 2 +- .github/workflows/beam_Publish_Website.yml | 4 ++-- .../beam_Python_CostBenchmarks_Dataflow.yml | 2 +- ...beam_Python_ValidatesContainer_Dataflow_ARM.yml | 2 +- .github/workflows/beam_Release_NightlySnapshot.yml | 2 +- .../beam_Release_Python_NightlySnapshot.yml | 2 +- .../workflows/beam_StressTests_Java_BigQueryIO.yml | 2 +- .../workflows/beam_StressTests_Java_BigTableIO.yml | 2 +- .../workflows/beam_StressTests_Java_KafkaIO.yml | 2 +- .../workflows/beam_StressTests_Java_PubSubIO.yml | 2 +- .../workflows/beam_StressTests_Java_SpannerIO.yml | 2 +- .github/workflows/build_release_candidate.yml | 14 +++++++------- .github/workflows/build_runner_image.yml | 2 +- .github/workflows/build_wheels.yml | 6 +++--- .github/workflows/cancel.yml | 2 +- .github/workflows/code_completion_plugin_tests.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/cut_release_branch.yml | 4 ++-- .github/workflows/dask_runner_tests.yml | 4 ++-- .../workflows/deploy_release_candidate_pypi.yaml | 2 +- .github/workflows/finalize_release.yml | 6 +++--- .github/workflows/flaky_test_detection.yml | 2 +- .github/workflows/git_tag_released_version.yml | 2 +- .github/workflows/go_tests.yml | 2 +- .github/workflows/issue-tagger.yml | 2 +- .github/workflows/java_tests.yml | 4 ++-- .github/workflows/local_env_tests.yml | 4 ++-- .github/workflows/playground_frontend_test.yml | 2 +- .github/workflows/pr-bot-new-prs.yml | 2 +- .github/workflows/pr-bot-pr-updates.yml | 2 +- .github/workflows/pr-bot-prs-needing-attention.yml | 2 +- .github/workflows/publish_github_release_notes.yml | 4 ++-- .github/workflows/python_dependency_tests.yml | 2 +- .github/workflows/python_tests.yml | 8 ++++---- .github/workflows/refresh_looker_metrics.yml | 2 +- .github/workflows/reportGenerator.yml | 2 +- .../republish_released_docker_containers.yml | 2 +- .github/workflows/run_perf_alert_tool.yml | 2 +- .../workflows/run_rc_validation_go_wordcount.yml | 2 +- .../run_rc_validation_java_mobile_gaming.yml | 2 +- .../run_rc_validation_java_quickstart.yml | 2 +- .../run_rc_validation_python_mobile_gaming.yml | 2 +- .../workflows/run_rc_validation_python_yaml.yml | 2 +- .github/workflows/tour_of_beam_backend.yml | 2 +- .../workflows/tour_of_beam_backend_integration.yml | 2 +- .github/workflows/tour_of_beam_frontend_test.yml | 2 +- .github/workflows/typescript_tests.yml | 8 ++++---- .github/workflows/update_python_dependencies.yml | 4 ++-- 331 files changed, 356 insertions(+), 356 deletions(-) diff --git a/.github/workflows/IO_Iceberg_Integration_Tests.yml b/.github/workflows/IO_Iceberg_Integration_Tests.yml index 6341c51124c5..f0daaf7966df 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests"] job_phrase: ["Run IcebergIO Integration Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml index 38a3a2514855..e0bb9a06f2d5 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests_Dataflow"] job_phrase: ["Run IcebergIO Integration Tests on Dataflow"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml index ba72a85204a1..60aa67a7081f 100644 --- a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Managed_Integration_Tests_Dataflow"] job_phrase: ["Run IcebergIO Managed Integration Tests on Dataflow"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Performance_Tests.yml b/.github/workflows/IO_Iceberg_Performance_Tests.yml index e3ec8b569012..f35923193937 100644 --- a/.github/workflows/IO_Iceberg_Performance_Tests.yml +++ b/.github/workflows/IO_Iceberg_Performance_Tests.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Performance_Tests"] job_phrase: ["Run IcebergIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Unit_Tests.yml b/.github/workflows/IO_Iceberg_Unit_Tests.yml index dd8069c74af0..6df57f1399a3 100644 --- a/.github/workflows/IO_Iceberg_Unit_Tests.yml +++ b/.github/workflows/IO_Iceberg_Unit_Tests.yml @@ -82,7 +82,7 @@ jobs: github.event.comment.body == 'Run IcebergIO Unit Tests' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/assign_milestone.yml b/.github/workflows/assign_milestone.yml index 542d2a551f64..3cc35ba342d2 100644 --- a/.github/workflows/assign_milestone.yml +++ b/.github/workflows/assign_milestone.yml @@ -31,7 +31,7 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 2 diff --git a/.github/workflows/beam_CancelStaleDataflowJobs.yml b/.github/workflows/beam_CancelStaleDataflowJobs.yml index 1328cf080c8c..62659a7b4238 100644 --- a/.github/workflows/beam_CancelStaleDataflowJobs.yml +++ b/.github/workflows/beam_CancelStaleDataflowJobs.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Cancel Stale Dataflow Jobs' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CleanUpDataprocResources.yml b/.github/workflows/beam_CleanUpDataprocResources.yml index bf1314b29519..57ce2229fbbe 100644 --- a/.github/workflows/beam_CleanUpDataprocResources.yml +++ b/.github/workflows/beam_CleanUpDataprocResources.yml @@ -55,7 +55,7 @@ jobs: timeout-minutes: 100 name: "beam_CleanUpDataprocResources" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Delete leaked resources for all the jobs that generates flink clusters run: | cd ${{ github.workspace }}/.test-infra/dataproc; ./cleanup.sh -xe \ No newline at end of file diff --git a/.github/workflows/beam_CleanUpGCPResources.yml b/.github/workflows/beam_CleanUpGCPResources.yml index d148b1e00b8f..480e48c1c210 100644 --- a/.github/workflows/beam_CleanUpGCPResources.yml +++ b/.github/workflows/beam_CleanUpGCPResources.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean GCP Resources' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml index 4378e98fe389..b2a686fb6e9b 100644 --- a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml +++ b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean Prebuilt Images' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml index 69ec18d225fc..85ba3f01e47c 100644 --- a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_CloudML_Benchmarks_Dataflow"] job_phrase: ["Run TFT Criteo Benchmarks"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_IODatastoresCredentialsRotation.yml b/.github/workflows/beam_IODatastoresCredentialsRotation.yml index 33327970a764..fdb570c4b61a 100644 --- a/.github/workflows/beam_IODatastoresCredentialsRotation.yml +++ b/.github/workflows/beam_IODatastoresCredentialsRotation.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_IODatastoresCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml index ae52cbb68e17..6bda0379bc7d 100644 --- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Inference_Python_Benchmarks_Dataflow"] job_phrase: ["Run Inference Benchmarks"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml index 75e5c00cb767..cfef684c6f9f 100644 --- a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml +++ b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml @@ -42,7 +42,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v4 diff --git a/.github/workflows/beam_Infrastructure_SecurityLogging.yml b/.github/workflows/beam_Infrastructure_SecurityLogging.yml index ea0a33a6d108..0af0cc81dffc 100644 --- a/.github/workflows/beam_Infrastructure_SecurityLogging.yml +++ b/.github/workflows/beam_Infrastructure_SecurityLogging.yml @@ -44,7 +44,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v4 diff --git a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml index b133a3543dfb..918116232b49 100644 --- a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml +++ b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml @@ -48,7 +48,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db diff --git a/.github/workflows/beam_Infrastructure_UsersPermissions.yml b/.github/workflows/beam_Infrastructure_UsersPermissions.yml index b084eec2a705..12d78b1925c3 100644 --- a/.github/workflows/beam_Infrastructure_UsersPermissions.yml +++ b/.github/workflows/beam_Infrastructure_UsersPermissions.yml @@ -45,7 +45,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.merged == true && github.base_ref || github.event.pull_request.head.sha }} - name: Setup gcloud diff --git a/.github/workflows/beam_Java_JMH.yml b/.github/workflows/beam_Java_JMH.yml index 68fcf0e91789..e2d88a89b40d 100644 --- a/.github/workflows/beam_Java_JMH.yml +++ b/.github/workflows/beam_Java_JMH.yml @@ -62,7 +62,7 @@ jobs: timeout-minutes: 900 name: "beam_Java_JMH" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action - name: run the Java JMH micro-benchmark harness suite diff --git a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml index 56b36dfdcb64..7fb9abb9a599 100644 --- a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml +++ b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Java_LoadTests_Combine_Smoke"] job_phrase: ["Run Java Load Tests Combine Smoke"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml index c682a0d359b9..0ec35e14536e 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Go CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml index 7efb22da4170..75cdee0a5e87 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_CoGBK_Flink_Batch"] job_phrase: ["Run Load Tests Go CoGBK Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml index 64d970024b71..49e9a51d8967 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Go Combine Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml index 044e987556ee..4e023f141da6 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_Combine_Flink_Batch"] job_phrase: ["Run Load Tests Go Combine Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml index aaddaaee7e0c..c346e4ef1139 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Go GBK Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml index 174aa25e5431..4d7b955c2ad2 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_GBK_Flink_Batch"] job_phrase: ["Run Load Tests Go GBK Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml index 96b2af740c38..4cfd1a8b21bf 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Go ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml index 7a02fd601290..745432a4a296 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_ParDo_Flink_Batch"] job_phrase: ["Run Load Tests Go ParDo Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml index 44a509615620..ccb73f51f9f5 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_SideInput_Dataflow_Batch"] job_phrase: ["Run Load Tests Go SideInput Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml index 22b66ad4c805..21109c0c89f2 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_SideInput_Flink_Batch"] job_phrase: ["Run Load Tests Go SideInput Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml index 461920600b98..d0ae3d337a4f 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Java CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml index 76cb31ae5ada..ccb77b6fcb8b 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java CoGBK Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml index 9e23fb7b42e7..c7798c6267f3 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml index daa1559cfb9d..e20c26dbcd2e 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml index 7501c70b7ab6..9769622b2cde 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java CoGBK SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml index 0de3f585e703..e1169fdeb2f9 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Java Combine Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml index 3651895a76d6..c2c6c3ab80bd 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java Combine Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml index 4840de2bb84c..5d4113a35d29 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java Combine SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml index bd662ac7f42e..91ad51daf0e0 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Java GBK Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml index 433d12a7a35a..4295085a4f8a 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java GBK Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml index 84b5e18f06e0..6237622e3959 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Batch"] job_phrase: ["Run Load Tests GBK Dataflow V2 Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml index 1899a5602d58..beb74f56d559 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17"] job_phrase: ["Run Load Tests Java 17 GBK Dataflow V2 Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml index c44747a3db7b..cb6dc88da539 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Streaming"] job_phrase: ["Run Load Tests GBK Dataflow V2 Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml index 5789be196d27..8bfe24be9a9d 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17"] job_phrase: ["Run Load Tests Java 17 GBK Dataflow V2 Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml index 9f3ab971c88e..fc143008eac4 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Smoke"] job_phrase: ["Run Java Load Tests GBK Smoke"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml index 9c1eb98b366f..40b0af0e8635 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java GBK SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml index 47046f000a0f..411c8fec77ab 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Java ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml index 37bcb095c126..0a490623daf5 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java ParDo Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml index 813efedd7984..cc80be262670 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml index e2ec6e8cbb4a..848632818c45 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml index d2a5a0748a00..20540be6a0d3 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java ParDo SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml index 6267bf959fde..915de7440603 100644 --- a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml +++ b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_PubsubIO"] job_phrase: ["Run Load Tests Java PubsubIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml index 16b53af2f698..cb12ef2bc4f9 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Python CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml index 85364a2964da..5f1774a0b6c8 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python CoGBK Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml index c603651a57ea..646a705eb85d 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Flink_Batch"] job_phrase: ["Run Load Tests Python CoGBK Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml index 60437c8439ed..8096fbeb5497 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Python Combine Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml index e275b14b9911..97682ce161c4 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python Combine Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml index 13168a002065..a72f2566cbba 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Flink_Batch"] job_phrase: ["Run Load Tests Python Combine Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml index 57e8b14df1be..cb2d45ffffc9 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Flink_Streaming"] job_phrase: ["Run Load Tests Python Combine Flink Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml index ec18d36cf583..b240daab30da 100644 --- a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml +++ b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_FnApiRunner_Microbenchmark"] job_phrase: ["Run Python Load Tests FnApiRunner Microbenchmark"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml index 6597e32ff808..1933569df097 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Python GBK Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml index d71c96c8b8a8..c9a75e714d34 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python GBK Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml index a792bf3ec720..2a8163a4a098 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Flink_Batch"] job_phrase: ["Run Load Tests Python GBK Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml index 235c9bd48b60..544db35178cd 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch"] job_phrase: ["Run Load Tests Python GBK reiterate Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml index 38a11a0ec329..fe7d19b8e758 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python GBK reiterate Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml index 508d1acd8786..f73ea2c3cf46 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Python ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml index 8efde4fa36d6..ea041470903f 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Dataflow_Streaming"] job_phrase: ["Run Python Load Tests ParDo Dataflow Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml index 3fefa634d2f6..f72ee9e98f2a 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Flink_Batch"] job_phrase: ["Run Load Tests Python ParDo Flink Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml index 26896710a8e3..345dc43386bc 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Flink_Streaming"] job_phrase: ["Run Load Tests Python ParDo Flink Streaming"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml index 2dc81f470e67..4ca3f7ef0276 100644 --- a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_SideInput_Dataflow_Batch"] job_phrase: ["Run Load Tests Python SideInput Dataflow Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Smoke.yml b/.github/workflows/beam_LoadTests_Python_Smoke.yml index d8f22e4b7b5d..9f70b0e98c57 100644 --- a/.github/workflows/beam_LoadTests_Python_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Python_Smoke.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_LoadTests_Python_Smoke"] job_phrase: ["Run Python Load Tests Smoke"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_MetricsCredentialsRotation.yml b/.github/workflows/beam_MetricsCredentialsRotation.yml index 3a8c8d424709..4498940783ba 100644 --- a/.github/workflows/beam_MetricsCredentialsRotation.yml +++ b/.github/workflows/beam_MetricsCredentialsRotation.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_MetricsCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Metrics_Report.yml b/.github/workflows/beam_Metrics_Report.yml index db07b31da7e3..75c2ff4c01cc 100644 --- a/.github/workflows/beam_Metrics_Report.yml +++ b/.github/workflows/beam_Metrics_Report.yml @@ -61,7 +61,7 @@ jobs: ) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml index 6ad99f853555..7cbffd8e8bf0 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT"] job_phrase: ["Run Java AvroIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml index f44129034a89..a7ec4307593d 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT_HDFS"] job_phrase: ["Run Java AvroIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml index 55f341472b0e..78584e824f27 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Batch_Java_Avro"] job_phrase: ["Run BigQueryIO Batch Performance Test Java Avro"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml index b7b933e5bb6f..c71d8a7f17ee 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Batch_Java_Json"] job_phrase: ["Run BigQueryIO Batch Performance Test Java Json"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml index 5c414c4a94fd..4fe06419e821 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Streaming_Java"] job_phrase: ["Run BigQueryIO Streaming Performance Test Java"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml index 58de7f6dfeac..97ba3bbd13d4 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BiqQueryIO_Read_Python"] job_phrase: ["Run BigQueryIO Read Performance Test Python"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml index d7cb776ae265..298fe697dfda 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BiqQueryIO_Write_Python_Batch"] job_phrase: ["Run BigQueryIO Write Performance Test Python"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Cdap.yml b/.github/workflows/beam_PerformanceTests_Cdap.yml index 94a43679470c..698c7924c23a 100644 --- a/.github/workflows/beam_PerformanceTests_Cdap.yml +++ b/.github/workflows/beam_PerformanceTests_Cdap.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Cdap"] job_phrase: ["Run Java CdapIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml index 017cb90d86e1..996984f4d085 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT"] job_phrase: ["Run Java CompressedTextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml index 3803d275b8a3..d0b1e0f0bd2b 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT_HDFS"] job_phrase: ["Run Java CompressedTextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml index dc6d5b6914dc..2f93007c8420 100644 --- a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml +++ b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_HadoopFormat"] job_phrase: ["Run Java HadoopFormatIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_JDBC.yml b/.github/workflows/beam_PerformanceTests_JDBC.yml index 97283963dee5..dd5e3752c2d3 100644 --- a/.github/workflows/beam_PerformanceTests_JDBC.yml +++ b/.github/workflows/beam_PerformanceTests_JDBC.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_JDBC"] job_phrase: ["Run Java JdbcIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml index fa13bceb6d76..a90fb2f17228 100644 --- a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml +++ b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml @@ -64,7 +64,7 @@ jobs: env: KAFKA_SERVICE_PORT: 32400 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml index feb909c1d338..f7a932545c22 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT"] job_phrase: ["Run Java ManyFilesTextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml index 3699d1cbace4..f2ad58c4e6e3 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT_HDFS"] job_phrase: ["Run Java ManyFilesTextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml index db2d786d09f5..5bd3d6a271f2 100644 --- a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_MongoDBIO_IT"] job_phrase: ["Run Java MongoDBIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml index b9e7d9caa7e5..7b187a636f54 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT"] job_phrase: ["Run Java ParquetIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml index af5b6680d6ea..7496a7e89e56 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT_HDFS"] job_phrase: ["Run Java ParquetIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml index 8ff05fb05f5d..4d23c0a832d2 100644 --- a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml +++ b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_PubsubIOIT_Python_Streaming"] job_phrase: ["Run PubsubIO Performance Test Python"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml index 8fb2c8710e09..07b7c30c5aed 100644 --- a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml +++ b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SQLBigQueryIO_Batch_Java"] job_phrase: ["Run SQLBigQueryIO Batch Performance Test Java"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml index 58ac703602a9..91063b956765 100644 --- a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml +++ b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml @@ -65,7 +65,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java SingleStoreIO Performance Test' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml index 52c296652df3..053aaff33cc4 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SpannerIO_Read_2GB_Python"] job_phrase: ["Run SpannerIO Read 2GB Performance Test Python"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml index 3996acedf195..beeaf2d67078 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch"] job_phrase: ["Run SpannerIO Write 2GB Performance Test Python Batch"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml index f170f3ca4a3a..7ac0cedb9cb6 100644 --- a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml +++ b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SparkReceiver_IO"] job_phrase: ["Run Java SparkReceiverIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml index 0490ca78cbee..6103e6180eee 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT"] job_phrase: ["Run Java TFRecordIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml index a2da2ca848e3..5a3f22f858fd 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT_HDFS"] job_phrase: ["Run Java TFRecordIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_TextIOIT.yml index 538866170fa6..a3cf7033f7eb 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT"] job_phrase: ["Run Java TextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml index e4e4a607580a..01820ac6bb70 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_HDFS"] job_phrase: ["Run Java TextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml index e396812b187c..c25afec144c7 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_Python"] job_phrase: ["Run Python TextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml index ccbe286fc2f1..927bc7960442 100644 --- a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml +++ b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: [WordCountIT Performance Test] python_version: ['3.10'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml index 6c3e3cdbdc99..135a80a8aef6 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT"] job_phrase: ["Run Java XmlIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml index 7a1f86f687fd..c11a2f34479f 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT_HDFS"] job_phrase: ["Run Java XmlIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml index 564f1be187d7..94c556e4c475 100644 --- a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml +++ b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PerfTests_xlang_KafkaIO_Python"] job_phrase: ["Run Python xlang KafkaIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Playground_CI_Nightly.yml b/.github/workflows/beam_Playground_CI_Nightly.yml index fc79c6fda9bc..539cdeddc2b8 100644 --- a/.github/workflows/beam_Playground_CI_Nightly.yml +++ b/.github/workflows/beam_Playground_CI_Nightly.yml @@ -61,7 +61,7 @@ jobs: sdk: ["python", "java", "go"] fail-fast: false steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index d04863743dd7..da253cc11bfd 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -47,7 +47,7 @@ jobs: PYTHON_VERSION: '3.10' JAVA_VERSION: '17' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go.yml b/.github/workflows/beam_PostCommit_Go.yml index aacaa4f8171b..3da9821cefad 100644 --- a/.github/workflows/beam_PostCommit_Go.yml +++ b/.github/workflows/beam_PostCommit_Go.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go"] job_phrase: ["Run Go PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml index 71942e21ee30..64667f10736c 100644 --- a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml @@ -65,7 +65,7 @@ jobs: job_name: ["beam_PostCommit_Go_Dataflow_ARM"] job_phrase: ["Run Go PostCommit Dataflow ARM"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml index 6ea7ee837716..d476e36a4cc2 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Flink"] job_phrase: ["Run Go Flink ValidatesRunner"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml index abbded6c3bcf..34df660a2b55 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Spark"] job_phrase: ["Run Go Spark ValidatesRunner"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java.yml b/.github/workflows/beam_PostCommit_Java.yml index 98c7a6bfae06..74f1c0a13b69 100644 --- a/.github/workflows/beam_PostCommit_Java.yml +++ b/.github/workflows/beam_PostCommit_Java.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java PostCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml index 4ca15d53aa20..f702e0a15690 100644 --- a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Avro Versions PostCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml index 61481b1cd8ea..89201eadcd5f 100644 --- a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml +++ b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java BigQueryEarlyRollout PostCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml index 76f57afa2489..35eb3d331c0f 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_Dataflow' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml index d87685a63c25..5c38e94ed7dd 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_DataflowV2' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml index 5e6e090a9794..4acbb637a6d7 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples on Dataflow' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml index 57069b34fef0..fb6b6df52768 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'pull_request_target' || startswith(github.event.comment.body, 'Run Java_Examples_Dataflow_ARM PostCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml index c45725652dc6..a33513af45ac 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Java examples on Dataflow Java') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml index 274e579c19ed..4b95f1674664 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples on Dataflow Runner V2' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml index 70e8a40023c3..a2ef615b8c5f 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml @@ -69,7 +69,7 @@ jobs: (contains(github.event.comment.body, 'Run Java') && contains(github.event.comment.body, 'Examples on Dataflow Runner V2')) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml index ef6f7f3b77a0..27fbbdd82bbc 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Direct' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml index de984a4b2bb7..765953d23d51 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Flink' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml index 380167cc99a2..caa22f10e0ff 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Spark' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml index 4ae2b3c6dd74..37102fa1918e 100644 --- a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_Hadoop_Versions' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml index 8820e32812ea..db43441f2cda 100644 --- a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml +++ b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml @@ -66,7 +66,7 @@ jobs: job_phrase: ["Run Java PostCommit IO Performance Tests"] test_case: ["GCSPerformanceTest", "BigTablePerformanceTest", "BigQueryStorageApiStreamingPerformanceTest"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: echo "BEAM_VERSION=${BEAM_VERSION}" >> $GITHUB_ENV - name: Checkout release branch if: github.event_name == 'schedule' #This has scheduled runs run against the latest release - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ env.BEAM_VERSION }} repository: apache/beam diff --git a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml index 2a12e9652c28..91f79a68c56c 100644 --- a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml @@ -65,7 +65,7 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java InfluxDbIO_IT' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml index 42d80bbbe6a6..b3b1e54ae9ea 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Dataflow"] job_phrase: ["Run Jpms Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml index b5ac893f60b8..de1b6417f8e9 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run Jpms Dataflow Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml index 6c1f97f37855..dd627b7bd235 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Direct"] job_phrase: ["Run Jpms Direct PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml index f9bbdb93e0c4..fa2f0c5753b4 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run Jpms Direct Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml index e6dd44033f4a..1501eae26b61 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Flink_Java11"] job_phrase: ["Run Jpms Flink Java 11 PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml index 307d55c16d96..21ea989f2758 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Spark_Java11"] job_phrase: ["Run Jpms Spark Java 11 PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml index d648a8c4ba08..ff94638922fa 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml @@ -93,7 +93,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Runner Nexmark Tests' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml index 4c91daf38bec..81916d8fe7a5 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml @@ -93,7 +93,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Runner V2 Nexmark Tests' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml index 82f862a837ed..a58e814c5567 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml @@ -95,7 +95,7 @@ jobs: (contains(github.event.comment.body, 'Run Dataflow Runner V2 Java') && contains(github.event.comment.body, 'Nexmark Tests')) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml index db8eff3e748c..5175f8db2176 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml @@ -88,7 +88,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Direct Runner Nexmark Tests' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml index 4a3574906a28..96470ca3e1bf 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml @@ -87,7 +87,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Flink Runner Nexmark Tests' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml index f397c7ae18a5..99ed344def91 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml @@ -87,7 +87,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark Runner Nexmark Tests' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml index da8c29e4955b..10138c8425e4 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml @@ -79,7 +79,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Flink_Batch PostCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml index f3c6560f994f..45d9a95ac9b1 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Flink PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml index 3001ce0debd3..ce51a09ada02 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Spark v3 PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml index 9a34099cebb9..4442e8c55ff1 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml @@ -64,7 +64,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml index 38866bedeb1b..7a66f317c974 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Spark v4 PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml index e2ba7ec824ee..f14e32ad2901 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml @@ -64,7 +64,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml index 33dcc3bd3bb7..3fb73d6c1311 100644 --- a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml @@ -67,7 +67,7 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java SingleStoreIO_IT' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml index 25890c3545e3..ea822f0895e0 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml @@ -90,7 +90,7 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Dataflow"] job_phrase: ["Run Dataflow Runner Tpcds Tests"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml index 49416266b487..a96dc9416e24 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml @@ -87,7 +87,7 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Flink"] job_phrase: ["Run Flink Runner Tpcds Tests"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml index 557db1c89794..2204670a6caf 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml @@ -86,7 +86,7 @@ jobs: job_phrase: ["Run Spark Runner Tpcds Tests"] runner: [SparkRunner, SparkStructuredStreamingRunner] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml index ef07fac8f807..1bdf330ffbb5 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml index d32e22e319be..750621a7c43b 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Dataflow ValidatesRunner Java') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml index f70cbf0db32f..7a4133d7c425 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Streaming ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml index 422eb989d09c..d1501a8696e6 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml @@ -64,7 +64,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Streaming Engine ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml index 12340233310a..204fd44ed9d1 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow Streaming TagEncodingV2 ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml index 39cd812d4776..c85d7c4cac63 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow V2 ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml index 1d64520d2cc7..679cc01cc0ea 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow V2 ValidatesRunner Streaming' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml index 29d42d8e6edc..c69a5ce9704a 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Direct ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml index 48fd46342d22..ec7f63b011f5 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Direct ValidatesRunner Java') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml index b55d6342cdbb..d33b3b412498 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Flink ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml index 48f328789042..9f8af5d6699e 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml index c6e742204894..704539ac92de 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark4 ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml index 5e000bfa1666..75bc7a1ddf94 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark StructuredStreaming ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml index 9a10cf8c2f9c..83e0bef0c0f5 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Twister2 ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml index 74400d0934ad..d84409f839e8 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run ULR Loopback ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Javadoc.yml b/.github/workflows/beam_PostCommit_Javadoc.yml index 538648c46e7d..53a967406f07 100644 --- a/.github/workflows/beam_PostCommit_Javadoc.yml +++ b/.github/workflows/beam_PostCommit_Javadoc.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Javadoc PostCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml index 9dd6efb968d7..0764e7d89fb4 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Flink"] job_phrase: ["Run PortableJar_Flink PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml index da9826febd53..5840f3d9b6d4 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Spark"] job_phrase: ["Run PortableJar_Spark PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python.yml b/.github/workflows/beam_PostCommit_Python.yml index 41f72fa50e92..5d9d45b54bd3 100644 --- a/.github/workflows/beam_PostCommit_Python.yml +++ b/.github/workflows/beam_PostCommit_Python.yml @@ -69,7 +69,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Python PostCommit 3.') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Arm.yml b/.github/workflows/beam_PostCommit_Python_Arm.yml index c226f1a771a8..cb4b3c7cf5cf 100644 --- a/.github/workflows/beam_PostCommit_Python_Arm.yml +++ b/.github/workflows/beam_PostCommit_Python_Arm.yml @@ -66,7 +66,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository diff --git a/.github/workflows/beam_PostCommit_Python_Dependency.yml b/.github/workflows/beam_PostCommit_Python_Dependency.yml index 18349ab173ec..1bd982860afd 100644 --- a/.github/workflows/beam_PostCommit_Python_Dependency.yml +++ b/.github/workflows/beam_PostCommit_Python_Dependency.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Python PostCommit Dependency') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml index eb80bf1e7e79..752036143e8e 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Examples_Dataflow"] job_phrase: ["Run Python Examples_Dataflow"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml index 368dba46c1c2..c5f02f1eed2b 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Direct"] python_version: ['3.10','3.11','3.12', '3.13','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml index 037dbca6a0a8..22607406cafb 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Flink"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml index 643ce2dcde28..4a2cb5663195 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Spark"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml index 305960e130e2..e01b765ff5e7 100644 --- a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_MongoDBIO_IT"] job_phrase: ["Run Python MongoDBIO_IT"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml index 71adf5c37e91..71038c3c8941 100644 --- a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml @@ -108,7 +108,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Python Direct Runner Nexmark Tests' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml index 78af1ff48e9a..adb4923d7092 100644 --- a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml @@ -67,7 +67,7 @@ jobs: # Run modes not covered by PreCommit_Python_PVR_Flink (i.e. other than 'LOOPBACK') environment_type: ['DOCKER'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml index 2c36e665dc63..c03e8999d805 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml @@ -67,7 +67,7 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesContainer"] python_version: ['3.10','3.11','3.12','3.13','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml index fddf74577243..069836fabea3 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python RC Dataflow ValidatesContainer"] python_version: ['3.10','3.11','3.12','3.13','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml index 1b3aa2890783..4b57013dc4f0 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml index 1b769788fd51..d7fec110ea52 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Flink ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml index fc68dc021db5..7d3faaa844ad 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Spark ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Versions.yml b/.github/workflows/beam_PostCommit_Python_Versions.yml index c0f79faefdda..b7dd2f500ebd 100644 --- a/.github/workflows/beam_PostCommit_Python_Versions.yml +++ b/.github/workflows/beam_PostCommit_Python_Versions.yml @@ -88,7 +88,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml index 2c442af725e6..3202c29a89ab 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Gcp_Dataflow"] job_phrase: ["Run Python_Xlang_Gcp_Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml index 5dc1ed0dbaf2..1862512a83db 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Gcp_Direct"] job_phrase: ["Run Python_Xlang_Gcp_Direct PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml index 7bfe29e4b509..036ba9e25f39 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_IO_Dataflow"] job_phrase: ["Run Python_Xlang_IO_Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml index ebb891dae457..d091a6bd6060 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_IO_Direct"] job_phrase: ["Run Python_Xlang_IO_Direct PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_SQL.yml b/.github/workflows/beam_PostCommit_SQL.yml index 54fde69e28a0..8c198aa6f38a 100644 --- a/.github/workflows/beam_PostCommit_SQL.yml +++ b/.github/workflows/beam_PostCommit_SQL.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run SQL PostCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_TransformService_Direct.yml b/.github/workflows/beam_PostCommit_TransformService_Direct.yml index 2111afa48015..0da5ed93b202 100644 --- a/.github/workflows/beam_PostCommit_TransformService_Direct.yml +++ b/.github/workflows/beam_PostCommit_TransformService_Direct.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run TransformService_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Website_Test.yml b/.github/workflows/beam_PostCommit_Website_Test.yml index c58a2727fbe8..a0bfce216996 100644 --- a/.github/workflows/beam_PostCommit_Website_Test.yml +++ b/.github/workflows/beam_PostCommit_Website_Test.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Website_Test"] job_phrase: ["Run Full Website Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_Direct.yml b/.github/workflows/beam_PostCommit_XVR_Direct.yml index d95fe3bec3d0..c8e03769bd04 100644 --- a/.github/workflows/beam_PostCommit_XVR_Direct.yml +++ b/.github/workflows/beam_PostCommit_XVR_Direct.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml b/.github/workflows/beam_PostCommit_XVR_Flink.yml index ac52e88f529c..273cf5c994cc 100644 --- a/.github/workflows/beam_PostCommit_XVR_Flink.yml +++ b/.github/workflows/beam_PostCommit_XVR_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run XVR_Flink PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml index 78e5c8afc3f0..d9f58f649e0a 100644 --- a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml @@ -66,7 +66,7 @@ jobs: job_name: ["beam_PostCommit_XVR_GoUsingJava_Dataflow"] job_phrase: ["Run XVR_GoUsingJava_Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml index 32dbbee7842a..68fa1e646311 100644 --- a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_JavaUsingPython_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml index 8ec8a941ed48..c180e43cc8cc 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow"] job_phrase: ["Run XVR_PythonUsingJavaSQL_Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml index acf4c47ad2c1..1c7bc806c035 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_PythonUsingJava_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_Spark3.yml b/.github/workflows/beam_PostCommit_XVR_Spark3.yml index 2db33583a317..e303e80a7ea3 100644 --- a/.github/workflows/beam_PostCommit_XVR_Spark3.yml +++ b/.github/workflows/beam_PostCommit_XVR_Spark3.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_Spark3 PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index e1887ce7864e..740c901a2d7b 100644 --- a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml @@ -63,7 +63,7 @@ jobs: job_phrase: ["Run Yaml_Xlang_Direct PostCommit"] test_set: ["data", "databases", "messaging"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostRelease_NightlySnapshot.yml b/.github/workflows/beam_PostRelease_NightlySnapshot.yml index 5e811fa47581..f83b6499e582 100644 --- a/.github/workflows/beam_PostRelease_NightlySnapshot.yml +++ b/.github/workflows/beam_PostRelease_NightlySnapshot.yml @@ -54,7 +54,7 @@ jobs: name: beam_PostRelease_NightlySnapshot runs-on: [self-hosted, ubuntu-24.04, highmem] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_PreCommit_CommunityMetrics.yml b/.github/workflows/beam_PreCommit_CommunityMetrics.yml index 72bb47fd7314..97fd004c2937 100644 --- a/.github/workflows/beam_PreCommit_CommunityMetrics.yml +++ b/.github/workflows/beam_PreCommit_CommunityMetrics.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run CommunityMetrics PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Flink_Container.yml b/.github/workflows/beam_PreCommit_Flink_Container.yml index 1f6c209cfd2b..7e9571dca6f3 100644 --- a/.github/workflows/beam_PreCommit_Flink_Container.yml +++ b/.github/workflows/beam_PreCommit_Flink_Container.yml @@ -98,7 +98,7 @@ jobs: job_name: ["beam_PreCommit_Flink_Container"] job_phrase: ["Run Flink Container PreCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GHA.yml b/.github/workflows/beam_PreCommit_GHA.yml index 76c5b1cba0fd..af5513a2835e 100644 --- a/.github/workflows/beam_PreCommit_GHA.yml +++ b/.github/workflows/beam_PreCommit_GHA.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GHA PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Go.yml b/.github/workflows/beam_PreCommit_Go.yml index c382815bdbd1..4d64cba3675c 100644 --- a/.github/workflows/beam_PreCommit_Go.yml +++ b/.github/workflows/beam_PreCommit_Go.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Go PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GoPortable.yml b/.github/workflows/beam_PreCommit_GoPortable.yml index dad631236112..1d64bb3c23c6 100644 --- a/.github/workflows/beam_PreCommit_GoPortable.yml +++ b/.github/workflows/beam_PreCommit_GoPortable.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPortable PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GoPrism.yml b/.github/workflows/beam_PreCommit_GoPrism.yml index ce9679142a10..6ff29459a047 100644 --- a/.github/workflows/beam_PreCommit_GoPrism.yml +++ b/.github/workflows/beam_PreCommit_GoPrism.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPrism PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_ItFramework.yml b/.github/workflows/beam_PreCommit_ItFramework.yml index 5f30ac844703..dc00bea0ee7c 100644 --- a/.github/workflows/beam_PreCommit_ItFramework.yml +++ b/.github/workflows/beam_PreCommit_ItFramework.yml @@ -76,7 +76,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run It_Framework PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java.yml b/.github/workflows/beam_PreCommit_Java.yml index caa737abe624..b3e89b46218e 100644 --- a/.github/workflows/beam_PreCommit_Java.yml +++ b/.github/workflows/beam_PreCommit_Java.yml @@ -170,7 +170,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml index ff27933a1004..e3fdb9717313 100644 --- a/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_Amazon-Web-Services2_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml index 0a477eaec9b7..884f1f64667e 100644 --- a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Amqp_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml index 384a8fe5f83a..f06c3e6db095 100644 --- a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_Azure_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml index 5aeddb5107c6..c81e712e20af 100644 --- a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Cassandra_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml index 2617601216ea..0367a70b4a3c 100644 --- a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml @@ -80,7 +80,7 @@ jobs: github.event.comment.body == 'Run Java_Cdap_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml index 50368fdbd7e9..8671235d587a 100644 --- a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Clickhouse_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml index 1dcaeb168e45..f409ae559582 100644 --- a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Csv_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml index 7501b342959f..48e563c905fc 100644 --- a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Datadog_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Dataflow.yml index 53044970d117..3e477197f824 100644 --- a/.github/workflows/beam_PreCommit_Java_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Dataflow.yml @@ -77,7 +77,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java Dataflow Non-portable Worker PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml index dbd3dfef3e42..7b839616efac 100644 --- a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Debezium_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml index 617260e55b88..63439b38bc55 100644 --- a/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml @@ -82,7 +82,7 @@ jobs: github.event.comment.body == 'Run Java_Delta_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml index 5bc2d491de40..67eb433e4db5 100644 --- a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_ElasticSearch_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml index 2ff6f4b08764..dd58e8d474f9 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml @@ -87,7 +87,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Examples_Dataflow PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml index 455b12b73b39..2c4b607a1a5e 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml @@ -86,7 +86,7 @@ jobs: github.event.comment.body == 'Run Java_Examples_Dataflow_Java21 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml index 03b8beecfdc6..00f42519fd11 100644 --- a/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_File-schema-transform_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml index 541dc08146c9..7308963dc6d3 100644 --- a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml @@ -75,7 +75,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java_Flink_Versions PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml index 2cf942f5d488..eb9739c33b59 100644 --- a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_GCP_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml index 292d4bafb288..41e857a0ab42 100644 --- a/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Google-ads_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml index 4a7553ddcd2c..35d00af9aafd 100644 --- a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_HBase_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml index 0a9de29e968b..f8561eb7c856 100644 --- a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_HCatalog_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml index b8a085f8447f..6c0e93f37e3d 100644 --- a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml @@ -102,7 +102,7 @@ jobs: github.event.comment.body == 'Run Java_Hadoop_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml index 349d6d1dd06c..a3af909a9ad6 100644 --- a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml @@ -79,7 +79,7 @@ jobs: github.event.comment.body == 'Run Java_IOs_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml index dabac47afa4f..84177c33b5af 100644 --- a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_InfluxDb_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml index fd595d867a39..2ac937a34548 100644 --- a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_JDBC_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml index d032b22d3527..309a316a99a0 100644 --- a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Jms_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml index 94ac5a6fec8e..39c4b064c863 100644 --- a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml @@ -84,7 +84,7 @@ jobs: github.event.comment.body == 'Run Java_Kafka_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml index d60c67ab4e76..a4531c47aede 100644 --- a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Kudu_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml index 91bf02f0c0f3..54a512170cf6 100644 --- a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_MongoDb_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml index a202a12d57a6..0131ea6d7fe0 100644 --- a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Mqtt_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml index c17c69c9a5a6..224718385639 100644 --- a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_Neo4j_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml index 43549eb32c52..f4b1b4545e32 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml @@ -82,7 +82,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Flink_Batch PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml index 9d95d830614d..a173e38eb47d 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml @@ -87,7 +87,7 @@ jobs: github.event.comment.body == 'Run Java_PVR_Flink_Docker PreCommit' timeout-minutes: 240 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml index c66b4a133bcf..19be2efc087b 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml @@ -88,7 +88,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Prism_Loopback PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml index 2e6e4c4548ed..cf2d3f6ab203 100644 --- a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Parquet_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml index 59c5aeb249f8..03a037c87306 100644 --- a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Pulsar_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml index 296b36fbe228..3faf5a1a2daf 100644 --- a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_RabbitMq_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml index de5d80c9cb32..e38dd57fefda 100644 --- a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Redis_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml index 78b290fb45e4..15f83fc9dc87 100644 --- a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_RequestResponse_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml index b5816df45c2b..9f552343232a 100644 --- a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_SingleStore_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml index ce2d64387944..b78854779e51 100644 --- a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml @@ -80,7 +80,7 @@ jobs: github.event.comment.body == 'Run Java_Snowflake_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml index 42d8009c6b79..e72aa00bdd15 100644 --- a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Solace_IO_Direct PreCommit' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml index bcd6ea621282..ee2ea171dd4a 100644 --- a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Solr_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml index 6f8689d00caa..9821419da139 100644 --- a/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml @@ -78,7 +78,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Spark_Versions PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml index 797390c81683..405e82f4517d 100644 --- a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Splunk_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml index a85e8cdaf60b..1286ffcc7816 100644 --- a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Thrift_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml index c4cb837fb3a9..477397c1e30a 100644 --- a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Tika_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml index b05fff74e51c..35c3786c00e3 100644 --- a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml +++ b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml @@ -88,7 +88,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Kotlin_Examples PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Portable_Python.yml b/.github/workflows/beam_PreCommit_Portable_Python.yml index fedb485bac8c..2a616e7dd944 100644 --- a/.github/workflows/beam_PreCommit_Portable_Python.yml +++ b/.github/workflows/beam_PreCommit_Portable_Python.yml @@ -90,7 +90,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Portable_Python PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Prism_Python.yml b/.github/workflows/beam_PreCommit_Prism_Python.yml index ca2e6a08a23c..4571b5b3c8c8 100644 --- a/.github/workflows/beam_PreCommit_Prism_Python.yml +++ b/.github/workflows/beam_PreCommit_Prism_Python.yml @@ -84,7 +84,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Prism_Python PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python.yml b/.github/workflows/beam_PreCommit_Python.yml index 4ba7a4758592..d856f011a64a 100644 --- a/.github/workflows/beam_PreCommit_Python.yml +++ b/.github/workflows/beam_PreCommit_Python.yml @@ -91,7 +91,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonDocker.yml b/.github/workflows/beam_PreCommit_PythonDocker.yml index 34b8a2c4f02c..ca950aac2116 100644 --- a/.github/workflows/beam_PreCommit_PythonDocker.yml +++ b/.github/workflows/beam_PreCommit_PythonDocker.yml @@ -69,7 +69,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonDocs.yml b/.github/workflows/beam_PreCommit_PythonDocs.yml index b4fca4070277..9d7caa15f858 100644 --- a/.github/workflows/beam_PreCommit_PythonDocs.yml +++ b/.github/workflows/beam_PreCommit_PythonDocs.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonDocs PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonFormatter.yml b/.github/workflows/beam_PreCommit_PythonFormatter.yml index 5d06cdb8d6cc..bbc04512c80d 100644 --- a/.github/workflows/beam_PreCommit_PythonFormatter.yml +++ b/.github/workflows/beam_PreCommit_PythonFormatter.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonFormatter PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonLint.yml b/.github/workflows/beam_PreCommit_PythonLint.yml index 2ed76a8098d1..f4c15b5a0115 100644 --- a/.github/workflows/beam_PreCommit_PythonLint.yml +++ b/.github/workflows/beam_PreCommit_PythonLint.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonLint PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Coverage.yml b/.github/workflows/beam_PreCommit_Python_Coverage.yml index ad27dfc5de01..4799c1e7b09e 100644 --- a/.github/workflows/beam_PreCommit_Python_Coverage.yml +++ b/.github/workflows/beam_PreCommit_Python_Coverage.yml @@ -74,7 +74,7 @@ jobs: github.event_name == 'workflow_dispatch' || startswith(github.event.comment.body, 'Run Python_Coverage PreCommit 3.') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Dataframes.yml b/.github/workflows/beam_PreCommit_Python_Dataframes.yml index 3266ea1b5a52..f80b8c0d298a 100644 --- a/.github/workflows/beam_PreCommit_Python_Dataframes.yml +++ b/.github/workflows/beam_PreCommit_Python_Dataframes.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dataframes PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Dill.yml b/.github/workflows/beam_PreCommit_Python_Dill.yml index 3ee44408d1a0..6a807bda69c1 100644 --- a/.github/workflows/beam_PreCommit_Python_Dill.yml +++ b/.github/workflows/beam_PreCommit_Python_Dill.yml @@ -81,7 +81,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dill PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Examples.yml b/.github/workflows/beam_PreCommit_Python_Examples.yml index 0fedaa6437ae..c8608a1909ff 100644 --- a/.github/workflows/beam_PreCommit_Python_Examples.yml +++ b/.github/workflows/beam_PreCommit_Python_Examples.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Examples PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Integration.yml b/.github/workflows/beam_PreCommit_Python_Integration.yml index 4228c3c05c9f..22a334ee442d 100644 --- a/.github/workflows/beam_PreCommit_Python_Integration.yml +++ b/.github/workflows/beam_PreCommit_Python_Integration.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Integration PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_ML.yml b/.github/workflows/beam_PreCommit_Python_ML.yml index 94a953f82d40..568ae36c1bef 100644 --- a/.github/workflows/beam_PreCommit_Python_ML.yml +++ b/.github/workflows/beam_PreCommit_Python_ML.yml @@ -87,7 +87,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_ML PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) if: contains(matrix.os, 'ubuntu-latest') uses: jlumbroso/free-disk-space@v1.3.1 diff --git a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml index 5fce132513ce..2125eb41bfb3 100644 --- a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml +++ b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml @@ -90,7 +90,7 @@ jobs: github.event.comment.body == 'Run Python_PVR_Flink PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Runners.yml b/.github/workflows/beam_PreCommit_Python_Runners.yml index 82e106fb778e..70d9ed4cd02f 100644 --- a/.github/workflows/beam_PreCommit_Python_Runners.yml +++ b/.github/workflows/beam_PreCommit_Python_Runners.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Runners PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Transforms.yml b/.github/workflows/beam_PreCommit_Python_Transforms.yml index df13c2e41ee1..24c86947abce 100644 --- a/.github/workflows/beam_PreCommit_Python_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Python_Transforms.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Transforms PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_RAT.yml b/.github/workflows/beam_PreCommit_RAT.yml index 46a648180844..a8fc9ce32b3c 100644 --- a/.github/workflows/beam_PreCommit_RAT.yml +++ b/.github/workflows/beam_PreCommit_RAT.yml @@ -69,7 +69,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run RAT PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_SQL.yml b/.github/workflows/beam_PreCommit_SQL.yml index 1176976b0332..7f4cd810ffd7 100644 --- a/.github/workflows/beam_PreCommit_SQL.yml +++ b/.github/workflows/beam_PreCommit_SQL.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run SQL PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_SQL_Java17.yml b/.github/workflows/beam_PreCommit_SQL_Java17.yml index 4bb5a6a5d69f..ddd508e79e29 100644 --- a/.github/workflows/beam_PreCommit_SQL_Java17.yml +++ b/.github/workflows/beam_PreCommit_SQL_Java17.yml @@ -71,7 +71,7 @@ jobs: github.event.comment.body == 'Run SQL_Java17 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Spotless.yml b/.github/workflows/beam_PreCommit_Spotless.yml index 145e31c4448a..64701930e856 100644 --- a/.github/workflows/beam_PreCommit_Spotless.yml +++ b/.github/workflows/beam_PreCommit_Spotless.yml @@ -76,7 +76,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Spotless PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Typescript.yml b/.github/workflows/beam_PreCommit_Typescript.yml index 1fe8898d6d8e..a20f96f79105 100644 --- a/.github/workflows/beam_PreCommit_Typescript.yml +++ b/.github/workflows/beam_PreCommit_Typescript.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Typescript PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Website.yml b/.github/workflows/beam_PreCommit_Website.yml index 103735e0b151..59718f02c7b1 100644 --- a/.github/workflows/beam_PreCommit_Website.yml +++ b/.github/workflows/beam_PreCommit_Website.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml index 150cfe06bad3..0fdfe3974bb4 100644 --- a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml +++ b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website_Stage_GCS PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Whitespace.yml b/.github/workflows/beam_PreCommit_Whitespace.yml index 7c79a465d48a..a1f6beb7a623 100644 --- a/.github/workflows/beam_PreCommit_Whitespace.yml +++ b/.github/workflows/beam_PreCommit_Whitespace.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Whitespace PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml index 2f9741c01a01..df4162ec984c 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -92,7 +92,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Xlang_Generated_Transforms PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 10693af1bf4e..6a1b24539161 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -77,7 +77,7 @@ jobs: job_name: ["beam_PreCommit_Yaml_Xlang_Direct"] job_phrase: ["Run Yaml_Xlang_Direct PreCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Prober_CommunityMetrics.yml b/.github/workflows/beam_Prober_CommunityMetrics.yml index 7a37a93fdfc2..abe9e0e4e974 100644 --- a/.github/workflows/beam_Prober_CommunityMetrics.yml +++ b/.github/workflows/beam_Prober_CommunityMetrics.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Prober_CommunityMetrics"] job_phrase: ["Run Community Metrics Prober"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_BeamMetrics.yml b/.github/workflows/beam_Publish_BeamMetrics.yml index db2e791c7dbd..111913692feb 100644 --- a/.github/workflows/beam_Publish_BeamMetrics.yml +++ b/.github/workflows/beam_Publish_BeamMetrics.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PostCommit_BeamMetrics_Publish"] job_phrase: ["Run Beam Metrics Deployment"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index ebf2ec703f61..8ffd35c06baa 100644 --- a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml +++ b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml @@ -74,7 +74,7 @@ jobs: - "python:container:ml:py313:docker" - "java:expansion-service:container:docker" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml b/.github/workflows/beam_Publish_Docker_Snapshots.yml index 0895c226d479..c8251c3d913e 100644 --- a/.github/workflows/beam_Publish_Docker_Snapshots.yml +++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml @@ -61,7 +61,7 @@ jobs: job_name: ["beam_Publish_Docker_Snapshots"] job_phrase: ["Publish Docker Snapshots"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_Website.yml b/.github/workflows/beam_Publish_Website.yml index 51bed6a3eff3..4420878bd280 100644 --- a/.github/workflows/beam_Publish_Website.yml +++ b/.github/workflows/beam_Publish_Website.yml @@ -53,7 +53,7 @@ jobs: timeout-minutes: 30 name: beam_Publish_Website steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -69,4 +69,4 @@ jobs: with: gradle-command: :website:clean :website:publishWebsite arguments: -PgitPublishRemote="https://github.com/apache/beam.git" -PgcpCredsFile="${{ steps.auth-gcp.outputs.credentials_file_path }}" - - uses: actions/checkout@v6 # Extra checkout to make sure we're on master for post steps. + - uses: actions/checkout@v7 # Extra checkout to make sure we're on master for post steps. diff --git a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml index 93f147626b7a..3a904db7d32e 100644 --- a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml +++ b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml @@ -61,7 +61,7 @@ jobs: job_name: ["beam_Python_CostBenchmark_Dataflow"] job_phrase: ["Run Python Dataflow Cost Benchmarks"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml index a7f35055b0b7..19c506c93ebe 100644 --- a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml +++ b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository diff --git a/.github/workflows/beam_Release_NightlySnapshot.yml b/.github/workflows/beam_Release_NightlySnapshot.yml index 38c2f635d2c5..012d46907f4d 100644 --- a/.github/workflows/beam_Release_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_NightlySnapshot.yml @@ -51,7 +51,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Release_Python_NightlySnapshot.yml b/.github/workflows/beam_Release_Python_NightlySnapshot.yml index 46c64b30f22f..e0c3ffd93011 100644 --- a/.github/workflows/beam_Release_Python_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_Python_NightlySnapshot.yml @@ -53,7 +53,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml index 690c4bdc8900..c2bdbd465568 100644 --- a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_BigQueryIO"] job_phrase: ["Run Stress Tests Java BigQueryIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_BigTableIO.yml b/.github/workflows/beam_StressTests_Java_BigTableIO.yml index f50f4499e3e4..2e5eb3a4ebdc 100644 --- a/.github/workflows/beam_StressTests_Java_BigTableIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigTableIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_BigTableIO"] job_phrase: ["Run Stress Tests Java BigTableIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_KafkaIO.yml b/.github/workflows/beam_StressTests_Java_KafkaIO.yml index d4dc1a20e918..b1ed4c8d5801 100644 --- a/.github/workflows/beam_StressTests_Java_KafkaIO.yml +++ b/.github/workflows/beam_StressTests_Java_KafkaIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_KafkaIO"] job_phrase: ["Run Stress Tests Java KafkaIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_PubSubIO.yml b/.github/workflows/beam_StressTests_Java_PubSubIO.yml index f6b25638bd00..ed911e128092 100644 --- a/.github/workflows/beam_StressTests_Java_PubSubIO.yml +++ b/.github/workflows/beam_StressTests_Java_PubSubIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_PubSubIO"] job_phrase: ["Run Stress Tests Java PubSubIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_SpannerIO.yml b/.github/workflows/beam_StressTests_Java_SpannerIO.yml index cb343cb70877..3f56af490d77 100644 --- a/.github/workflows/beam_StressTests_Java_SpannerIO.yml +++ b/.github/workflows/beam_StressTests_Java_SpannerIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_SpannerIO"] job_phrase: ["Run Stress Tests Java SpannerIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index 9e2a0c820178..3f9aa95ede15 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -61,7 +61,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -170,7 +170,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Mask Apache Password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -270,7 +270,7 @@ jobs: ] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -317,13 +317,13 @@ jobs: with: docker-images: false - name: Checkout Beam Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam path: beam - name: Checkout Beam Site Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: apache/beam-site path: beam-site @@ -433,7 +433,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -561,7 +561,7 @@ jobs: MANAGED_IO_DOCS_PATH: website/www/site/content/en/documentation/io/managed-io.md steps: - name: Checkout Beam Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index 7cd67ef0192e..4b556628e6c8 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} - name: GCloud Docker credential helper diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 532d946d4094..e4fc279d0b34 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -55,7 +55,7 @@ jobs: py-versions-full: ${{ steps.set-py-versions.outputs.py-versions-full }} py-versions-test: ${{ steps.set-py-versions.outputs.py-versions-test }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -88,7 +88,7 @@ jobs: rc_num: ${{ steps.get_rc_version.outputs.RC_NUM }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: @@ -365,7 +365,7 @@ jobs: if: github.repository_owner == 'apache' && github.event_name == 'schedule' steps: - name: Checkout code on master branch - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml index ab4df9fb4fd7..d8960a5a5815 100644 --- a/.github/workflows/cancel.yml +++ b/.github/workflows/cancel.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/code_completion_plugin_tests.yml b/.github/workflows/code_completion_plugin_tests.yml index 78022196fb29..a9930567d648 100644 --- a/.github/workflows/code_completion_plugin_tests.yml +++ b/.github/workflows/code_completion_plugin_tests.yml @@ -57,13 +57,13 @@ jobs: # Check out beam repository - name: Fetch beam Sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: main # Check out intellij community repository for tests - name: Fetch intellij-community Sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: JetBrains/intellij-community path: intellij diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ce9363b3d669..0a7aa09530a8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -74,7 +74,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go if: matrix.language == 'go' diff --git a/.github/workflows/cut_release_branch.yml b/.github/workflows/cut_release_branch.yml index c5da893ee5fd..ee571874ce13 100644 --- a/.github/workflows/cut_release_branch.yml +++ b/.github/workflows/cut_release_branch.yml @@ -61,7 +61,7 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -110,7 +110,7 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/dask_runner_tests.yml b/.github/workflows/dask_runner_tests.yml index 674b1ca866fb..2b279bd3ac2f 100644 --- a/.github/workflows/dask_runner_tests.yml +++ b/.github/workflows/dask_runner_tests.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: @@ -69,7 +69,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/deploy_release_candidate_pypi.yaml b/.github/workflows/deploy_release_candidate_pypi.yaml index 9fed87d42304..b8eb07c44172 100644 --- a/.github/workflows/deploy_release_candidate_pypi.yaml +++ b/.github/workflows/deploy_release_candidate_pypi.yaml @@ -30,7 +30,7 @@ jobs: PYPI_API_TOKEN=$(jq -r '.inputs.PYPI_API_TOKEN' $GITHUB_EVENT_PATH) echo "::add-mask::$PYPI_API_TOKEN" - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 6beb1dd4f45a..16c3c2df2c0f 100644 --- a/.github/workflows/finalize_release.yml +++ b/.github/workflows/finalize_release.yml @@ -98,7 +98,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Mask PyPi password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -141,7 +141,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ github.event.inputs.REPO_TOKEN }} repository: apache/beam @@ -185,7 +185,7 @@ jobs: POST_RELEASE_BRANCH: "release-${{ github.event.inputs.RELEASE }}-postrelease" steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ github.event.inputs.REPO_TOKEN }} ref: master diff --git a/.github/workflows/flaky_test_detection.yml b/.github/workflows/flaky_test_detection.yml index 733a15b2037a..30d51147c666 100644 --- a/.github/workflows/flaky_test_detection.yml +++ b/.github/workflows/flaky_test_detection.yml @@ -38,7 +38,7 @@ jobs: flaky-test-detection: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/git_tag_released_version.yml b/.github/workflows/git_tag_released_version.yml index 3ca113de90de..ecb63232be5c 100644 --- a/.github/workflows/git_tag_released_version.yml +++ b/.github/workflows/git_tag_released_version.yml @@ -37,7 +37,7 @@ jobs: VERSION_PATH: ${{ github.event.inputs.VERSION_TAG }} steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 61c26be9cee3..644d9a52cb12 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -40,7 +40,7 @@ jobs: name: Go Build steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 2 - name: Setup environment diff --git a/.github/workflows/issue-tagger.yml b/.github/workflows/issue-tagger.yml index 831bae7325cd..a893a168934f 100644 --- a/.github/workflows/issue-tagger.yml +++ b/.github/workflows/issue-tagger.yml @@ -24,7 +24,7 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: damccorm/tag-ur-it@6fa72bbf1a2ea157b533d7e7abeafdb5855dbea5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/java_tests.yml b/.github/workflows/java_tests.yml index eedb1b102980..dcd6b052fded 100644 --- a/.github/workflows/java_tests.yml +++ b/.github/workflows/java_tests.yml @@ -48,7 +48,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest, windows-latest] steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -105,7 +105,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest, windows-latest] steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/local_env_tests.yml b/.github/workflows/local_env_tests.yml index fdee2f3492ea..96682a46f4dd 100644 --- a/.github/workflows/local_env_tests.yml +++ b/.github/workflows/local_env_tests.yml @@ -45,7 +45,7 @@ jobs: name: "Ubuntu run local environment shell script" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -62,7 +62,7 @@ jobs: name: "Mac run local environment shell script" runs-on: macos-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/playground_frontend_test.yml b/.github/workflows/playground_frontend_test.yml index 9fc7ae83230d..c4c762704869 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -45,7 +45,7 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: 'Cache Flutter Dependencies' uses: actions/cache@v4 diff --git a/.github/workflows/pr-bot-new-prs.yml b/.github/workflows/pr-bot-new-prs.yml index 14254c364746..04219453b1ea 100644 --- a/.github/workflows/pr-bot-new-prs.yml +++ b/.github/workflows/pr-bot-new-prs.yml @@ -33,7 +33,7 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/pr-bot-pr-updates.yml b/.github/workflows/pr-bot-pr-updates.yml index 2038823b2f4d..8f31a70f29c0 100644 --- a/.github/workflows/pr-bot-pr-updates.yml +++ b/.github/workflows/pr-bot-pr-updates.yml @@ -36,7 +36,7 @@ jobs: steps: # Pin to master so users can't do anything malicious on their own branch and run it here. - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: 'master' - name: Setup Node diff --git a/.github/workflows/pr-bot-prs-needing-attention.yml b/.github/workflows/pr-bot-prs-needing-attention.yml index d8a2e56d2d3d..977e730a7721 100644 --- a/.github/workflows/pr-bot-prs-needing-attention.yml +++ b/.github/workflows/pr-bot-prs-needing-attention.yml @@ -33,7 +33,7 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/publish_github_release_notes.yml b/.github/workflows/publish_github_release_notes.yml index f1a1183789e6..27257e095957 100644 --- a/.github/workflows/publish_github_release_notes.yml +++ b/.github/workflows/publish_github_release_notes.yml @@ -36,7 +36,7 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -49,7 +49,7 @@ jobs: name: Publish Github Release Notes steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Publish github release notes run: | POST_PATH="website/www/site/content/en/blog/beam-${{env.RELEASE_VERSION}}.md" diff --git a/.github/workflows/python_dependency_tests.yml b/.github/workflows/python_dependency_tests.yml index 777e57f6d585..8a9010bf7817 100644 --- a/.github/workflows/python_dependency_tests.yml +++ b/.github/workflows/python_dependency_tests.yml @@ -34,7 +34,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install libsnappy-dev run: sudo apt-get update && sudo apt-get install -y libsnappy-dev - name: Install python diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index 6740b45c7956..b650f2ea5cda 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -45,7 +45,7 @@ jobs: outputs: gcp-variables-set: ${{ steps.check_gcp_variables.outputs.gcp-variables-set }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -68,7 +68,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -101,7 +101,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -137,7 +137,7 @@ jobs: python: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install python uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/refresh_looker_metrics.yml b/.github/workflows/refresh_looker_metrics.yml index 2c4d0bcdbe4e..187752549741 100644 --- a/.github/workflows/refresh_looker_metrics.yml +++ b/.github/workflows/refresh_looker_metrics.yml @@ -31,7 +31,7 @@ jobs: refresh_looker_metrics: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/reportGenerator.yml b/.github/workflows/reportGenerator.yml index 142b6f9b86a0..fbd33d8ee6c2 100644 --- a/.github/workflows/reportGenerator.yml +++ b/.github/workflows/reportGenerator.yml @@ -26,7 +26,7 @@ jobs: name: Generate issue report runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index efc1030844f2..6c72422d3d5a 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -57,7 +57,7 @@ jobs: ] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: "release-${{ env.release }}-postrelease" repository: apache/beam diff --git a/.github/workflows/run_perf_alert_tool.yml b/.github/workflows/run_perf_alert_tool.yml index 055880d54d21..301fe0d12ce5 100644 --- a/.github/workflows/run_perf_alert_tool.yml +++ b/.github/workflows/run_perf_alert_tool.yml @@ -35,7 +35,7 @@ jobs: issues: write steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/run_rc_validation_go_wordcount.yml b/.github/workflows/run_rc_validation_go_wordcount.yml index 5f60ae9dbe42..2112bec6b3f8 100644 --- a/.github/workflows/run_rc_validation_go_wordcount.yml +++ b/.github/workflows/run_rc_validation_go_wordcount.yml @@ -48,7 +48,7 @@ jobs: runs-on: self-hosted steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up environment uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/run_rc_validation_java_mobile_gaming.yml b/.github/workflows/run_rc_validation_java_mobile_gaming.yml index f8aabfd126b2..0a2d08b80517 100644 --- a/.github/workflows/run_rc_validation_java_mobile_gaming.yml +++ b/.github/workflows/run_rc_validation_java_mobile_gaming.yml @@ -78,7 +78,7 @@ jobs: run: echo "GCS_BUCKET_NAME=$(echo ${{ github.event.inputs.GCS_BUCKET }} | sed 's/^gs:\/\///')" >> $GITHUB_ENV - name: Checkout code at RC tag - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: v${{ github.event.inputs.RELEASE_VER }}-RC${{ github.event.inputs.RC_NUM }} diff --git a/.github/workflows/run_rc_validation_java_quickstart.yml b/.github/workflows/run_rc_validation_java_quickstart.yml index 0675d33c3181..10e743b9cfee 100644 --- a/.github/workflows/run_rc_validation_java_quickstart.yml +++ b/.github/workflows/run_rc_validation_java_quickstart.yml @@ -68,7 +68,7 @@ jobs: timeout-minutes: 60 # Adjust timeout as needed steps: - name: Checkout code at RC tag - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/run_rc_validation_python_mobile_gaming.yml b/.github/workflows/run_rc_validation_python_mobile_gaming.yml index 7bffc0555e53..8776fa2b70bf 100644 --- a/.github/workflows/run_rc_validation_python_mobile_gaming.yml +++ b/.github/workflows/run_rc_validation_python_mobile_gaming.yml @@ -89,7 +89,7 @@ jobs: steps: - name: Checkout code at RC tag - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/run_rc_validation_python_yaml.yml b/.github/workflows/run_rc_validation_python_yaml.yml index 2a8b828d29d7..64da2d6b0465 100644 --- a/.github/workflows/run_rc_validation_python_yaml.yml +++ b/.github/workflows/run_rc_validation_python_yaml.yml @@ -81,7 +81,7 @@ jobs: steps: - name: Checkout code at RC tag - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml index 3632a728d4a2..c61c0607d115 100644 --- a/.github/workflows/tour_of_beam_backend.yml +++ b/.github/workflows/tour_of_beam_backend.yml @@ -41,7 +41,7 @@ jobs: run: working-directory: ./learning/tour-of-beam/backend steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-go@v6 with: # pin to the biggest Go version supported by Cloud Functions runtime diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml index e4b96793906c..81dbdfc1b2e9 100644 --- a/.github/workflows/tour_of_beam_backend_integration.yml +++ b/.github/workflows/tour_of_beam_backend_integration.yml @@ -76,7 +76,7 @@ jobs: run: working-directory: ./learning/tour-of-beam/backend steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/tour_of_beam_frontend_test.yml b/.github/workflows/tour_of_beam_frontend_test.yml index 0cbdb01ad864..0812740fabdc 100644 --- a/.github/workflows/tour_of_beam_frontend_test.yml +++ b/.github/workflows/tour_of_beam_frontend_test.yml @@ -47,7 +47,7 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: 'Cache Flutter Dependencies' uses: actions/cache@v4 diff --git a/.github/workflows/typescript_tests.yml b/.github/workflows/typescript_tests.yml index 17947290d4c3..973f7a8f91ef 100644 --- a/.github/workflows/typescript_tests.yml +++ b/.github/workflows/typescript_tests.yml @@ -54,7 +54,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest] steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -96,7 +96,7 @@ jobs: fail-fast: false steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -142,7 +142,7 @@ jobs: outputs: gcp-variables-set: ${{ steps.check_gcp_variables.outputs.gcp-variables-set }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -164,7 +164,7 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/update_python_dependencies.yml b/.github/workflows/update_python_dependencies.yml index 753fb6037fcc..36752bc3c65a 100644 --- a/.github/workflows/update_python_dependencies.yml +++ b/.github/workflows/update_python_dependencies.yml @@ -41,7 +41,7 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -51,7 +51,7 @@ jobs: name: Update Python Dependencies steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: From 7f1d16e2a6b2f9b46f971a9b700b91b40735f8b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 08:08:52 -0400 Subject: [PATCH 442/490] Bump google.golang.org/api from 0.285.0 to 0.286.0 in /sdks (#39068) Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.285.0 to 0.286.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.285.0...v0.286.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.286.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/go.mod | 2 +- sdks/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/go.mod b/sdks/go.mod index 5d0d2ec141c7..272f91451db8 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/sync v0.21.0 golang.org/x/sys v0.46.0 golang.org/x/text v0.38.0 - google.golang.org/api v0.285.0 + google.golang.org/api v0.286.0 google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 diff --git a/sdks/go.sum b/sdks/go.sum index 11bb3dca7582..a8692d22a9e9 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1326,8 +1326,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.285.0 h1:B7eHHoKGAX/LrPkQvhQqnGwjgWxofbdGwCTQvpm8FkM= -google.golang.org/api v0.285.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= +google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= +google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From d2d3dd6015a7ddcaa9a9af7affb1012fd1d6151a Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 23 Jun 2026 08:23:15 -0400 Subject: [PATCH 443/490] Fix Flink 2.1+ dependency resolution (#39061) --- .../beam_PostCommit_Java_Nexmark_Flink.json | 2 +- .../beam_PostCommit_Java_Tpcds_Flink.json | 4 ++++ .../beam_PostCommit_Java_Nexmark_Flink.yml | 3 ++- .../apache/beam/gradle/BeamModulePlugin.groovy | 12 ++++++++++++ examples/java/common.gradle | 12 ++---------- runners/flink/2.1/build.gradle | 10 ++-------- runners/flink/2.1/job-server/build.gradle | 10 ++-------- runners/flink/2.2/build.gradle | 10 ++-------- runners/flink/2.2/job-server/build.gradle | 10 ++-------- sdks/java/testing/nexmark/build.gradle | 15 +++++++++++++++ sdks/java/testing/tpcds/build.gradle | 15 +++++++++++++++ 11 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 .github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json diff --git a/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json b/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json index e3d6056a5de9..b26833333238 100644 --- a/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json +++ b/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 2 } diff --git a/.github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json b/.github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "modification": 1 +} diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml index 96470ca3e1bf..f64a9bd41fb6 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml @@ -109,6 +109,7 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:java:testing:nexmark:run + # TODO: Fix query 6,9 for Flink 2 arguments: | - -Pnexmark.runner=:runners:flink:2.2 \ + -Pnexmark.runner=:runners:flink:1.20 \ "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index b5aef5d23ad4..34132bc8149d 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -954,6 +954,18 @@ class BeamModulePlugin implements Plugin { /** ***********************************************************************************************/ + // Resolves a capability conflict for a given configuration by selecting the preferred capability provider. + project.ext.resolveCapabilitiesConflict = { Configuration configuration, String capability, String preferred -> + configuration.resolutionStrategy.capabilitiesResolution.withCapability(capability) { + def candidate = candidates.find { it.id.toString().contains(preferred) } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } + } + // Returns a string representing the relocated path to be used with the shadow plugin when // given a suffix such as "com.google.common". project.ext.getJavaRelocatedPath = { String suffix -> diff --git a/examples/java/common.gradle b/examples/java/common.gradle index 0e800e7cf987..f32667d733fb 100644 --- a/examples/java/common.gradle +++ b/examples/java/common.gradle @@ -35,16 +35,8 @@ configurations.sparkRunnerPreCommit { exclude group: "org.slf4j", module: "jul-to-slf4j" exclude group: "org.slf4j", module: "slf4j-jdk14" } -configurations.flinkRunnerPreCommit { - resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") { - def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } - if (candidate != null) { - select(candidate) - } else { - selectHighestVersion() - } - } -} +resolveCapabilitiesConflict(configurations.flinkRunnerPreCommit, 'org.lz4:lz4-java', 'at.yawk.lz4') + dependencies { directRunnerPreCommit project(path: ":runners:direct-java", configuration: "shadow") diff --git a/runners/flink/2.1/build.gradle b/runners/flink/2.1/build.gradle index e9092c2977f7..1e0d565b50d9 100644 --- a/runners/flink/2.1/build.gradle +++ b/runners/flink/2.1/build.gradle @@ -45,12 +45,6 @@ apply from: "../flink_runner.gradle" // Flink 2.1 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java // Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict configurations.all { - resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { - def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } - if (candidate != null) { - select(candidate) - } else { - selectHighestVersion() - } - } + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') } + diff --git a/runners/flink/2.1/job-server/build.gradle b/runners/flink/2.1/job-server/build.gradle index 277ddc07fdaf..0910fef11208 100644 --- a/runners/flink/2.1/job-server/build.gradle +++ b/runners/flink/2.1/job-server/build.gradle @@ -33,12 +33,6 @@ apply from: "$basePath/flink_job_server.gradle" // Flink 2.1 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java // Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict configurations.all { - resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { - def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } - if (candidate != null) { - select(candidate) - } else { - selectHighestVersion() - } - } + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') } + diff --git a/runners/flink/2.2/build.gradle b/runners/flink/2.2/build.gradle index 3856d20a3512..0321dcf42d10 100644 --- a/runners/flink/2.2/build.gradle +++ b/runners/flink/2.2/build.gradle @@ -60,12 +60,6 @@ apply from: "../flink_runner.gradle" // Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java // Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict configurations.all { - resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { - def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } - if (candidate != null) { - select(candidate) - } else { - selectHighestVersion() - } - } + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') } + diff --git a/runners/flink/2.2/job-server/build.gradle b/runners/flink/2.2/job-server/build.gradle index 3f6b184e3fca..f116f5a1dcbf 100644 --- a/runners/flink/2.2/job-server/build.gradle +++ b/runners/flink/2.2/job-server/build.gradle @@ -33,12 +33,6 @@ apply from: "$basePath/flink_job_server.gradle" // Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java // Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict configurations.all { - resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { - def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } - if (candidate != null) { - select(candidate) - } else { - selectHighestVersion() - } - } + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') } + diff --git a/sdks/java/testing/nexmark/build.gradle b/sdks/java/testing/nexmark/build.gradle index ec3447579613..bd917a3935ad 100644 --- a/sdks/java/testing/nexmark/build.gradle +++ b/sdks/java/testing/nexmark/build.gradle @@ -39,6 +39,17 @@ def nexmarkRunnerDependency = project.findProperty(nexmarkRunnerProperty) def nexmarkRunnerVersionProperty = "nexmark.runner.version" def nexmarkRunnerVersion = project.findProperty(nexmarkRunnerVersionProperty) def isSparkRunner = nexmarkRunnerDependency.startsWith(":runners:spark:") +def isFlink2 = nexmarkRunnerDependency.startsWith(":runners:flink:") && { + def version = nexmarkRunnerDependency.substring(":runners:flink:".length()) + try { + def parts = version.split('\\.') + def major = parts[0].toInteger() + def minor = parts.length > 1 ? parts[1].toInteger() : 0 + return (major > 2) || (major == 2 && minor >= 1) + } catch (Exception e) { + return false + } +}() def isDataflowRunner = ":runners:google-cloud-dataflow-java".equals(nexmarkRunnerDependency) def isDataflowRunnerV2 = isDataflowRunner && "V2".equals(nexmarkRunnerVersion) def runnerConfiguration = ":runners:direct-java".equals(nexmarkRunnerDependency) ? "shadow" : null @@ -92,6 +103,10 @@ dependencies { gradleRun project(path: nexmarkRunnerDependency, configuration: runnerConfiguration) } +if (isFlink2) { + resolveCapabilitiesConflict(configurations.gradleRun, 'org.lz4:lz4-java', 'at.yawk.lz4') +} + if (isSparkRunner) { configurations.gradleRun { // Using Spark runner causes a StackOverflowError if slf4j-jdk14 is on the classpath diff --git a/sdks/java/testing/tpcds/build.gradle b/sdks/java/testing/tpcds/build.gradle index 1714e6e61a80..6a05df96ab20 100644 --- a/sdks/java/testing/tpcds/build.gradle +++ b/sdks/java/testing/tpcds/build.gradle @@ -34,6 +34,17 @@ def tpcdsRunnerProperty = "tpcds.runner" def tpcdsRunnerDependency = project.findProperty(tpcdsRunnerProperty) ?: ":runners:direct-java" def isSpark = tpcdsRunnerDependency.startsWith(":runners:spark:") +def isFlink2 = tpcdsRunnerDependency.startsWith(":runners:flink:") && { + def version = tpcdsRunnerDependency.substring(":runners:flink:".length()) + try { + def parts = version.split('\\.') + def major = parts[0].toInteger() + def minor = parts.length > 1 ? parts[1].toInteger() : 0 + return (major > 2) || (major == 2 && minor >= 1) + } catch (Exception e) { + return false + } +}() def isDataflowRunner = ":runners:google-cloud-dataflow-java".equals(tpcdsRunnerDependency) def runnerConfiguration = ":runners:direct-java".equals(tpcdsRunnerDependency) ? "shadow" : null @@ -83,6 +94,10 @@ dependencies { gradleRun project(path: tpcdsRunnerDependency, configuration: runnerConfiguration) } +if (isFlink2) { + resolveCapabilitiesConflict(configurations.gradleRun, 'org.lz4:lz4-java', 'at.yawk.lz4') +} + if (isSpark) { configurations.gradleRun { exclude group: "org.slf4j", module: "slf4j-jdk14" From 7b86e25077a676a2b4ae9e7a60872d01800ae1ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:35:21 -0400 Subject: [PATCH 444/490] Update Python Dependencies (#39070) Co-authored-by: shunping --- .../ml/py310/base_image_requirements.txt | 55 ++++++------- .../ml/py310/gpu_image_requirements.txt | 73 +++++++++--------- .../ml/py311/base_image_requirements.txt | 57 +++++++------- .../ml/py311/gpu_image_requirements.txt | 75 +++++++++--------- .../ml/py312/base_image_requirements.txt | 59 +++++++------- .../ml/py312/gpu_image_requirements.txt | 77 ++++++++++--------- .../ml/py313/base_image_requirements.txt | 59 +++++++------- .../py310/base_image_requirements.txt | 51 ++++++------ .../py311/base_image_requirements.txt | 53 ++++++------- .../py312/base_image_requirements.txt | 55 ++++++------- .../py313/base_image_requirements.txt | 55 ++++++------- .../py314/base_image_requirements.txt | 55 ++++++------- 12 files changed, 368 insertions(+), 356 deletions(-) diff --git a/sdks/python/container/ml/py310/base_image_requirements.txt b/sdks/python/container/ml/py310/base_image_requirements.txt index 74419bff9cd9..415b7579dc8b 100644 --- a/sdks/python/container/ml/py310/base_image_requirements.txt +++ b/sdks/python/container/ml/py310/base_image_requirements.txt @@ -27,7 +27,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astunparse==1.6.3 async-timeout==5.0.1 @@ -39,7 +39,7 @@ betterproto==2.0.0b7 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -57,49 +57,49 @@ exceptiongroup==1.3.1 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -122,7 +122,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.12.2 @@ -156,7 +156,7 @@ optree==0.19.1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -180,9 +180,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -202,14 +203,14 @@ rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.15.3 -scramp==1.4.8 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 @@ -223,7 +224,7 @@ threadpoolctl==3.6.0 tokenizers==0.21.4 tomli==2.4.1 torch==2.8.0+cpu -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 @@ -234,7 +235,7 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py310/gpu_image_requirements.txt b/sdks/python/container/ml/py310/gpu_image_requirements.txt index 249a14d8f791..a1ece225e0c6 100644 --- a/sdks/python/container/ml/py310/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py310/gpu_image_requirements.txt @@ -28,7 +28,7 @@ aiohttp==3.14.1 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astor==0.8.1 astunparse==1.6.3 @@ -38,12 +38,12 @@ backports.tarfile==1.2.0 beartype==0.22.9 beautifulsoup4==4.15.0 betterproto==2.0.0b7 -blake3==1.0.8 +blake3==1.0.9 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 cbor2==6.1.2 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -70,56 +70,56 @@ email-validator==2.3.0 envoy-data-plane==0.2.6 exceptiongroup==1.3.1 execnet==2.1.2 -fastapi==0.136.3 -fastapi-cli==0.0.24 -fastapi-cloud-cli==0.19.0 +fastapi==0.138.0 +fastapi-cli==0.0.27 +fastapi-cloud-cli==0.20.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 gguf==0.19.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -145,7 +145,7 @@ jeepney==0.9.0 Jinja2==3.1.6 jiter==0.15.0 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.12.2 @@ -169,7 +169,7 @@ mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 mpmath==1.3.0 -msgpack==1.1.2 +msgpack==1.2.1 msgspec==0.21.1 multidict==6.7.1 namex==0.1.0 @@ -222,7 +222,7 @@ oracledb==4.0.1 orjson==3.11.9 outlines_core==0.2.10 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 @@ -231,7 +231,7 @@ pip==26.1.2 platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 -prometheus-fastapi-instrumentator==8.0.0 +prometheus-fastapi-instrumentator==8.0.1 prometheus_client==0.25.0 propcache==0.5.2 proto-plus==1.28.0 @@ -248,16 +248,17 @@ pycountry==26.2.16 pycparser==3.0 pydantic==2.13.4 pydantic-extra-types==2.11.1 -pydantic-settings==2.14.1 +pydantic-settings==2.14.2 pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -282,10 +283,10 @@ rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.15.3 -scramp==1.4.8 +scramp==1.4.9 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.62.0 +sentry-sdk==2.63.0 setproctitle==1.3.7 setuptools==81.0.0 shellingham==1.5.4 @@ -295,10 +296,10 @@ sortedcontainers==2.4.0 soundfile==0.14.0 soupsieve==2.8.4 soxr==1.1.0 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -starlette==1.2.1 +starlette==1.3.1 sympy==1.14.0 tenacity==9.1.4 tensorboard==2.20.0 @@ -315,7 +316,7 @@ tomli==2.4.1 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 triton==3.3.1 typer==0.26.7 @@ -333,7 +334,7 @@ watchfiles==1.2.0 websockets==16.0 Werkzeug==3.1.8 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 xformers==0.0.31 xgrammar==0.1.21 yarl==1.24.2 diff --git a/sdks/python/container/ml/py311/base_image_requirements.txt b/sdks/python/container/ml/py311/base_image_requirements.txt index 85ae711b2324..feafa1d64d02 100644 --- a/sdks/python/container/ml/py311/base_image_requirements.txt +++ b/sdks/python/container/ml/py311/base_image_requirements.txt @@ -27,7 +27,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astunparse==1.6.3 attrs==26.1.0 @@ -38,7 +38,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -55,50 +55,50 @@ envoy_data_plane==1.0.3 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -121,7 +121,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.14.1 @@ -155,7 +155,7 @@ optree==0.19.1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -179,9 +179,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -201,14 +202,14 @@ rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 -scramp==1.4.8 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 @@ -221,7 +222,7 @@ textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 torch==2.8.0+cpu -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 @@ -232,7 +233,7 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py311/gpu_image_requirements.txt b/sdks/python/container/ml/py311/gpu_image_requirements.txt index 2aa43f9c8fd3..ca6b47fa87b5 100644 --- a/sdks/python/container/ml/py311/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py311/gpu_image_requirements.txt @@ -28,7 +28,7 @@ aiohttp==3.14.1 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astor==0.8.1 astunparse==1.6.3 @@ -37,12 +37,12 @@ backports.tarfile==1.2.0 beartype==0.22.9 beautifulsoup4==4.15.0 betterproto==2.0.0b6 -blake3==1.0.8 +blake3==1.0.9 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 cbor2==6.1.2 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -68,57 +68,57 @@ einops==0.8.2 email-validator==2.3.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastapi==0.136.3 -fastapi-cli==0.0.24 -fastapi-cloud-cli==0.19.0 +fastapi==0.138.0 +fastapi-cli==0.0.27 +fastapi-cloud-cli==0.20.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 gguf==0.19.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -144,7 +144,7 @@ jeepney==0.9.0 Jinja2==3.1.6 jiter==0.15.0 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.14.1 @@ -168,7 +168,7 @@ mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 mpmath==1.3.0 -msgpack==1.1.2 +msgpack==1.2.1 msgspec==0.21.1 multidict==6.7.1 namex==0.1.0 @@ -221,7 +221,7 @@ oracledb==4.0.1 orjson==3.11.9 outlines_core==0.2.10 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 @@ -230,7 +230,7 @@ pip==26.1.2 platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 -prometheus-fastapi-instrumentator==8.0.0 +prometheus-fastapi-instrumentator==8.0.1 prometheus_client==0.25.0 propcache==0.5.2 proto-plus==1.28.0 @@ -247,16 +247,17 @@ pycountry==26.2.16 pycparser==3.0 pydantic==2.13.4 pydantic-extra-types==2.11.1 -pydantic-settings==2.14.1 +pydantic-settings==2.14.2 pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -281,10 +282,10 @@ rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 scipy==1.17.1 -scramp==1.4.8 +scramp==1.4.9 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.62.0 +sentry-sdk==2.63.0 setproctitle==1.3.7 setuptools==81.0.0 shellingham==1.5.4 @@ -294,10 +295,10 @@ sortedcontainers==2.4.0 soundfile==0.14.0 soupsieve==2.8.4 soxr==1.1.0 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -starlette==1.2.1 +starlette==1.3.1 sympy==1.14.0 tenacity==9.1.4 tensorboard==2.20.0 @@ -313,7 +314,7 @@ tokenizers==0.21.4 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 triton==3.3.1 typer==0.26.7 @@ -331,7 +332,7 @@ watchfiles==1.2.0 websockets==16.0 Werkzeug==3.1.8 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 xformers==0.0.31 xgrammar==0.1.21 yarl==1.24.2 diff --git a/sdks/python/container/ml/py312/base_image_requirements.txt b/sdks/python/container/ml/py312/base_image_requirements.txt index 2b8283180794..a65289f9212b 100644 --- a/sdks/python/container/ml/py312/base_image_requirements.txt +++ b/sdks/python/container/ml/py312/base_image_requirements.txt @@ -27,7 +27,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astunparse==1.6.3 attrs==26.1.0 @@ -37,7 +37,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -54,50 +54,50 @@ envoy_data_plane==1.0.3 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -119,7 +119,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.14.1 @@ -153,7 +153,7 @@ optree==0.19.1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -177,9 +177,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -198,15 +199,15 @@ rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 -scipy==1.17.1 -scramp==1.4.8 +scipy==1.18.0 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 @@ -219,7 +220,7 @@ textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 torch==2.8.0+cpu -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 @@ -230,6 +231,6 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/ml/py312/gpu_image_requirements.txt b/sdks/python/container/ml/py312/gpu_image_requirements.txt index c659def93a8c..8ae5c75d981d 100644 --- a/sdks/python/container/ml/py312/gpu_image_requirements.txt +++ b/sdks/python/container/ml/py312/gpu_image_requirements.txt @@ -28,7 +28,7 @@ aiohttp==3.14.1 aiosignal==1.4.0 annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astor==0.8.1 astunparse==1.6.3 @@ -36,12 +36,12 @@ attrs==26.1.0 beartype==0.22.9 beautifulsoup4==4.15.0 betterproto==2.0.0b6 -blake3==1.0.8 +blake3==1.0.9 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 cbor2==6.1.2 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -67,57 +67,57 @@ einops==0.8.2 email-validator==2.3.0 envoy_data_plane==1.0.3 execnet==2.1.2 -fastapi==0.136.3 -fastapi-cli==0.0.24 -fastapi-cloud-cli==0.19.0 +fastapi==0.138.0 +fastapi-cli==0.0.27 +fastapi-cloud-cli==0.20.0 fastar==0.11.0 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 gguf==0.19.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -142,7 +142,7 @@ jeepney==0.9.0 Jinja2==3.1.6 jiter==0.15.0 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.14.1 @@ -166,7 +166,7 @@ mmh3==5.2.1 mock==5.2.0 more-itertools==11.1.0 mpmath==1.3.0 -msgpack==1.1.2 +msgpack==1.2.1 msgspec==0.21.1 multidict==6.7.1 namex==0.1.0 @@ -219,7 +219,7 @@ oracledb==4.0.1 orjson==3.11.9 outlines_core==0.2.10 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 partial-json-parser==0.2.1.1.post7 pg8000==1.31.5 @@ -228,7 +228,7 @@ pip==26.1.2 platformdirs==4.10.0 pluggy==1.6.0 portalocker==3.2.0 -prometheus-fastapi-instrumentator==8.0.0 +prometheus-fastapi-instrumentator==8.0.1 prometheus_client==0.25.0 propcache==0.5.2 proto-plus==1.28.0 @@ -245,16 +245,17 @@ pycountry==26.2.16 pycparser==3.0 pydantic==2.13.4 pydantic-extra-types==2.11.1 -pydantic-settings==2.14.1 +pydantic-settings==2.14.2 pydantic_core==2.46.4 pydot==1.4.2 Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -278,11 +279,11 @@ rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 -scipy==1.17.1 -scramp==1.4.8 +scipy==1.18.0 +scramp==1.4.9 SecretStorage==3.5.0 sentencepiece==0.2.1 -sentry-sdk==2.62.0 +sentry-sdk==2.63.0 setproctitle==1.3.7 setuptools==79.0.1 shellingham==1.5.4 @@ -292,10 +293,10 @@ sortedcontainers==2.4.0 soundfile==0.14.0 soupsieve==2.8.4 soxr==1.1.0 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 -starlette==1.2.1 +starlette==1.3.1 sympy==1.14.0 tenacity==9.1.4 tensorboard==2.20.0 @@ -311,7 +312,7 @@ tokenizers==0.21.4 torch==2.7.1 torchaudio==2.7.1 torchvision==0.22.1 -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 triton==3.3.1 typer==0.26.7 @@ -329,7 +330,7 @@ watchfiles==1.2.0 websockets==16.0 Werkzeug==3.1.8 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 xformers==0.0.31 xgrammar==0.1.21 yarl==1.24.2 diff --git a/sdks/python/container/ml/py313/base_image_requirements.txt b/sdks/python/container/ml/py313/base_image_requirements.txt index b98bef893b89..704ede901eae 100644 --- a/sdks/python/container/ml/py313/base_image_requirements.txt +++ b/sdks/python/container/ml/py313/base_image_requirements.txt @@ -27,7 +27,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 astunparse==1.6.3 attrs==26.1.0 @@ -37,7 +37,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -54,49 +54,49 @@ envoy_data_plane==1.0.3 execnet==2.1.2 fastavro==1.12.2 fasteners==0.20 -filelock==3.29.1 +filelock==3.29.4 flatbuffers==25.12.19 freezegun==1.5.5 frozenlist==1.8.0 -fsspec==2026.4.0 +fsspec==2026.6.0 future==1.0.0 gast==0.7.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.35 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-pasta==0.2.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -118,7 +118,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keras==3.14.1 @@ -152,7 +152,7 @@ optree==0.19.1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -176,9 +176,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -197,15 +198,15 @@ rpds-py==2026.5.1 rsa==4.9.1 safetensors==0.8.0 scikit-learn==1.7.2 -scipy==1.17.1 -scramp==1.4.8 +scipy==1.18.0 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 sympy==1.14.0 @@ -218,7 +219,7 @@ textual==8.2.7 threadpoolctl==3.6.0 tokenizers==0.21.4 torch==2.8.0+cpu -tqdm==4.68.2 +tqdm==4.68.3 transformers==4.55.4 typing-inspection==0.4.2 typing_extensions==4.15.0 @@ -229,6 +230,6 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/py310/base_image_requirements.txt b/sdks/python/container/py310/base_image_requirements.txt index f3a840c1809e..acbc3447f464 100644 --- a/sdks/python/container/py310/base_image_requirements.txt +++ b/sdks/python/container/py310/base_image_requirements.txt @@ -26,7 +26,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 async-timeout==5.0.1 attrs==26.1.0 @@ -37,7 +37,7 @@ betterproto==2.0.0b7 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -61,38 +61,38 @@ future==1.0.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -112,7 +112,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 @@ -138,7 +138,7 @@ opentelemetry-semantic-conventions==0.63b1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -162,9 +162,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -183,14 +184,14 @@ rpds-py==0.30.0 rsa==4.9.1 scikit-learn==1.7.2 scipy==1.15.3 -scramp==1.4.8 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 @@ -198,7 +199,7 @@ testcontainers==4.14.2 textual==8.2.7 threadpoolctl==3.6.0 tomli==2.4.1 -tqdm==4.68.2 +tqdm==4.68.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 @@ -208,7 +209,7 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/py311/base_image_requirements.txt b/sdks/python/container/py311/base_image_requirements.txt index 68f5e5469390..4401c8ecd65f 100644 --- a/sdks/python/container/py311/base_image_requirements.txt +++ b/sdks/python/container/py311/base_image_requirements.txt @@ -26,7 +26,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 attrs==26.1.0 backports.tarfile==1.2.0 @@ -36,7 +36,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -59,39 +59,39 @@ future==1.0.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -111,7 +111,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 @@ -137,7 +137,7 @@ opentelemetry-semantic-conventions==0.63b1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -161,9 +161,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -182,21 +183,21 @@ rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 scipy==1.17.1 -scramp==1.4.8 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.68.2 +tqdm==4.68.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 @@ -206,7 +207,7 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zipp==4.1.0 zstandard==0.25.0 diff --git a/sdks/python/container/py312/base_image_requirements.txt b/sdks/python/container/py312/base_image_requirements.txt index 3f4194f20ea4..2b13a0b3d5e8 100644 --- a/sdks/python/container/py312/base_image_requirements.txt +++ b/sdks/python/container/py312/base_image_requirements.txt @@ -26,7 +26,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 attrs==26.1.0 beartype==0.22.9 @@ -35,7 +35,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -58,39 +58,39 @@ future==1.0.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.31 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-profiler==4.1.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -109,7 +109,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 @@ -135,7 +135,7 @@ opentelemetry-semantic-conventions==0.63b1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -159,9 +159,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -179,22 +180,22 @@ rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 -scipy==1.17.1 -scramp==1.4.8 +scipy==1.18.0 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.68.2 +tqdm==4.68.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 @@ -204,6 +205,6 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/py313/base_image_requirements.txt b/sdks/python/container/py313/base_image_requirements.txt index 31ef940c1e20..469d4c55dbdb 100644 --- a/sdks/python/container/py313/base_image_requirements.txt +++ b/sdks/python/container/py313/base_image_requirements.txt @@ -26,7 +26,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 attrs==26.1.0 beartype==0.22.9 @@ -35,7 +35,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -58,38 +58,38 @@ future==1.0.0 google-api-core==2.31.0 google-api-python-client==2.197.0 google-apitools==0.5.35 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -108,7 +108,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 @@ -134,7 +134,7 @@ opentelemetry-semantic-conventions==0.63b1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -158,9 +158,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -178,22 +179,22 @@ rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 -scipy==1.17.1 -scramp==1.4.8 +scipy==1.18.0 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.68.2 +tqdm==4.68.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 @@ -203,6 +204,6 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zstandard==0.25.0 diff --git a/sdks/python/container/py314/base_image_requirements.txt b/sdks/python/container/py314/base_image_requirements.txt index 2c7fd33996c2..2317e26f305c 100644 --- a/sdks/python/container/py314/base_image_requirements.txt +++ b/sdks/python/container/py314/base_image_requirements.txt @@ -26,7 +26,7 @@ aiohappyeyeballs==2.6.2 aiohttp==3.14.1 aiosignal==1.4.0 annotated-types==0.7.0 -anyio==4.13.0 +anyio==4.14.0 asn1crypto==1.5.1 attrs==26.1.0 beartype==0.22.9 @@ -35,7 +35,7 @@ betterproto==2.0.0b6 bs4==0.0.2 build==1.5.0 cachetools==6.2.6 -certifi==2026.5.20 +certifi==2026.6.17 cffi==2.0.0 charset-normalizer==3.4.7 click==8.4.1 @@ -57,38 +57,38 @@ frozenlist==1.8.0 future==1.0.0 google-api-core==2.31.0 google-apitools==0.5.35 -google-auth==2.53.0 +google-auth==2.55.0 google-auth-httplib2==0.2.1 -google-cloud-aiplatform==1.157.0 -google-cloud-bigquery==3.41.0 +google-cloud-aiplatform==1.158.0 +google-cloud-bigquery==3.42.1 google-cloud-bigquery-storage==2.39.0 -google-cloud-bigtable==2.38.0 +google-cloud-bigtable==2.39.0 google-cloud-build==3.37.0 google-cloud-core==2.6.0 google-cloud-dataflow-client==0.13.0 google-cloud-datastore==2.25.0 -google-cloud-dlp==3.37.0 -google-cloud-kms==3.13.0 -google-cloud-language==2.20.0 +google-cloud-dlp==3.38.0 +google-cloud-kms==3.14.0 +google-cloud-language==2.21.0 google-cloud-monitoring==2.31.0 google-cloud-pubsub==2.39.0 google-cloud-recommendations-ai==0.10.18 -google-cloud-resource-manager==1.17.0 +google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 -google-cloud-spanner==3.67.0 -google-cloud-storage==3.11.0 -google-cloud-videointelligence==2.19.0 -google-cloud-vision==3.14.0 +google-cloud-spanner==3.68.0 +google-cloud-storage==3.12.0 +google-cloud-videointelligence==2.20.0 +google-cloud-vision==3.15.0 google-crc32c==1.8.0 -google-genai==2.8.0 +google-genai==2.9.0 google-resumable-media==2.10.0 googleapis-common-protos==1.75.0 -greenlet==3.5.1 +greenlet==3.5.2 grpc-google-iam-v1==0.14.4 grpc-interceptor==0.15.4 -grpcio==1.81.0 -grpcio-status==1.81.0 -grpcio-tools==1.81.0 +grpcio==1.81.1 +grpcio-status==1.81.1 +grpcio-tools==1.81.1 grpclib==0.4.9 guppy3==3.1.7 h11==0.16.0 @@ -107,7 +107,7 @@ jaraco.functools==4.5.0 jeepney==0.9.0 Jinja2==3.1.6 joblib==1.5.3 -jsonpickle==3.4.2 +jsonpickle==4.1.2 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 @@ -133,7 +133,7 @@ opentelemetry-semantic-conventions==0.63b1 oracledb==4.0.1 orjson==3.11.9 packaging==26.2 -pandas==2.2.3 +pandas==2.3.3 parameterized==0.9.0 pg8000==1.31.5 pillow==12.2.0 @@ -157,9 +157,10 @@ Pygments==2.20.0 PyHamcrest==2.1.0 pymongo==4.17.0 PyMySQL==1.2.0 +pyOpenSSL==26.2.0 pyparsing==3.3.2 pyproject_hooks==1.2.0 -pytest==9.0.3 +pytest==9.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 python-dateutil==2.9.0.post0 @@ -177,22 +178,22 @@ rich==15.0.0 rpds-py==2026.5.1 rsa==4.9.1 scikit-learn==1.7.2 -scipy==1.17.1 -scramp==1.4.8 +scipy==1.18.0 +scramp==1.4.9 SecretStorage==3.5.0 setuptools==82.0.1 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 soupsieve==2.8.4 -SQLAlchemy==2.0.50 +SQLAlchemy==2.0.51 sqlalchemy_pytds==1.0.2 sqlparse==0.5.5 tenacity==9.1.4 testcontainers==4.14.2 textual==8.2.7 threadpoolctl==3.6.0 -tqdm==4.68.2 +tqdm==4.68.3 typing-inspection==0.4.2 typing_extensions==4.15.0 tzdata==2026.2 @@ -201,6 +202,6 @@ urllib3==2.7.0 virtualenv-clone==0.5.7 websockets==16.0 wheel==0.47.0 -wrapt==2.2.1 +wrapt==2.2.2 yarl==1.24.2 zstandard==0.25.0 From e7e51ff1264694f874deb72df1ff5bc2877c49e2 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Tue, 23 Jun 2026 10:49:52 -0400 Subject: [PATCH 445/490] Enable precommit yaml xlang test on code push. (#39071) --- .github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 6a1b24539161..e3dd78460734 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -65,6 +65,7 @@ env: jobs: beam_PreCommit_Yaml_Xlang_Direct: if: | + github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || From 623e09fc58f630033b6bbde253caa8d874c7f792 Mon Sep 17 00:00:00 2001 From: TongruiLi <12992126+TongruiLi@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:08:09 -0700 Subject: [PATCH 446/490] Release Note: Rename Dataflow Runner to Dataflow Portable Runner (#38747) * Rename Dataflow Runner to Dataflow Portable Runner Updated CHANGES.md to reflect the renaming of Dataflow Runner. * Update CHANGES.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * added issue --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index bd9c6ea246a5..ac571c2f55ae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,7 @@ ## New Features / Improvements +* Dataflow Runner v2 has been renamed to Dataflow Portable Runner. Please refer to Dataflow [public documentation](https://docs.cloud.google.com/dataflow/docs/runner-v2) on when to enable Portable Runner.([#39000](https://github.com/apache/beam/issues/39000)). * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). * (Python) Added instrumentation to support off-the-shelf profiling agents when launching Python SDK Harness ([#38853](https://github.com/apache/beam/issues/38853)). * (Java) Added support to the FnApi Data stream protocol allowing runners to isolate bundles slowly processing input from other bundles. ([#39001](https://github.com/apache/beam/issues/39001)). From 44fd4a76ebb36d5f246f7ead194b003462f3067d Mon Sep 17 00:00:00 2001 From: claudevdm <33973061+claudevdm@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:53:21 -0400 Subject: [PATCH 447/490] init (#39025) --- .../apache/beam/sdk/io/parquet/ParquetIO.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java b/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java index feaceeeb4432..f869286b98bd 100644 --- a/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java +++ b/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java @@ -1033,6 +1033,8 @@ public abstract static class Sink implements FileIO.Sink { abstract @Nullable ValueProvider getMinRowCountForPageSizeCheck(); + abstract @Nullable ValueProvider getMaxRowCountForPageSizeCheck(); + abstract @Nullable Class getAvroDataModelClass(); abstract Builder toBuilder(); @@ -1056,6 +1058,9 @@ abstract static class Builder { abstract Builder setMinRowCountForPageSizeCheck( ValueProvider minRowCountForPageSizeCheck); + abstract Builder setMaxRowCountForPageSizeCheck( + ValueProvider maxRowCountForPageSizeCheck); + abstract Builder setAvroDataModelClass(Class modelClass); abstract Sink build(); @@ -1130,6 +1135,39 @@ public Sink withMinRowCountForPageSizeCheck( return toBuilder().setMinRowCountForPageSizeCheck(minRowCountForPageSizeCheck).build(); } + /** + * Specify the maximum number of rows to buffer before a page size check is forced. By default + * Parquet estimates the next check from the average row size and may defer it (up to {@code + * 10000} rows); a run of small rows followed by large rows can then let the column page buffer + * overflow {@code Integer.MAX_VALUE} before the deferred check fires. Setting this (e.g. {@code + * 1}) caps the interval so a check -- and flush -- happens at least this often regardless of + * the estimate. Pair it with {@link #withMinRowCountForPageSizeCheck(int)} to bound the buffer + * for tables whose row sizes vary widely. + */ + public Sink withMaxRowCountForPageSizeCheck(int maxRowCountForPageSizeCheck) { + checkArgument( + maxRowCountForPageSizeCheck > 0, "maxRowCountForPageSizeCheck must be positive"); + return toBuilder() + .setMaxRowCountForPageSizeCheck( + ValueProvider.StaticValueProvider.of(maxRowCountForPageSizeCheck)) + .build(); + } + + /** + * Like {@link #withMaxRowCountForPageSizeCheck(int)}, but accepts a {@link ValueProvider} so + * the value can be supplied at runtime (required for classic Dataflow templates). + */ + public Sink withMaxRowCountForPageSizeCheck( + ValueProvider maxRowCountForPageSizeCheck) { + checkNotNull(maxRowCountForPageSizeCheck, "maxRowCountForPageSizeCheck can not be null"); + if (maxRowCountForPageSizeCheck.isAccessible()) { + Integer value = maxRowCountForPageSizeCheck.get(); + checkNotNull(value, "maxRowCountForPageSizeCheck value cannot be null"); + checkArgument(value > 0, "maxRowCountForPageSizeCheck must be positive"); + } + return toBuilder().setMaxRowCountForPageSizeCheck(maxRowCountForPageSizeCheck).build(); + } + /** * Define the Avro data model; see {@link AvroParquetWriter.Builder#withDataModel(GenericData)}. */ @@ -1170,6 +1208,15 @@ public void open(WritableByteChannel channel) throws IOException { } } + ValueProvider maxRowCountProvider = getMaxRowCountForPageSizeCheck(); + if (maxRowCountProvider != null) { + Integer maxRowCount = maxRowCountProvider.get(); + if (maxRowCount != null) { + checkArgument(maxRowCount > 0, "maxRowCountForPageSizeCheck must be positive"); + builder = builder.withMaxRowCountForPageSizeCheck(maxRowCount); + } + } + if (modelClass != null) { try { builder.withDataModel(buildModelObject(modelClass)); From f6889065f7213e5796c76c67bc102c9e27f9cb40 Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Wed, 24 Jun 2026 05:56:18 +0200 Subject: [PATCH 448/490] [mqtt] Add Python Xlang Messaging PostCommit with MQTT integration tests (#38966) * [mqtt] Add Python Xlang Messaging PostCommit with MQTT integration tests --- ...tCommit_Python_Xlang_Messaging_Direct.json | 4 + .github/workflows/README.md | 1 + ...stCommit_Python_Xlang_Messaging_Direct.yml | 97 +++++++ .../io/external/xlang_mqttio_it_test.py | 259 ++++++++++++++++++ sdks/python/pytest.ini | 1 + sdks/python/test-suites/direct/build.gradle | 6 + sdks/python/test-suites/xlang/build.gradle | 8 +- 7 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 .github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json create mode 100644 .github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml create mode 100644 sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "modification": 1 +} diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 0785b12a1d12..1715365f4ec6 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -414,6 +414,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Dataflow Streaming Engine ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml?query=event%3Aschedule) | | [ PostCommit Python Portable Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Portable_Flink.yml) | N/A |`beam_PostCommit_Python_Portable_Flink.json`| [![.github/workflows/beam_PostCommit_Python_Portable_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Portable_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Portable_Flink.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_IO_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml?query=event%3Aschedule) | +| [ PostCommit Python Xlang Messaging Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_Messaging_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml?query=event%3Aschedule) | ### PerformanceTests and Benchmark Jobs diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml new file mode 100644 index 000000000000..943dfbaffd94 --- /dev/null +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: PostCommit Python Xlang Messaging Direct + +on: + schedule: + - cron: '45 5/6 * * *' + pull_request_target: + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json'] + workflow_dispatch: + +#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +permissions: + actions: write + pull-requests: write + checks: write + contents: read + deployments: read + id-token: none + issues: write + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + +jobs: + beam_PostCommit_Python_Xlang_Messaging_Direct: + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event.comment.body == 'Run Python_Xlang_Messaging_Direct PostCommit' + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 100 + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + strategy: + matrix: + job_name: ["beam_PostCommit_Python_Xlang_Messaging_Direct"] + job_phrase: ["Run Python_Xlang_Messaging_Direct PostCommit"] + steps: + - uses: actions/checkout@v6 + - name: Setup repository + uses: ./.github/actions/setup-action + with: + comment_phrase: ${{ matrix.job_phrase }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + python-version: | + 3.10 + 3.14 + - name: run PostCommit Python Xlang Messaging Direct script + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :sdks:python:test-suites:direct:messagingCrossLanguagePostCommit + arguments: -PuseWheelDistribution + - name: Archive Python Test Results + uses: actions/upload-artifact@v7 + if: failure() + with: + name: Python Test Results + path: '**/pytest*.xml' + - name: Publish Python Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + commit: '${{ env.prsha || env.GITHUB_SHA }}' + comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} + files: '**/pytest*.xml' + large_files: true diff --git a/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py b/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py new file mode 100644 index 000000000000..fa6ebed06efe --- /dev/null +++ b/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py @@ -0,0 +1,259 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Integration tests for the cross-language MQTT IO transforms +(ReadFromMqtt / WriteToMqtt), served by the messaging expansion service. + +Runs against an MQTT broker (Eclipse Mosquitto) started once per test class +via testcontainers. MqttIO reads are unbounded (streaming), so the end-to-end +read/write test runs on the Prism portable streaming runner -- the legacy +DirectRunner cannot execute an unbounded read (see the +MqttReadSchemaTransformProvider description). +""" + +import logging +import threading +import time +import unittest + +import pytest + +import apache_beam as beam +from apache_beam.options.pipeline_options import PipelineOptions +from apache_beam.options.pipeline_options import StandardOptions +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.typehints.row_type import RowTypeConstraint + +# pylint: disable=wrong-import-order, wrong-import-position, ungrouped-imports +try: + from apache_beam.io import ReadFromMqtt + from apache_beam.io import WriteToMqtt +except ImportError: + ReadFromMqtt = None + WriteToMqtt = None + +try: + from testcontainers.core.container import DockerContainer + from testcontainers.core.waiting_utils import wait_for_logs +except ImportError: + DockerContainer = None + +NUM_RECORDS = 3 +BYTES_ROW = RowTypeConstraint.from_fields([('bytes', bytes)]) + + +@pytest.mark.uses_messaging_java_expansion_service +@unittest.skipIf( + DockerContainer is None, 'testcontainers package is not installed') +@unittest.skipIf( + ReadFromMqtt is None or WriteToMqtt is None, + 'MQTT cross-language wrappers are not generated') +@unittest.skipIf( + TestPipeline().get_pipeline_options().view_as(StandardOptions).runner + is None, + 'Do not run this test on precommit suites.') +@unittest.skipIf( + 'Dataflow' in ( + TestPipeline().get_pipeline_options().view_as(StandardOptions).runner or + ''), + 'The testcontainers broker is not reachable from Dataflow workers; ' + 'a Dataflow variant would need a remotely hosted MQTT broker.') +class CrossLanguageMqttIOTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # The broker is expensive to spin up and tear down, so start a single + # shared instance for the whole class; each test uses its own topic(s). + cls.start_mqtt_container(retries=3) + host = cls.broker.get_container_host_ip() + port = cls.broker.get_exposed_port(1883) + cls.server_uri = 'tcp://%s:%s' % (host, port) + + @classmethod + def tearDownClass(cls): + # Sometimes stopping the container raises ReadTimeout. We can ignore it + # here to avoid the test failure. + try: + cls.broker.stop() + except Exception: + logging.error('Could not stop the MQTT broker container.') + + # Creating a container with testcontainers sometimes raises ReadTimeout + # error, so retry a couple of times. + @classmethod + def start_mqtt_container(cls, retries): + for i in range(retries): + try: + # /mosquitto-no-auth.conf ships with the image and enables an + # anonymous listener on port 1883. + cls.broker = DockerContainer('eclipse-mosquitto:2').with_command( + 'mosquitto -c /mosquitto-no-auth.conf').with_exposed_ports(1883) + cls.broker.start() + wait_for_logs(cls.broker, 'mosquitto version .* running', timeout=30) + break + except Exception as e: + # If start() succeeded but a later step (e.g. wait_for_logs) failed, + # stop the partially started container so the next retry / the raised + # error does not leak a running Docker container. + try: + cls.broker.stop() + except Exception: + pass + if i == retries - 1: + logging.error('Unable to initialize the MQTT broker container.') + raise e + + def _connection_configuration(self, topic, client_id): + return { + 'server_uri': self.server_uri, 'topic': topic, 'client_id': client_id + } + + def test_xlang_mqtt_write(self): + topic = 'xlang-mqtt-write-topic' + expected_payloads = [b'msg-%d' % i for i in range(NUM_RECORDS)] + subscriber_result = {} + + def subscribe(): + # mosquitto_sub exits after receiving NUM_RECORDS messages (-C) or + # after the timeout (-W), printing one payload per line. + container = self.broker.get_wrapped_container() + exit_code, output = container.exec_run([ + 'mosquitto_sub', + '-t', + topic, + '-q', + '1', + '-C', + str(NUM_RECORDS), + '-W', + '120' + ]) + subscriber_result['exit_code'] = exit_code + subscriber_result['output'] = output + + subscriber = threading.Thread(target=subscribe, daemon=True) + subscriber.start() + # Give the subscriber time to connect before publishing. + time.sleep(5) + + with TestPipeline() as p: + p.not_use_test_runner_api = True + _ = ( + p + | 'CreatePayloads' >> beam.Create(expected_payloads) + | 'ToRow' >> beam.Map(lambda payload: beam.Row(bytes=payload)). + with_output_types(BYTES_ROW) + | 'WriteToMqtt' >> WriteToMqtt( + connection_configuration=self._connection_configuration( + topic, 'xlang-mqtt-write'))) + + subscriber.join(timeout=150) + self.assertEqual(subscriber_result.get('exit_code'), 0) + received = sorted(subscriber_result.get('output', b'').split()) + self.assertEqual(sorted(expected_payloads), received) + + def test_xlang_mqtt_read_write_streaming(self): + """Exercises ReadFromMqtt and WriteToMqtt end to end on the Prism portable + streaming runner. MqttIO read is unbounded, which the legacy DirectRunner + cannot execute, so this is the single read test: an unbounded ReadFromMqtt + on a source topic feeds a WriteToMqtt on a sink topic, the result is + observed with a mosquitto_sub subscriber on the sink topic, and the + (never-terminating) pipeline is then cancelled. + + MQTT does not retain regular messages, so the reader must already be + subscribed when messages are published -- a Kafka-style sequential + write-then-read would read nothing. A background publisher therefore feeds + the source topic continuously while the streaming pipeline runs. + """ + source_topic = 'xlang-mqtt-streaming-source' + sink_topic = 'xlang-mqtt-streaming-sink' + stop_publishing = threading.Event() + subscriber_result = {} + + def publish_loop(): + container = self.broker.get_wrapped_container() + i = 0 + while not stop_publishing.is_set(): + container.exec_run([ + 'mosquitto_pub', '-t', source_topic, '-m', 'msg-%d' % i, '-q', '1' + ]) + i += 1 + time.sleep(0.5) + + def subscribe(): + container = self.broker.get_wrapped_container() + exit_code, output = container.exec_run([ + 'mosquitto_sub', + '-t', + sink_topic, + '-q', + '1', + '-C', + str(NUM_RECORDS), + '-W', + '180' + ]) + subscriber_result['exit_code'] = exit_code + subscriber_result['output'] = output + + publisher = threading.Thread(target=publish_loop, daemon=True) + subscriber = threading.Thread(target=subscribe, daemon=True) + publisher.start() + subscriber.start() + + options = PipelineOptions([ + '--runner=PrismRunner', + '--environment_type=LOOPBACK', + '--streaming', + ]) + p = TestPipeline(options=options) + p.not_use_test_runner_api = True + _ = ( + p + | 'ReadFromMqtt' >> ReadFromMqtt( + connection_configuration=self._connection_configuration( + source_topic, 'xlang-mqtt-streaming-read')) + | 'Passthrough' >> beam.Map( + lambda row: beam.Row(bytes=row.bytes)).with_output_types(BYTES_ROW) + | 'WriteToMqtt' >> WriteToMqtt( + connection_configuration=self._connection_configuration( + sink_topic, 'xlang-mqtt-streaming-write'))) + result = p.run() + try: + # The subscriber exits once NUM_RECORDS messages flowed through the + # streaming pipeline (or fails the assertions below on its timeout). + subscriber.join(timeout=200) + finally: + stop_publishing.set() + publisher.join() + try: + result.cancel() + except Exception: # pylint: disable=broad-except + # The unbounded pipeline never finishes on its own; cancellation + # after the assertion data was collected is best-effort. + logging.warning('Ignoring error while cancelling the pipeline.') + + self.assertEqual(subscriber_result.get('exit_code'), 0) + payloads = subscriber_result.get('output', b'').split() + self.assertEqual(NUM_RECORDS, len(payloads)) + for payload in payloads: + self.assertTrue( + payload.startswith(b'msg-'), 'Unexpected payload: %s' % payload) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + unittest.main() diff --git a/sdks/python/pytest.ini b/sdks/python/pytest.ini index e2f5a70cf353..082c3ed634b1 100644 --- a/sdks/python/pytest.ini +++ b/sdks/python/pytest.ini @@ -34,6 +34,7 @@ markers = uses_java_expansion_service: collect Cross Language Java transforms test runs uses_python_expansion_service: collect Cross Language Python transforms test runs uses_io_java_expansion_service: collect Cross Language IO Java transform test runs (with Kafka bootstrap server) + uses_messaging_java_expansion_service: collect Cross Language Messaging IO Java transform test runs xlang_wrapper_generation: collect tests that validate Cross Language wrapper generation uses_transform_service: collect Cross Language test runs that uses the Transform Service xlang_sql_expansion_service: collect for Cross Language with SQL expansion service test runs diff --git a/sdks/python/test-suites/direct/build.gradle b/sdks/python/test-suites/direct/build.gradle index 4b1025343985..d1fe45683a83 100644 --- a/sdks/python/test-suites/direct/build.gradle +++ b/sdks/python/test-suites/direct/build.gradle @@ -43,6 +43,12 @@ task ioCrossLanguagePostCommit { } } +task messagingCrossLanguagePostCommit { + getVersionsAsList('cross_language_validates_py_versions').each { + dependsOn.add(":sdks:python:test-suites:direct:py${getVersionSuffix(it)}:messagingCrossLanguagePythonUsingJava") + } +} + task crossLanguageWrapperValidationPreCommit { // Different python versions may output types that look different and lead to // false failures. To be consistent, we test on the lowest version only diff --git a/sdks/python/test-suites/xlang/build.gradle b/sdks/python/test-suites/xlang/build.gradle index 1cbbaa0db534..458e90be292e 100644 --- a/sdks/python/test-suites/xlang/build.gradle +++ b/sdks/python/test-suites/xlang/build.gradle @@ -42,6 +42,12 @@ def ioXlang = new CrossLanguageTask().tap { additionalEnvs = ["KAFKA_BOOTSTRAP_SERVER":project.findProperty('kafkaBootstrapServer')] } +def messagingXlang = new CrossLanguageTask().tap { + name = "messagingCrossLanguage" + expansionProjectPaths = [messagingExpansionPath] + collectMarker = "uses_messaging_java_expansion_service" +} + // This list should include all expansion service targets in sdks/python/standard_expansion_services.yaml def servicesToGenerateFrom = [ioExpansionPath, messagingExpansionPath, gcpExpansionPath] def xlangWrapperValidation = new CrossLanguageTask().tap { @@ -54,6 +60,6 @@ def xlangWrapperValidation = new CrossLanguageTask().tap { // List of task metadata objects to create cross-language tasks from. // Each object contains the minimum relevant metadata. -def xlangTasks = [gcpXlang, ioXlang, xlangWrapperValidation] +def xlangTasks = [gcpXlang, ioXlang, messagingXlang, xlangWrapperValidation] ext.xlangTasks = xlangTasks \ No newline at end of file From 0eb094aaf3275e1bb02e85b0460e3792ec538d78 Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Wed, 24 Jun 2026 02:20:28 -0700 Subject: [PATCH 449/490] [ReduceFnRunner] Fix Prefetches (#39082) 1. Add a prefetch for CombinedMetadata 2. Prefetch ReduceFn using renamedContext instead of directContext. This improves prefetching on merging windows --- .../main/java/org/apache/beam/runners/core/ReduceFnRunner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java index 78505f3c65f6..7fe3b711aa0a 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java @@ -1056,7 +1056,8 @@ private void prefetchOnTrigger( paneInfoTracker.prefetchPaneInfo(directContext); watermarkHold.prefetchExtract(renamedContext); nonEmptyPanes.isEmpty(renamedContext.state()).readLater(); - reduceFn.prefetchOnTrigger(directContext.state()); + directContext.state().access(METADATA_TAG).readLater(); + reduceFn.prefetchOnTrigger(renamedContext.state()); } /** From 5b8d530aa210f86f0e37857ce9d4c79a3eccd990 Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Wed, 24 Jun 2026 02:20:48 -0700 Subject: [PATCH 450/490] [Dataflow Streaming] Increase stuck commit invalidation timeout to 1 hour (#39083) --- .../dataflow/options/DataflowStreamingPipelineOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java index 90375ad445ae..0897e5263821 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java @@ -191,7 +191,7 @@ public interface DataflowStreamingPipelineOptions extends PipelineOptions { void setMaxStackTraceDepthToReport(int value); @Description("Necessary duration for a commit to be considered stuck and invalidated.") - @Default.Integer(10 * 60 * 1000) + @Default.Integer(60 * 60 * 1000) int getStuckCommitDurationMillis(); void setStuckCommitDurationMillis(int value); From 69768ce5dc294a36fe7c3a90c47c5116ac199b25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:58:07 -0400 Subject: [PATCH 451/490] Bump actions/cache from 4 to 6 (#39081) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/playground_frontend_test.yml | 2 +- .github/workflows/tour_of_beam_frontend_test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playground_frontend_test.yml b/.github/workflows/playground_frontend_test.yml index c4c762704869..b54fdc0ecfd6 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v7 - name: 'Cache Flutter Dependencies' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: /opt/hostedtoolcache/flutter key: ${{ runner.OS }}-flutter-install-cache-${{ env.FLUTTER_VERSION }} diff --git a/.github/workflows/tour_of_beam_frontend_test.yml b/.github/workflows/tour_of_beam_frontend_test.yml index 0812740fabdc..c6df5bf6ea17 100644 --- a/.github/workflows/tour_of_beam_frontend_test.yml +++ b/.github/workflows/tour_of_beam_frontend_test.yml @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v7 - name: 'Cache Flutter Dependencies' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: /opt/hostedtoolcache/flutter key: ${{ runner.OS }}-flutter-install-cache-${{ env.FLUTTER_VERSION }} From e02132d72bc8dac998aea22d45e1e39d90a920cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:58:35 -0400 Subject: [PATCH 452/490] Bump actions/checkout from 6 to 7 (#39080) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml index 943dfbaffd94..ef4c35c189ee 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Messaging_Direct"] job_phrase: ["Run Python_Xlang_Messaging_Direct PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: From a47c2ffa9f5352fded196077884ef6ecda04a2ba Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Wed, 24 Jun 2026 05:21:44 -0700 Subject: [PATCH 453/490] Add UnboundedCountingSource::to for bounded reads from an UnboundedCountingSource (#39084) * Add UnboundedCountingSource::to for bounded reads from an UnboundedCountingSource UnboundedCountingSource currently does not support configuring an end limit, this PR adds that capability. GenerateSequence with a from + to + rate boils down to a BoundedReadFromUnboundedSource. BoundedReadFromUnboundedSource splits emit all outputs from a single bundle that does not work well on Streaming jobs. UnboundedCountingSource::to is currently not exposed outside CountingSource. Eventually GenerateSequence can use UnboundedCountingSource::to instead of BoundedReadFromUnboundedSource --- .../apache/beam/sdk/io/CountingSource.java | 40 +++++++++++++++---- .../beam/sdk/io/CountingSourceTest.java | 36 +++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/CountingSource.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/CountingSource.java index 9d30efb2f113..4ef928480d75 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/CountingSource.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/CountingSource.java @@ -39,6 +39,7 @@ import org.apache.beam.sdk.metrics.SourceMetrics; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.transforms.SerializableFunction; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.checkerframework.checker.nullness.qual.Nullable; @@ -95,7 +96,8 @@ static BoundedSource createSourceForSubrange(long startIndex, long endInde /** Create a new {@link UnboundedCountingSource}. */ // package-private to return a typed UnboundedCountingSource rather than the UnboundedSource type. static UnboundedCountingSource createUnboundedFrom(long start) { - return new UnboundedCountingSource(start, 1, 1L, Duration.ZERO, new NowTimestampFn()); + return new UnboundedCountingSource( + start, 1, Long.MAX_VALUE, 1L, Duration.ZERO, new NowTimestampFn()); } /** @@ -130,7 +132,7 @@ public static UnboundedSource unbounded() { @Deprecated public static UnboundedSource unboundedWithTimestampFn( SerializableFunction timestampFn) { - return new UnboundedCountingSource(0, 1, 1L, Duration.ZERO, timestampFn); + return new UnboundedCountingSource(0, 1, Long.MAX_VALUE, 1L, Duration.ZERO, timestampFn); } ///////////////////////////////////////////////////////////////////////////////////////////// @@ -267,11 +269,13 @@ public void close() throws IOException {} } /** An implementation of {@link CountingSource} that produces an unbounded {@link PCollection}. */ - static class UnboundedCountingSource extends UnboundedSource { + public static class UnboundedCountingSource extends UnboundedSource { /** The first number (>= 0) generated by this {@link UnboundedCountingSource}. */ private final long start; /** The interval between numbers generated by this {@link UnboundedCountingSource}. */ private final long stride; + /** The exclusive limit for the sequence. */ + private final long end; /** The number of elements to produce each period. */ private final long elementsPerPeriod; /** The time between producing numbers from this {@link UnboundedCountingSource}. */ @@ -291,11 +295,13 @@ static class UnboundedCountingSource extends UnboundedSource private UnboundedCountingSource( long start, long stride, + long end, long elementsPerPeriod, Duration period, SerializableFunction timestampFn) { this.start = start; this.stride = stride; + this.end = end; checkArgument( elementsPerPeriod > 0L, "Must produce at least one element per period, got %s", @@ -312,7 +318,8 @@ private UnboundedCountingSource( * will be produced with an interval between them equal to the period. */ public UnboundedCountingSource withRate(long elementsPerPeriod, Duration period) { - return new UnboundedCountingSource(start, stride, elementsPerPeriod, period, timestampFn); + return new UnboundedCountingSource( + start, stride, end, elementsPerPeriod, period, timestampFn); } /** @@ -324,7 +331,18 @@ public UnboundedCountingSource withRate(long elementsPerPeriod, Duration period) public UnboundedCountingSource withTimestampFn( SerializableFunction timestampFn) { checkNotNull(timestampFn); - return new UnboundedCountingSource(start, stride, elementsPerPeriod, period, timestampFn); + return new UnboundedCountingSource( + start, stride, end, elementsPerPeriod, period, timestampFn); + } + + /** + * Returns an {@link UnboundedCountingSource} like this one but with the specified exclusive + * limit. + */ + public UnboundedCountingSource to(long end) { + checkArgument(end >= start, "end (%s) must be >= start (%s)", end, start); + return new UnboundedCountingSource( + start, stride, end, elementsPerPeriod, period, timestampFn); } /** @@ -348,7 +366,7 @@ public List> split( // 0, 2, and 4. splits.add( new UnboundedCountingSource( - start + i * stride, newStride, elementsPerPeriod, period, timestampFn)); + start + i * stride, newStride, end, elementsPerPeriod, period, timestampFn)); } return splits.build(); } @@ -376,6 +394,7 @@ public boolean equals(@Nullable Object other) { UnboundedCountingSource that = (UnboundedCountingSource) other; return this.start == that.start && this.stride == that.stride + && this.end == that.end && this.elementsPerPeriod == that.elementsPerPeriod && Objects.equals(this.period, that.period) && Objects.equals(this.timestampFn, that.timestampFn); @@ -383,7 +402,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return Objects.hash(start, stride, elementsPerPeriod, period, timestampFn); + return Objects.hash(start, stride, end, elementsPerPeriod, period, timestampFn); } } @@ -431,6 +450,9 @@ public boolean advance() throws IOException { return false; } long nextValue = current + source.stride; + if (nextValue >= source.end) { + return false; + } if (expectedValue() < nextValue) { return false; } @@ -453,7 +475,9 @@ private long expectedValue() { @Override public Instant getWatermark() { - return source.timestampFn.apply(current); + return (current >= source.end - source.stride) + ? BoundedWindow.TIMESTAMP_MAX_VALUE + : source.timestampFn.apply(current); } @Override diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/CountingSourceTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/CountingSourceTest.java index 70a09083619d..337462340df1 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/CountingSourceTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/CountingSourceTest.java @@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -315,4 +316,39 @@ public void testUnboundedSourceCheckpointMark() throws Exception { assertEquals(numToSkip + 1, (long) reader.getCurrent()); assertEquals(numToSkip + 1, reader.getCurrentTimestamp().getMillis()); } + + @Test + @Category(NeedsRunner.class) + public void testUnboundedSourceWithFromAndTo() { + long start = 5; + long end = 10; + PCollection input = p.apply(Read.from(CountingSource.createUnboundedFrom(start).to(end))); + + PAssert.that(input).containsInAnyOrder(5L, 6L, 7L, 8L, 9L); + p.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testUnboundedSourceWithFromAndToAndRate() { + long start = 5; + long end = 15; + long elementsPerPeriod = 2; + Duration period = Duration.millis(10); + PCollection input = + p.apply( + Read.from( + CountingSource.createUnboundedFrom(start) + .to(end) + .withRate(elementsPerPeriod, period))); + + PAssert.that(input).containsInAnyOrder(5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L); + + Instant startTime = Instant.now(); + p.run(); + Instant endTime = Instant.now(); + + long expectedMinimumMillis = ((end - start) * period.getMillis()) / elementsPerPeriod; + assertFalse(endTime.isBefore(startTime.plus(Duration.millis(expectedMinimumMillis)))); + } } From ce12a75da4e6cb71c85ef91eee6917c76bc8a04e Mon Sep 17 00:00:00 2001 From: scwhittle Date: Wed, 24 Jun 2026 15:30:10 +0200 Subject: [PATCH 454/490] [Dataflow Java Streaming] Add a timeout to how long commits will retry to the service. (#39085) * [Dataflow Java Streaming] Add a timeout to how long commits will retry to the service. In cases where the service is unavailable this prevents build-up of commits on workers that are no longer relevant. This defaults to 30 minutes but can be disabled via an experiment or by setting the option. --- .../DataflowStreamingPipelineOptions.java | 17 ++++ .../worker/StreamingDataflowWorker.java | 2 + .../client/grpc/GrpcCommitWorkStream.java | 94 ++++++++++++++----- .../grpc/GrpcWindmillStreamFactory.java | 10 ++ .../client/grpc/GrpcCommitWorkStreamTest.java | 82 ++++++++++++++++ 5 files changed, 183 insertions(+), 22 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java index 0897e5263821..cca261fff627 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java @@ -196,6 +196,13 @@ public interface DataflowStreamingPipelineOptions extends PipelineOptions { void setStuckCommitDurationMillis(int value); + @Description( + "Retry commits on stream errors until this much time has elapsed since the commit was scheduled. If zero, retry forever.") + @Default.InstanceFactory(CommitWorkStreamRetryTimeoutMillisFactory.class) + long getCommitWorkStreamRetryTimeoutMillis(); + + void setCommitWorkStreamRetryTimeoutMillis(long value); + @Description( "Period for sending 'global get config' requests to the service. The duration is " + "specified as seconds in 'PTx.yS' format, e.g. 'PT5.125S'." @@ -343,4 +350,14 @@ public Boolean create(PipelineOptions options) { return ExperimentalOptions.hasExperiment(options, "enable_windmill_service_direct_path"); } } + + class CommitWorkStreamRetryTimeoutMillisFactory implements DefaultValueFactory { + @Override + public Long create(PipelineOptions options) { + if (ExperimentalOptions.hasExperiment(options, "disable_commit_retry_timeout")) { + return 0L; + } + return Duration.standardMinutes(30).getMillis(); + } + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java index 9e82343474c6..9f063d393703 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java @@ -756,6 +756,8 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o new WorkHeartbeatResponseProcessor(computationStateCache::get)) .setHealthCheckIntervalMillis( options.getWindmillServiceStreamingRpcHealthCheckPeriodMs()) + .setCommitWorkStreamRetryTimeout( + java.time.Duration.ofMillis(options.getCommitWorkStreamRetryTimeoutMillis())) .build(); return ConfigFetcherComputationStateCacheAndWindmillClient.builder() .setWindmillDispatcherClient(dispatcherClient) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java index d24676652fd8..160b0cce0133 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java @@ -20,7 +20,6 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; -import com.google.auto.value.AutoValue; import java.io.PrintWriter; import java.time.Duration; import java.util.HashMap; @@ -77,6 +76,7 @@ private static class StreamAndRequest { private final JobHeader jobHeader; private final int streamingRpcBatchLimit; private volatile boolean logMissingResponse = true; + private final Duration maxRetryDuration; private GrpcCommitWorkStream( String backendWorkerToken, @@ -90,6 +90,7 @@ private GrpcCommitWorkStream( AtomicLong idGenerator, int streamingRpcBatchLimit, Duration halfClosePhysicalStreamAfter, + Duration maxRetryDuration, ScheduledExecutorService executor) { super( LOG, @@ -104,6 +105,7 @@ private GrpcCommitWorkStream( this.idGenerator = idGenerator; this.jobHeader = jobHeader; this.streamingRpcBatchLimit = streamingRpcBatchLimit; + this.maxRetryDuration = maxRetryDuration; } static GrpcCommitWorkStream create( @@ -118,6 +120,7 @@ static GrpcCommitWorkStream create( AtomicLong idGenerator, int streamingRpcBatchLimit, Duration halfClosePhysicalStreamAfter, + Duration maxRetryDuration, ScheduledExecutorService executor) { return new GrpcCommitWorkStream( backendWorkerToken, @@ -130,6 +133,7 @@ static GrpcCommitWorkStream create( idGenerator, streamingRpcBatchLimit, halfClosePhysicalStreamAfter, + maxRetryDuration, executor); } @@ -224,14 +228,48 @@ public void onResponse(StreamingCommitResponse response) { failureHandler.throwIfNonEmpty(); } - @Override @SuppressWarnings("ReferenceEquality") + private boolean belongsToThisHandler(StreamAndRequest streamAndRequest) { + return streamAndRequest.handler == this; + } + + @Override public boolean hasPendingRequests() { - return pending.entrySet().stream().anyMatch(e -> e.getValue().handler == this); + return pending.entrySet().stream().anyMatch(e -> belongsToThisHandler(e.getValue())); } @Override + @SuppressWarnings("ReferenceEquality") public void onDone(Status status) { + if (maxRetryDuration.compareTo(Duration.ZERO) > 0) { + // Remove the requests that have exceeded the retry time so they are not retried. + long nowNanos = System.nanoTime(); + long maxRetryDurationNanos = maxRetryDuration.toNanos(); + Iterator> iterator = pending.entrySet().iterator(); + int keptRequests = 0, removedRequests = 0; + while (iterator.hasNext()) { + StreamAndRequest streamAndRequest = checkNotNull(iterator.next().getValue()); + PendingRequest pendingRequest = streamAndRequest.request; + if (!belongsToThisHandler(streamAndRequest) + || nowNanos - pendingRequest.getStartTimeNanos() < maxRetryDurationNanos) { + ++keptRequests; + continue; + } + ++removedRequests; + iterator.remove(); + try { + pendingRequest.completeWithStatus(CommitStatus.ABORTED); + } catch (RuntimeException e) { + LOG.warn("Exception while aborting commit due to retry timeout.", e); + } + } + if (removedRequests > 0) { + LOG.info( + "Aborting {} commits which have exceeded retry deadline, kept {}. Work will be retried as needed by service.", + removedRequests, + keptRequests); + } + } if (status.isOk() && hasPendingRequests()) { LOG.warn("Unexpected requests without responses on drained physical stream, retrying."); } @@ -270,7 +308,7 @@ private void flushInternal(Map requests) if (requests.size() == 1) { Map.Entry elem = requests.entrySet().iterator().next(); - if (elem.getValue().request().getSerializedSize() + if (elem.getValue().getRequest().getSerializedSize() > AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE) { issueMultiChunkRequest(elem.getKey(), elem.getValue()); } else { @@ -286,7 +324,7 @@ private void issueSingleRequest(long id, PendingRequest pendingRequest) StreamingCommitWorkRequest.Builder requestBuilder = StreamingCommitWorkRequest.newBuilder(); requestBuilder .addCommitChunkBuilder() - .setComputationId(pendingRequest.computationId()) + .setComputationId(pendingRequest.getComputationId()) .setRequestId(id) .setShardingKey(pendingRequest.shardingKey()) .setSerializedWorkItemCommit(pendingRequest.serializedCommit()); @@ -311,9 +349,9 @@ private void issueBatchedRequest(Map requests) for (Map.Entry entry : requests.entrySet()) { PendingRequest request = entry.getValue(); StreamingCommitRequestChunk.Builder chunkBuilder = requestBuilder.addCommitChunkBuilder(); - if (lastComputation == null || !lastComputation.equals(request.computationId())) { - chunkBuilder.setComputationId(request.computationId()); - lastComputation = request.computationId(); + if (lastComputation == null || !lastComputation.equals(request.getComputationId())) { + chunkBuilder.setComputationId(request.getComputationId()); + lastComputation = request.getComputationId(); } chunkBuilder .setRequestId(entry.getKey()) @@ -338,7 +376,7 @@ private void issueBatchedRequest(Map requests) private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) throws WindmillStreamShutdownException { - checkNotNull(pendingRequest.computationId(), "Cannot commit WorkItem w/o a computationId."); + checkNotNull(pendingRequest.getComputationId(), "Cannot commit WorkItem w/o a computationId."); ByteString serializedCommit = pendingRequest.serializedCommit(); synchronized (this) { if (isShutdown) { @@ -359,7 +397,7 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) StreamingCommitRequestChunk.newBuilder() .setRequestId(id) .setSerializedWorkItemCommit(chunk) - .setComputationId(pendingRequest.computationId()) + .setComputationId(pendingRequest.getComputationId()) .setShardingKey(pendingRequest.shardingKey()); int remaining = serializedCommit.size() - end; if (remaining > 0) { @@ -376,34 +414,46 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) } } - @AutoValue - abstract static class PendingRequest { + private static class PendingRequest { + private final String computationId; + private final WorkItemCommitRequest request; + private final Consumer onDone; + private final long startTimeNanos; // System.nanoTime() of when request began. - private static PendingRequest create( + private PendingRequest( String computationId, WorkItemCommitRequest request, Consumer onDone) { - return new AutoValue_GrpcCommitWorkStream_PendingRequest(computationId, request, onDone); + this.computationId = computationId; + this.request = request; + this.onDone = onDone; + this.startTimeNanos = System.nanoTime(); } - abstract String computationId(); + String getComputationId() { + return computationId; + } - abstract WorkItemCommitRequest request(); + WorkItemCommitRequest getRequest() { + return request; + } - abstract Consumer onDone(); + long getStartTimeNanos() { + return startTimeNanos; + } private long getBytes() { - return (long) request().getSerializedSize() + computationId().length(); + return (long) request.getSerializedSize() + computationId.length(); } private ByteString serializedCommit() { - return request().toByteString(); + return request.toByteString(); } private void completeWithStatus(CommitStatus commitStatus) { - onDone().accept(commitStatus); + onDone.accept(commitStatus); } private long shardingKey() { - return request().getShardingKey(); + return request.getShardingKey(); } private void abort() { @@ -462,7 +512,7 @@ public boolean commitWorkItem( return false; } - PendingRequest request = PendingRequest.create(computation, commitRequest, onDone); + PendingRequest request = new PendingRequest(computation, commitRequest, onDone); add(idGenerator.incrementAndGet(), request); return true; } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java index 0184b88d53cd..97ca3c4e83d7 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillStreamFactory.java @@ -100,6 +100,7 @@ public class GrpcWindmillStreamFactory implements StatusDataProvider { private final Consumer> processHeartbeatResponses; private final java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter; private final Supplier executorServiceSupplier; + private final java.time.Duration commitWorkStreamRetryTimeout; private GrpcWindmillStreamFactory( JobHeader jobHeader, @@ -111,6 +112,7 @@ private GrpcWindmillStreamFactory( Consumer> processHeartbeatResponses, Supplier maxBackOffSupplier, java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter, + java.time.Duration commitWorkStreamRetryTimeout, Supplier executorServiceSupplier) { this.jobHeader = jobHeader; this.logEveryNStreamFailures = logEveryNStreamFailures; @@ -132,6 +134,7 @@ private GrpcWindmillStreamFactory( this.directStreamingRpcPhysicalStreamHalfCloseAfter = directStreamingRpcPhysicalStreamHalfCloseAfter; this.executorServiceSupplier = executorServiceSupplier; + this.commitWorkStreamRetryTimeout = commitWorkStreamRetryTimeout; } /** @implNote Used for {@link AutoBuilder} {@link Builder} class, do not call directly. */ @@ -146,6 +149,7 @@ static GrpcWindmillStreamFactory create( Supplier maxBackOffSupplier, int healthCheckIntervalMillis, java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter, + java.time.Duration commitWorkStreamRetryTimeout, Supplier scheduledExecutorServiceSupplier) { GrpcWindmillStreamFactory streamFactory = new GrpcWindmillStreamFactory( @@ -158,6 +162,7 @@ static GrpcWindmillStreamFactory create( processHeartbeatResponses, maxBackOffSupplier, directStreamingRpcPhysicalStreamHalfCloseAfter, + commitWorkStreamRetryTimeout, scheduledExecutorServiceSupplier); if (healthCheckIntervalMillis >= 0) { @@ -200,6 +205,7 @@ public static GrpcWindmillStreamFactory.Builder of(JobHeader jobHeader) { .setProcessHeartbeatResponses(ignored -> {}) .setDirectStreamingRpcPhysicalStreamHalfCloseAfter( DEFAULT_DIRECT_STREAMING_RPC_PHYSICAL_STREAM_HALF_CLOSE_AFTER) + .setCommitWorkStreamRetryTimeout(java.time.Duration.ZERO) .setScheduledExecutorServiceSupplier(() -> null); } @@ -347,6 +353,7 @@ public CommitWorkStream createCommitWorkStream(CloudWindmillServiceV1Alpha1Stub streamIdGenerator, streamingRpcBatchLimit, java.time.Duration.ZERO, + commitWorkStreamRetryTimeout, executorForDispatchedStreams("CommitWork")); } @@ -363,6 +370,7 @@ public CommitWorkStream createDirectCommitWorkStream(WindmillConnection connecti streamIdGenerator, streamingRpcBatchLimit, directStreamingRpcPhysicalStreamHalfCloseAfter, + java.time.Duration.ZERO, executorForDirectStreams(connection.backendWorkerToken(), "CommitWork")); } @@ -426,6 +434,8 @@ Builder setProcessHeartbeatResponses( Builder setDirectStreamingRpcPhysicalStreamHalfCloseAfter(java.time.Duration timeout); + Builder setCommitWorkStreamRetryTimeout(java.time.Duration timeout); + Builder setScheduledExecutorServiceSupplier( Supplier scheduledExecutorServiceSupplier); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java index e9fd55fa5668..9c3d5c9c3ef3 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java @@ -38,6 +38,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.apache.beam.runners.dataflow.worker.windmill.CloudWindmillServiceV1Alpha1Grpc; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; @@ -1133,6 +1134,87 @@ public void testCommitWorkItem_multiplePhysicalStreams_multipleHandovers_halfClo assertTrue(commitWorkStream.awaitTermination(10, TimeUnit.SECONDS)); } + @Test + public void testCommitWorkItem_stopsRetriesAfterDuration() throws Exception { + int numCommits = 1; + CountDownLatch commitProcessed = new CountDownLatch(numCommits); + AtomicReference commitStatus = new AtomicReference<>(); + GrpcCommitWorkStream commitWorkStream = + (GrpcCommitWorkStream) + GrpcWindmillStreamFactory.of(TEST_JOB_HEADER) + .setDirectStreamingRpcPhysicalStreamHalfCloseAfter(Duration.ofMinutes(1)) + .setCommitWorkStreamRetryTimeout(Duration.ofNanos(1)) + .build() + .createCommitWorkStream(CloudWindmillServiceV1Alpha1Grpc.newStub(inProcessChannel)); + commitWorkStream.start(); + + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitWorkItem( + COMPUTATION_ID, + workItemCommitRequest(0), + status -> { + commitStatus.set(status); + commitProcessed.countDown(); + })); + } + + // The next request should have some chunks. + assertThat(streamInfo.requests.take().getCommitChunkList()).isNotEmpty(); + + // We won't get responses so we will have some pending requests. + assertThat(commitProcessed.getCount()).isGreaterThan(0); + streamInfo.responseObserver.onError(new IOException("test error")); + commitProcessed.await(); + assertThat(commitStatus.get()).isEqualTo(Windmill.CommitStatus.ABORTED); + } + + @Test + public void testCommitWorkItem_retriesWithLongerCommitRetryTimeout() throws Exception { + int numCommits = 1; + CountDownLatch commitProcessed = new CountDownLatch(numCommits); + AtomicReference commitStatus = new AtomicReference<>(); + GrpcCommitWorkStream commitWorkStream = + (GrpcCommitWorkStream) + GrpcWindmillStreamFactory.of(TEST_JOB_HEADER) + .setDirectStreamingRpcPhysicalStreamHalfCloseAfter(Duration.ofMinutes(1)) + // Verifies that if this is set but is not exceeded that a retry occurs. + .setCommitWorkStreamRetryTimeout(Duration.ofMinutes(10)) + .build() + .createCommitWorkStream(CloudWindmillServiceV1Alpha1Grpc.newStub(inProcessChannel)); + commitWorkStream.start(); + + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitWorkItem( + COMPUTATION_ID, + workItemCommitRequest(0), + status -> { + commitStatus.set(status); + commitProcessed.countDown(); + })); + } + + // The next request should have some chunks. + assertThat(streamInfo.requests.take().getCommitChunkList()).isNotEmpty(); + + // We won't get responses so we will have some pending requests. + assertThat(commitProcessed.getCount()).isGreaterThan(0); + streamInfo.responseObserver.onError(new IOException("test error")); + + // The stream should reconnect and retry the requests. + FakeWindmillGrpcService.CommitStreamInfo reconnectStreamInfo = + waitForConnectionAndConsumeHeader(); + Windmill.StreamingCommitWorkRequest reconnectRequest = reconnectStreamInfo.requests.take(); + assertEquals(1, reconnectRequest.getCommitChunkCount()); + reconnectStreamInfo.responseObserver.onNext( + Windmill.StreamingCommitResponse.newBuilder().addRequestId(1).build()); + commitProcessed.await(); + assertThat(commitStatus.get()).isEqualTo(Windmill.CommitStatus.OK); + } + private FakeWindmillGrpcService.CommitStreamInfo waitForConnectionAndConsumeHeader() { try { FakeWindmillGrpcService.CommitStreamInfo info = fakeService.waitForConnectedCommitStream(); From cb6c409fb07effae0f406a6b5f2301b09f70e5c9 Mon Sep 17 00:00:00 2001 From: Tobias Kaymak Date: Wed, 24 Jun 2026 15:44:06 +0200 Subject: [PATCH 455/490] [mqtt] Fix streaming xlang IT failing the Messaging PostCommit (#39088) * [mqtt] Fix streaming xlang IT: amend test pipeline options, run non-blocking --- ...tCommit_Python_Xlang_Messaging_Direct.json | 2 +- .../io/external/xlang_mqttio_it_test.py | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json index e3d6056a5de9..c537844dc84a 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 3 } diff --git a/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py b/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py index fa6ebed06efe..889096d1f22b 100644 --- a/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py +++ b/sdks/python/apache_beam/io/external/xlang_mqttio_it_test.py @@ -33,7 +33,7 @@ import pytest import apache_beam as beam -from apache_beam.options.pipeline_options import PipelineOptions +from apache_beam.options.pipeline_options import PortableOptions from apache_beam.options.pipeline_options import StandardOptions from apache_beam.testing.test_pipeline import TestPipeline from apache_beam.typehints.row_type import RowTypeConstraint @@ -214,12 +214,21 @@ def subscribe(): publisher.start() subscriber.start() - options = PipelineOptions([ - '--runner=PrismRunner', - '--environment_type=LOOPBACK', - '--streaming', - ]) - p = TestPipeline(options=options) + # MqttIO read is unbounded, so this pipeline runs in streaming mode and + # never terminates on its own. Amend the harness-provided pipeline options + # rather than discarding them: enable streaming, run non-blocking so the + # observe-then-cancel logic below can execute, and target the Prism portable + # runner. The latter is required because SwitchingDirectRunner disables its + # Prism delegation for pipelines containing external (cross-language) + # transforms (see runners/direct/direct_runner.py) and falls back to the + # BundleBasedDirectRunner, which cannot execute an unbounded read. + # The runner is instantiated during TestPipeline construction, so it must be + # passed to the constructor; the remaining harness-provided options are + # preserved and only amended (streaming + LOOPBACK environment) afterwards. + p = TestPipeline(runner='PrismRunner', blocking=False) + p.get_pipeline_options().view_as(StandardOptions).streaming = True + p.get_pipeline_options().view_as( + PortableOptions).environment_type = 'LOOPBACK' p.not_use_test_runner_api = True _ = ( p From b19c3dfd8687e4028fb44705ab4f6febc3aa61a5 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 24 Jun 2026 10:42:05 -0400 Subject: [PATCH 456/490] Add a github workflow to publish vllm image. (#39089) * Add a github workflow to publish vllm image. * Add the new github workflow the workflows/README.md --- .github/workflows/README.md | 1 + .../beam_Publish_Python_VLLM_Image.yml | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .github/workflows/beam_Publish_Python_VLLM_Image.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 1715365f4ec6..4a32aa271df1 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -534,6 +534,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ Publish Beam SDK Snapshots ](https://github.com/apache/beam/actions/workflows/beam_Publish_Beam_SDK_Snapshots.yml) | N/A | [![.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Beam_SDK_Snapshots.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Beam_SDK_Snapshots.yml?query=event%3Aschedule) | | [ Publish BeamMetrics ](https://github.com/apache/beam/actions/workflows/beam_Publish_BeamMetrics.yml) | N/A | [![.github/workflows/beam_Publish_BeamMetrics.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_BeamMetrics.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_BeamMetrics.yml?query=event%3Aschedule) | [ Publish Docker Snapshots ](https://github.com/apache/beam/actions/workflows/beam_Publish_Docker_Snapshots.yml) | N/A | [![.github/workflows/beam_Publish_Docker_Snapshots.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Docker_Snapshots.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Docker_Snapshots.yml?query=event%3Aschedule) | +| [ Publish Python VLLM Image ](https://github.com/apache/beam/actions/workflows/beam_Publish_Python_VLLM_Image.yml) | N/A | [![.github/workflows/beam_Publish_Python_VLLM_Image.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Python_VLLM_Image.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Python_VLLM_Image.yml?query=event%3Aschedule) | | [ Publish Website ](https://github.com/apache/beam/actions/workflows/beam_Publish_Website.yml) | N/A | [![.github/workflows/beam_Publish_Website.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Website.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Website.yml?query=event%3Aschedule) | | [ Release Nightly Snapshot ](https://github.com/apache/beam/actions/workflows/beam_Release_NightlySnapshot.yml) | N/A | [![.github/workflows/beam_Release_NightlySnapshot.yml](https://github.com/apache/beam/actions/workflows/beam_Release_NightlySnapshot.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Release_NightlySnapshot.yml?query=event%3Aschedule) | | [ Release Nightly Snapshot Python ](https://github.com/apache/beam/actions/workflows/beam_Release_Python_NightlySnapshot.yml) | N/A | [![.github/workflows/beam_Release_Python_NightlySnapshot.yml](https://github.com/apache/beam/actions/workflows/beam_Release_Python_NightlySnapshot.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Release_Python_NightlySnapshot.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_Publish_Python_VLLM_Image.yml b/.github/workflows/beam_Publish_Python_VLLM_Image.yml new file mode 100644 index 000000000000..21b6dc8d53c8 --- /dev/null +++ b/.github/workflows/beam_Publish_Python_VLLM_Image.yml @@ -0,0 +1,75 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Publish Python VLLM Image + +on: + schedule: + - cron: '0 0 * * 0' # Run weekly (Sunday at 00:00) + workflow_dispatch: + +# Setting explicit permissions for the action to avoid the default permissions +permissions: + actions: write + pull-requests: read + checks: read + contents: read + deployments: read + id-token: none + issues: read + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.issue.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +jobs: + build_and_push_image: + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Authenticate on GCP + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 + with: + service_account: ${{ secrets.GCP_SA_EMAIL }} + credentials_json: ${{ secrets.GCP_SA_KEY }} + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 + - name: GCloud Docker credential helper + run: | + gcloud auth configure-docker us.gcr.io + - name: Build and Push Multi-Arch Image + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t us.gcr.io/apache-beam-testing/python-postcommit-it/vllm:latest \ + -f sdks/python/apache_beam/ml/inference/test_resources/vllm.dockerfile.old \ + --push \ + . From 2397bb26f6f64040b77993db6df879c6f3b58188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Wed, 24 Jun 2026 18:55:55 +0200 Subject: [PATCH 457/490] [Python] Optimize BigQuery copy jobs in file loads using multi-source copy (#38983) * [GCP] Optimize BigQuery TriggerCopyJobs performance for WRITE_APPEND * Optimize BigQuery copy jobs in file loads using multi-source copy Updates BigQuery file loads in Python SDK to use multi-source copy jobs when copying temporary tables to the final destination table. * Update BigQueryWrapper._insert_copy_job to support a list of source tables, utilizing BigQuery's multi-source copy capability. * Update TriggerCopyJobs to process temporary tables in batch, splitting them into chunks of 1,200 (BigQuery limit) and triggering multi-source copy jobs. * Implement inline wait for the first chunk in TriggerCopyJobs when write disposition is WRITE_TRUNCATE or WRITE_EMPTY and there are multiple chunks. This ensures the destination table is initialized by the first job before subsequent chunks append to it. * Fix grouping key in _load_data for WRITE_TRUNCATE/WRITE_EMPTY to use the full hashable destination instead of just tableId, preventing incorrect grouping of tables with the same name in different datasets. * Fix TriggerLoadJobs to use bq_wrapper with mock client in tests, resolving credential refresh warnings. * Fix PartitionFiles to avoid yielding empty partitions when a file exceeds limits. TAG=agy CONV=126370d2-f42e-4132-a237-16bd5ccf72a3 --- .../apache_beam/io/gcp/bigquery_file_loads.py | 162 +++++++------- .../io/gcp/bigquery_file_loads_test.py | 205 ++++++++++++++---- .../apache_beam/io/gcp/bigquery_tools.py | 18 +- 3 files changed, 250 insertions(+), 135 deletions(-) diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py index 4e45d0324ee2..4ef6c392254b 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py @@ -491,6 +491,8 @@ class TriggerCopyJobs(beam.DoFn): """ TRIGGER_DELETE_TEMP_TABLES = 'TriggerDeleteTempTables' + # https://docs.cloud.google.com/bigquery/quotas#copy_jobs + MAX_SOURCES_PER_COPY_JOB = 1200 def __init__( self, @@ -528,96 +530,90 @@ def process( self, element_list, job_name_prefix=None, unused_schema_mod_jobs=None): if isinstance(element_list, tuple): # Allow this for streaming update compatibility while fixing BEAM-24535. - self.process_one(element_list, job_name_prefix) - else: - for element in element_list: - self.process_one(element, job_name_prefix) + element_list = [element_list] - def process_one(self, element, job_name_prefix): - destination, job_reference = element + if not element_list: + return - copy_to_reference = bigquery_tools.parse_table_reference(destination) + first_destination = element_list[0][0] + copy_to_reference = bigquery_tools.parse_table_reference(first_destination) if copy_to_reference.projectId is None: copy_to_reference.projectId = vp.RuntimeValueProvider.get_value( 'project', str, '') or self.project - copy_from_reference = bigquery_tools.parse_table_reference(destination) - copy_from_reference.tableId = job_reference.jobId - if copy_from_reference.projectId is None: - copy_from_reference.projectId = vp.RuntimeValueProvider.get_value( - 'project', str, '') or self.project - - _LOGGER.info( - "Triggering copy job from %s to %s", - copy_from_reference, - copy_to_reference) + copy_from_references = [] + for destination, job_reference in element_list: + copy_from_reference = bigquery_tools.parse_table_reference(destination) + copy_from_reference.tableId = job_reference.jobId + if copy_from_reference.projectId is None: + copy_from_reference.projectId = vp.RuntimeValueProvider.get_value( + 'project', str, '') or self.project + copy_from_references.append(copy_from_reference) - wait_for_job, write_disposition = ( - self._determine_write_disposition(copy_to_reference)) + full_table_ref = bigquery_tools.get_hashable_destination(copy_to_reference) - if not self.bq_io_metadata: - self.bq_io_metadata = create_bigquery_io_metadata(self._step_name) + is_first_time = full_table_ref not in self._observed_tables + if is_first_time: + self._observed_tables.add(full_table_ref) + if self.bq_io_metadata: + Lineage.sinks().add( + 'bigquery', + copy_to_reference.projectId, + copy_to_reference.datasetId, + copy_to_reference.tableId) + + # Split into chunks of MAX_SOURCES_PER_COPY_JOB + chunks = [ + copy_from_references[i:i + self.MAX_SOURCES_PER_COPY_JOB] + for i in range( + 0, len(copy_from_references), self.MAX_SOURCES_PER_COPY_JOB) + ] + + copy_job_name_base = '%s_%s' % ( + job_name_prefix, + _bq_uuid(bigquery_tools.get_hashable_destination(copy_to_reference))) project_id = ( copy_to_reference.projectId if self.load_job_project_id is None else self.load_job_project_id) - copy_job_name = '%s_%s' % ( - job_name_prefix, - _bq_uuid( - '%s:%s.%s' % ( - copy_from_reference.projectId, - copy_from_reference.datasetId, - copy_from_reference.tableId))) - job_reference = self.bq_wrapper._insert_copy_job( - project_id, - copy_job_name, - copy_from_reference, - copy_to_reference, - create_disposition=self.create_disposition, - write_disposition=write_disposition, - job_labels=self.bq_io_metadata.add_additional_bq_job_labels()) - - if wait_for_job: - self.bq_wrapper.wait_for_bq_job(job_reference, sleep_duration_sec=10) - self.pending_jobs.append( - GlobalWindows.windowed_value((destination, job_reference))) - def _determine_write_disposition(self, copy_to_reference) -> tuple[bool, str]: - """ - Determines the write disposition for a BigQuery copy job, - based on destination. - - When the write_disposition for a job is WRITE_TRUNCATE, multiple copy jobs - to the same destination can interfere with each other, truncate data, and - write to the BigQuery table repeatedly. To prevent this, the first copy job - runs with the user's specified write_disposition, but subsequent jobs must - always use WRITE_APPEND. This ensures that subsequent copy jobs do not - clear out data appended by previous jobs. - - Args: - copy_to_reference: The reference to the destination table. - - Returns: - A tuple containing a boolean indicating whether to wait for the job to - complete and the write disposition to use for the job. - """ - full_table_ref = '%s:%s.%s' % ( - copy_to_reference.projectId, - copy_to_reference.datasetId, - copy_to_reference.tableId) - if full_table_ref not in self._observed_tables: - write_disposition = self.write_disposition - wait_for_job = True - self._observed_tables.add(full_table_ref) - Lineage.sinks().add( - 'bigquery', - copy_to_reference.projectId, - copy_to_reference.datasetId, - copy_to_reference.tableId) - else: - wait_for_job = False - write_disposition = 'WRITE_APPEND' - return wait_for_job, write_disposition + for i, chunk in enumerate(chunks): + if i == 0 and is_first_time: + write_disposition = self.write_disposition + # Wait inline only if we have multiple chunks and write disposition is WRITE_TRUNCATE or WRITE_EMPTY. + # This ensures the first chunk initializes the table, and subsequent chunks (WRITE_APPEND) append to it. + wait_for_job = ( + self.write_disposition in ('WRITE_TRUNCATE', 'WRITE_EMPTY') and + len(chunks) > 1) + else: + write_disposition = 'WRITE_APPEND' + wait_for_job = False + + chunk_job_name = copy_job_name_base + if len(chunks) > 1: + chunk_job_name = f"{copy_job_name_base}_{i}" + + _LOGGER.info( + "Triggering copy job %s from %s to %s (write_disposition: %s)", + chunk_job_name, [str(r) for r in chunk], + copy_to_reference, + write_disposition) + + job_reference = self.bq_wrapper._insert_copy_job( + project_id, + chunk_job_name, + chunk, + copy_to_reference, + create_disposition=self.create_disposition, + write_disposition=write_disposition, + job_labels=self.bq_io_metadata.add_additional_bq_job_labels() + if self.bq_io_metadata else None) + + if wait_for_job: + self.bq_wrapper.wait_for_bq_job(job_reference, sleep_duration_sec=10) + + self.pending_jobs.append( + GlobalWindows.windowed_value((first_destination, job_reference))) def finish_bundle(self): for windowed_value in self.pending_jobs: @@ -744,7 +740,7 @@ def process( else: try: schema = bigquery_tools.table_schema_to_dict( - bigquery_tools.BigQueryWrapper().get_table( + self.bq_wrapper.get_table( project_id=table_reference.projectId, dataset_id=table_reference.datasetId, table_id=table_reference.tableId).schema) @@ -855,7 +851,8 @@ def process(self, element): if latest_partition.can_accept(file_size): latest_partition.add(file_path, file_size) else: - partitions.append(latest_partition.files) + if latest_partition.files: + partitions.append(latest_partition.files) latest_partition = PartitionFiles.Partition( self.max_partition_size, self.max_files_per_partition) latest_partition.add(file_path, file_size) @@ -1181,12 +1178,13 @@ def _load_data( # the truncation happens only once. See # https://github.com/apache/beam/issues/24535. finished_temp_tables_load_job_ids_list_pc = ( - finished_temp_tables_load_job_ids_pc | beam.MapTuple( + finished_temp_tables_load_job_ids_pc + | beam.MapTuple( lambda destination, job_reference: ( - bigquery_tools.parse_table_reference(destination).tableId, + bigquery_tools.get_hashable_destination(destination), (destination, job_reference))) | beam.GroupByKey() - | beam.MapTuple(lambda tableId, batch: list(batch))) + | beam.MapTuple(lambda dest, batch: list(batch))) else: # Loads can happen in parallel. finished_temp_tables_load_job_ids_list_pc = ( diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py index 191719e6a208..47c1ce5ea1bb 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py @@ -924,69 +924,180 @@ def dynamic_destination_resolver(element, *side_inputs): write_disposition=BigQueryDisposition.WRITE_TRUNCATE)) from apache_beam.io.gcp.internal.clients.bigquery import TableReference - mock_insert_copy_job.assert_has_calls( - [ - call( - 'project1', - mock.ANY, + mock_insert_copy_job.assert_has_calls([ + call( + 'project1', + mock.ANY, + [ TableReference( datasetId='dataset1', projectId='project1', tableId='job_name1'), - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='table1'), - create_disposition=None, - write_disposition='WRITE_TRUNCATE', - job_labels={'step_name': 'bigquerybatchfileloads'}), - call( - 'project1', - mock.ANY, TableReference( datasetId='dataset1', projectId='project1', tableId='job_name1'), - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='table1'), - create_disposition=None, - write_disposition='WRITE_APPEND', - job_labels={'step_name': 'bigquerybatchfileloads'}), - call( - 'project1', - mock.ANY, + ], + TableReference( + datasetId='dataset1', projectId='project1', tableId='table1'), + create_disposition=None, + write_disposition='WRITE_TRUNCATE', + job_labels={'step_name': 'bigquerybatchfileloads'}), + call( + 'project1', + mock.ANY, + [ TableReference( datasetId='dataset2', projectId='project1', tableId='job_name1'), - TableReference( - datasetId='dataset2', - projectId='project1', - tableId='table1'), - create_disposition=None, - # Previously this was `WRITE_APPEND`. - write_disposition='WRITE_TRUNCATE', - job_labels={'step_name': 'bigquerybatchfileloads'}), - call( - 'project1', - mock.ANY, + ], + TableReference( + datasetId='dataset2', projectId='project1', tableId='table1'), + create_disposition=None, + write_disposition='WRITE_TRUNCATE', + job_labels={'step_name': 'bigquerybatchfileloads'}), + call( + 'project1', + mock.ANY, + [ TableReference( datasetId='dataset3', projectId='project1', tableId='job_name1'), - TableReference( - datasetId='dataset3', - projectId='project1', - tableId='table1'), - create_disposition=None, - # Previously this was `WRITE_APPEND`. - write_disposition='WRITE_TRUNCATE', - job_labels={'step_name': 'bigquerybatchfileloads'}), - ], - any_order=True) - self.assertEqual(4, mock_insert_copy_job.call_count) + ], + TableReference( + datasetId='dataset3', projectId='project1', tableId='table1'), + create_disposition=None, + write_disposition='WRITE_TRUNCATE', + job_labels={'step_name': 'bigquerybatchfileloads'}), + ], + any_order=True) + self.assertEqual(3, mock_insert_copy_job.call_count) + + @mock.patch( + 'apache_beam.io.gcp.bigquery_tools.BigQueryWrapper.wait_for_bq_job') + @mock.patch( + 'apache_beam.io.gcp.bigquery_tools.BigQueryWrapper._insert_copy_job') + def test_copy_jobs_splitting( + self, mock_insert_copy_job, mock_wait_for_bq_job): + destination = 'project1:dataset1.table1' + + from apache_beam.io.gcp.bigquery_file_loads import TriggerCopyJobs + original_max_sources = TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB + TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB = 2 + + try: + job_reference = bigquery_api.JobReference() + job_reference.projectId = 'project1' + job_reference.jobId = 'job_name1' + result_job = mock.Mock() + result_job.jobReference = job_reference + + mock_job = mock.Mock() + mock_job.status.state = 'DONE' + mock_job.status.errorResult = None + mock_job.jobReference = job_reference + + bq_client = mock.Mock() + bq_client.jobs.Get.return_value = mock_job + bq_client.jobs.Insert.return_value = result_job + bq_client.tables.Delete.return_value = None + mock_insert_copy_job.return_value = job_reference + temp_dir = self._new_tempdir() + + with TestPipeline('FnApiRunner') as p: + _ = ( + p + | beam.Create([ + { + 'name': 'a' + }, + { + 'name': 'b' + }, + { + 'name': 'c' + }, + { + 'name': 'd' + }, + { + 'name': 'e' + }, + ], + reshuffle=False) + | bqfl.BigQueryBatchFileLoads( + destination, + custom_gcs_temp_location=temp_dir, + test_client=bq_client, + validate=False, + temp_file_format=bigquery_tools.FileFormat.JSON, + max_file_size=10, + max_partition_size=10, + max_files_per_partition=1, + write_disposition=BigQueryDisposition.WRITE_TRUNCATE)) + + self.assertEqual(3, mock_insert_copy_job.call_count) + + from apache_beam.io.gcp.internal.clients.bigquery import TableReference + expected_calls = [ + call( + 'project1', + mock.ANY, + [ + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='job_name1'), + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='job_name1'), + ], + TableReference( + datasetId='dataset1', projectId='project1', tableId='table1'), + create_disposition=None, + write_disposition='WRITE_TRUNCATE', + job_labels=mock.ANY), + call( + 'project1', + mock.ANY, + [ + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='job_name1'), + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='job_name1'), + ], + TableReference( + datasetId='dataset1', projectId='project1', tableId='table1'), + create_disposition=None, + write_disposition='WRITE_APPEND', + job_labels=mock.ANY), + call( + 'project1', + mock.ANY, + [ + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='job_name1'), + ], + TableReference( + datasetId='dataset1', projectId='project1', tableId='table1'), + create_disposition=None, + write_disposition='WRITE_APPEND', + job_labels=mock.ANY), + ] + mock_insert_copy_job.assert_has_calls(expected_calls, any_order=True) + self.assertEqual(9, mock_wait_for_bq_job.call_count) + + finally: + TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB = original_max_sources @parameterized.expand([ param(is_streaming=False, with_auto_sharding=False, compat_version=None), diff --git a/sdks/python/apache_beam/io/gcp/bigquery_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_tools.py index 8dd58cd55a01..491b7a39b0b7 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_tools.py @@ -506,16 +506,22 @@ def _insert_copy_job( reference = bigquery.JobReference() reference.jobId = job_id reference.projectId = project_id + + copy_config = bigquery.JobConfigurationTableCopy( + destinationTable=to_table_reference, + createDisposition=create_disposition, + writeDisposition=write_disposition, + ) + if isinstance(from_table_reference, list): + copy_config.sourceTables = from_table_reference + else: + copy_config.sourceTable = from_table_reference + request = bigquery.BigqueryJobsInsertRequest( projectId=project_id, job=bigquery.Job( configuration=bigquery.JobConfiguration( - copy=bigquery.JobConfigurationTableCopy( - destinationTable=to_table_reference, - sourceTable=from_table_reference, - createDisposition=create_disposition, - writeDisposition=write_disposition, - ), + copy=copy_config, labels=_build_job_labels(job_labels), ), jobReference=reference, From ad7df112f56ea75f32897a84dd36517150e2f822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Wed, 24 Jun 2026 18:56:23 +0200 Subject: [PATCH 458/490] [OpenTelemetry] Turn off gcp otel auth extension by default. (#38940) * turn off gcp otel auth extension by default. --- .../runners/dataflow/worker/StreamingDataflowWorker.java | 6 ++++++ .../src/main/java/org/apache/beam/fn/harness/FnHarness.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java index 9f063d393703..180dda153bb6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java @@ -1061,6 +1061,12 @@ public static void main(String[] args) throws Exception { } }); LOG.info("Enabled Open Telemetry with properties: {}", openTelemetryProperties); + } else { + // turn off auth extension so it doesn't interfere if user is configuring otel e.g. via + // JvmInitializer. + if (System.getProperty("google.otel.auth.target.signals") == null) { + System.setProperty("google.otel.auth.target.signals", "none"); + } } LOG.debug("Creating StreamingDataflowWorker from options: {}", options); diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java index 60e83251f147..7bda0e18cad8 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnHarness.java @@ -315,6 +315,12 @@ public static void main( } }); LOG.info("Enabled Open Telemetry with properties: {}", openTelemetryProperties); + } else { + // turn off auth extension so it doesn't interfere if user is configuring otel e.g. via + // JvmInitializer. + if (System.getProperty("google.otel.auth.target.signals") == null) { + System.setProperty("google.otel.auth.target.signals", "none"); + } } EnumMap< BeamFnApi.InstructionRequest.RequestCase, From 75da7a46bbb2064326128e74bd35921f8ab8a532 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Tue, 23 Jun 2026 14:35:12 -0400 Subject: [PATCH 459/490] Fix Flink XVR for Flink 2 --- .../beam_PostCommit_XVR_Flink.json | 2 +- .../beam_PostCommit_XVR_Samza.json | 1 - .../flink/job-server/flink_job_server.gradle | 20 +++++++++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) delete mode 100644 .github/trigger_files/beam_PostCommit_XVR_Samza.json diff --git a/.github/trigger_files/beam_PostCommit_XVR_Flink.json b/.github/trigger_files/beam_PostCommit_XVR_Flink.json index a926b3314ed6..7dcd6398db10 100644 --- a/.github/trigger_files/beam_PostCommit_XVR_Flink.json +++ b/.github/trigger_files/beam_PostCommit_XVR_Flink.json @@ -1,4 +1,4 @@ { - "modification": 2, + "modification": 3, "trigger-2026-04-04": "portable_runner expand_sdf opt-in" } diff --git a/.github/trigger_files/beam_PostCommit_XVR_Samza.json b/.github/trigger_files/beam_PostCommit_XVR_Samza.json deleted file mode 100644 index 2bf3f556083b..000000000000 --- a/.github/trigger_files/beam_PostCommit_XVR_Samza.json +++ /dev/null @@ -1 +0,0 @@ -{"modification": 2} \ No newline at end of file diff --git a/runners/flink/job-server/flink_job_server.gradle b/runners/flink/job-server/flink_job_server.gradle index 335439cbda91..a6abca5e8586 100644 --- a/runners/flink/job-server/flink_job_server.gradle +++ b/runners/flink/job-server/flink_job_server.gradle @@ -248,10 +248,26 @@ def setupTask = project.tasks.register("flinkJobServerSetup", Exec) { def flinkJobServerJar = shadowJar.archivePath def flinkDir = project.project(":runners:flink").projectDir def additionalArgs = "" - if (project.hasProperty('flinkConfDir')) + + if (project.hasProperty('flinkConfDir')) { additionalArgs += " --flink-conf-dir=${project.property('flinkConfDir')}" - else + } + else if (isFlink2) { + def flinkConfDir = "$flinkDir/2.0/src/test/resources" + additionalArgs += "--flink-conf-dir=${project.buildDir}/flink-conf" + + doFirst { + copy { + from "$flinkDir/2.0/src/test/resources/flink-test-config.yaml" + into "${project.buildDir}/flink-conf" + + // Rename the file during the copy process + rename 'flink-test-config.yaml', 'config.yaml' + } + } + } else { additionalArgs += "--flink-conf-dir=$flinkDir/src/test/resources" + } executable 'sh' args '-c', "$pythonDir/scripts/run_job_server.sh stop --group_id ${project.name} && $pythonDir/scripts/run_job_server.sh start --group_id ${project.name} --job_port ${jobPort} --artifact_port ${artifactPort} --job_server_jar ${flinkJobServerJar} --additional_args \"${additionalArgs}\"" From 190c64f3591216bbc87e72dc851ed56b183c7471 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 24 Jun 2026 09:57:08 -0400 Subject: [PATCH 460/490] Fix Go Flink VR conf path --- .github/trigger_files/beam_PostCommit_Go_VR_Flink.json | 2 +- sdks/go/test/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json b/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json index 939c43396fda..504c32974c7c 100644 --- a/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json +++ b/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json @@ -1,6 +1,6 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 2, + "modification": 3, "https://github.com/apache/beam/pull/32440": "testing datastream optimizations", "pr": "37640" } diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index 3437ba12f2c7..8fcb09166146 100644 --- a/sdks/go/test/build.gradle +++ b/sdks/go/test/build.gradle @@ -92,7 +92,7 @@ task flinkValidatesRunner { doFirst { // Copy Flink conf file copy { - from "${project.rootDir}/runners/flink/${flinkVersion}/src/test/resources/flink-test-config.yaml" + from "${project.rootDir}/runners/flink/2.0/src/test/resources/flink-test-config.yaml" into "${project.buildDir}/flink-conf" // Rename the file during the copy process From ab280675ff93d0a5a2714ce00534f31e28d84128 Mon Sep 17 00:00:00 2001 From: Yi Hu Date: Wed, 24 Jun 2026 10:20:30 -0400 Subject: [PATCH 461/490] Update CHANGES.md and website --- CHANGES.md | 4 ++-- website/www/site/content/en/documentation/runners/flink.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ac571c2f55ae..6d7bad8c97f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -75,8 +75,8 @@ * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). * (Python) Added instrumentation to support off-the-shelf profiling agents when launching Python SDK Harness ([#38853](https://github.com/apache/beam/issues/38853)). * (Java) Added support to the FnApi Data stream protocol allowing runners to isolate bundles slowly processing input from other bundles. ([#39001](https://github.com/apache/beam/issues/39001)). -* (Java) Flink 2.1 support added ([#38947](https://github.com/apache/beam/issues/38947)). -* (Java) Flink 2.2 support added ([#38978](https://github.com/apache/beam/issues/38978)). +* (Java) Flink 2.1 and 2.2 support is added ([#38947](https://github.com/apache/beam/issues/38947)) ([#38978](https://github.com/apache/beam/issues/38978)); Flink 1.17 and 1.18 support is dropped. +* (Python) MqttIO is now supported in Python via cross-language ([#21060](https://github.com/apache/beam/issues/21060)). ## Breaking Changes diff --git a/website/www/site/content/en/documentation/runners/flink.md b/website/www/site/content/en/documentation/runners/flink.md index d201d2546f8c..e924ccdb7bd6 100644 --- a/website/www/site/content/en/documentation/runners/flink.md +++ b/website/www/site/content/en/documentation/runners/flink.md @@ -358,12 +358,12 @@ To find out which version of Flink is compatible with Beam please see the table 1.18.x beam-runners-flink-1.18 - ≥ 2.57.0 + 2.57.0 - 2.74.0 1.17.x beam-runners-flink-1.17 - ≥ 2.56.0 + 2.56.0 - 2.74.0 1.16.x From bc25c5acf22819bdf365d15aec85f56a1b262eaa Mon Sep 17 00:00:00 2001 From: Arun Pandian Date: Wed, 24 Jun 2026 16:29:12 +0000 Subject: [PATCH 462/490] Disable flaky SDF finalization tests --- runners/google-cloud-dataflow-java/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index c569c03b956e..015825fd6de1 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -486,6 +486,9 @@ def validatesRunnerStreamingConfig = [ 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionInSetupStateful', 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionInStartBundle', 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionInStartBundleStateful', + // Bundle finalizer tests are flaky https://github.com/apache/beam/issues/38710 + 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testBundleFinalizationOccursOnBoundedSplittableDoFn', + 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testBundleFinalizationOccursOnUnboundedSplittableDoFn', ] ] From 9265c97d6b8551a3b5cab15dbbbea1aeff21ccec Mon Sep 17 00:00:00 2001 From: Vitaly Terentyev Date: Wed, 24 Jun 2026 23:28:31 +0400 Subject: [PATCH 463/490] Fix Install xmllint --- .github/workflows/cut_release_branch.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cut_release_branch.yml b/.github/workflows/cut_release_branch.yml index ee571874ce13..8b591524ba55 100644 --- a/.github/workflows/cut_release_branch.yml +++ b/.github/workflows/cut_release_branch.yml @@ -116,7 +116,10 @@ jobs: git config user.name $GITHUB_ACTOR git config user.email actions@"$RUNNER_NAME".local - name: Install xmllint - run: sudo apt-get install -y libxml2-utils + run: | + sudo apt-get clean + sudo apt-get update + sudo apt-get install -y --no-install-recommends libxml2-utils - name: Update .asf.yaml to protect new release branch from force push run: | sed -i -e "s/master: {}/master: {}\n release-${RELEASE}: {}/g" .asf.yaml From 9f34821a12685e19fc5ab7da3b6f2aeefead71ba Mon Sep 17 00:00:00 2001 From: Amar3tto Date: Wed, 24 Jun 2026 20:27:18 +0000 Subject: [PATCH 464/490] Moving to 2.76.0-SNAPSHOT on master branch. --- .asf.yaml | 1 + gradle.properties | 4 ++-- scripts/beam-sql.sh | 2 +- sdks/go/pkg/beam/core/core.go | 2 +- sdks/python/apache_beam/version.py | 2 +- sdks/typescript/package.json | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.asf.yaml b/.asf.yaml index b650499326d9..7adaf351c8f5 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -51,6 +51,7 @@ github: protected_branches: master: {} + release-2.75: {} release-2.74.0-postrelease: {} release-2.74: {} release-2.73.0-postrelease: {} diff --git a/gradle.properties b/gradle.properties index bc84397c63ce..1200e0a9f7f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,8 +30,8 @@ signing.gnupg.useLegacyGpg=true # buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy. # To build a custom Beam version make sure you change it in both places, see # https://github.com/apache/beam/issues/21302. -version=2.75.0-SNAPSHOT -sdk_version=2.75.0.dev +version=2.76.0-SNAPSHOT +sdk_version=2.76.0.dev javaVersion=11 diff --git a/scripts/beam-sql.sh b/scripts/beam-sql.sh index 9df48f6c18d4..906812eb1597 100755 --- a/scripts/beam-sql.sh +++ b/scripts/beam-sql.sh @@ -22,7 +22,7 @@ set -e # Exit immediately if a command exits with a non-zero status. # --- Configuration --- -DEFAULT_BEAM_VERSION="2.75.0" +DEFAULT_BEAM_VERSION="2.76.0" MAIN_CLASS="org.apache.beam.sdk.extensions.sql.jdbc.BeamSqlLine" # Directory to store cached executable JAR files CACHE_DIR="${HOME}/.beam/cache" diff --git a/sdks/go/pkg/beam/core/core.go b/sdks/go/pkg/beam/core/core.go index e3a8900b1e77..c9c26a7311cd 100644 --- a/sdks/go/pkg/beam/core/core.go +++ b/sdks/go/pkg/beam/core/core.go @@ -27,7 +27,7 @@ const ( // SdkName is the human readable name of the SDK for UserAgents. SdkName = "Apache Beam SDK for Go" // SdkVersion is the current version of the SDK. - SdkVersion = "2.75.0.dev" + SdkVersion = "2.76.0.dev" // DefaultDockerImage represents the associated image for this release. DefaultDockerImage = "apache/beam_go_sdk:" + SdkVersion diff --git a/sdks/python/apache_beam/version.py b/sdks/python/apache_beam/version.py index 7d1cd44ad012..870b93e530d6 100644 --- a/sdks/python/apache_beam/version.py +++ b/sdks/python/apache_beam/version.py @@ -17,4 +17,4 @@ """Apache Beam SDK version information and utilities.""" -__version__ = '2.75.0.dev' +__version__ = '2.76.0.dev' diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index a9468b04ac8b..d042bbaa7814 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "apache-beam", - "version": "2.75.0-SNAPSHOT", + "version": "2.76.0-SNAPSHOT", "devDependencies": { "@google-cloud/bigquery": "^5.12.0", "@types/mocha": "^9.0.0", From 28613651581b683d38a46c9c1f2e65bbdac257dc Mon Sep 17 00:00:00 2001 From: claudevdm <33973061+claudevdm@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:54:03 -0400 Subject: [PATCH 465/490] Update beam-master (#39095) --- runners/google-cloud-dataflow-java/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index 015825fd6de1..3d1f8d78777c 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -52,8 +52,8 @@ evaluationDependsOn(":sdks:java:container:java11") ext.dataflowLegacyEnvironmentMajorVersion = '8' ext.dataflowFnapiEnvironmentMajorVersion = '8' -ext.dataflowLegacyContainerVersion = 'beam-master-20260601' -ext.dataflowFnapiContainerVersion = 'beam-master-20260601' +ext.dataflowLegacyContainerVersion = 'beam-master-20260624' +ext.dataflowFnapiContainerVersion = 'beam-master-20260624' ext.dataflowContainerBaseRepository = 'gcr.io/cloud-dataflow/v1beta3' processResources { From 213b623ebcbdae6f09ed5b846622b4ecfeea743c Mon Sep 17 00:00:00 2001 From: HansMarcus01 Date: Wed, 24 Jun 2026 17:12:06 -0600 Subject: [PATCH 466/490] =?UTF-8?q?[GSOC=202026]=20Adding=20logic=20to=20v?= =?UTF-8?q?alidate=20and=20generate=20an=20error=20if=20there=20are=20key?= =?UTF-8?q?=E2=80=A6=20(#38992)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Adding logic to validate and generate an error if there are keys linked to a service account that was not rotated by our system * Fixing a critical bug to correctly identify obtained secrets * Feat: Add the notification system via Github/Issue for keys not managed by Beam's rotation system * Remove the unused Torch library and add a history for old reports to the problem body. --- infra/enforcement/account_keys.py | 103 ++++++++++++++++++++++++----- infra/enforcement/sending.py | 105 +++++++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 18 deletions(-) diff --git a/infra/enforcement/account_keys.py b/infra/enforcement/account_keys.py index 4c3a8190d23f..31c1354319d6 100644 --- a/infra/enforcement/account_keys.py +++ b/infra/enforcement/account_keys.py @@ -106,6 +106,53 @@ def _denormalize_username(self, username: str) -> str: return username.split(":", 1)[1].strip().lower() return username + def _get_user_managed_keys_from_iam(self, account_email: str) -> List[str]: + """" + Retrieves the list of user-managed keys for a given service account from IAM. + + Args: + account_email (str): The email of the service account to retrieve keys for. + + Returns: + List[str]: A list of key IDs for the user-managed keys associated with the service account + """ + request = types.ListServiceAccountKeysRequest() + request.name = f"projects/{self.project_id}/serviceAccounts/{account_email}" + request.key_types = [types.ListServiceAccountKeysRequest.KeyType.USER_MANAGED] + + try: + response = self.service_account_client.list_service_account_keys(request=request) + return [key.name.split("/")[-1] for key in response.keys] + except Exception as e: + self.logger.error(f"Failed to retrieve keys for service account '{account_email}': {e}") + return [] + + def _get_verified_keys_from_secret_manager(self, secret_name: str) -> List[str]: + """ + Retrieves the list of verified keys for a given service account from Secret Manager. + + Args: + secret_name (str): The name of the secret to retrieve keys for. + + Returns: + List[str]: A list of key IDs for the verified keys associated with the service account. + """ + verified_keys = [] + parent = self.secret_client.secret_path(self.project_id, secret_name) + + try: + versions = self.secret_client.list_secret_versions(request={"parent": parent}) + for version in versions: + if version.state.name == secretmanager.SecretVersion.State.ENABLED: + response = self.secret_client.access_secret_version(request={"name": version.name}) + data_str = response.payload.data.decode("UTF-8") + key_id = data_str.split(":",1)[0] + verified_keys.append(key_id) + return verified_keys + except Exception as e: + self.logger.error(f"Failed to retrieve verified keys from Secret Manager for secret '{secret_name}': {e}") + return [] + def _get_all_live_service_accounts(self) -> List[str]: """ Retrieves all service accounts that are currently active (not disabled) in the project. @@ -259,21 +306,35 @@ def check_compliance(self) -> List[str]: self.logger.info(f"No service account keys found in the {self.service_account_keys_file}.") compliance_issues = [] + live_service_accounts = self._get_all_live_service_accounts() + managed_secrets = self._get_all_live_managed_secrets() # Check that all service accounts that exist are declared - for service_account in self._get_all_live_service_accounts(): + for service_account in live_service_accounts: if self._denormalize_account_email(service_account) not in [account["account_id"] for account in file_service_accounts]: msg = f"Service account '{service_account}' is not declared in the service account keys file." compliance_issues.append(msg) self.logger.warning(msg) + else: + iam_keys = self._get_user_managed_keys_from_iam(service_account) + if iam_keys: + secret_name = f"{self._denormalize_account_email(service_account)}-key" + legal_keys = [] + if secret_name in managed_secrets: + legal_keys = self._get_verified_keys_from_secret_manager(secret_name) + unmanaged_keys = set(iam_keys) - set(legal_keys) + for unmanaged_key in unmanaged_keys: + msg = f"SECURITY ALERT: Unmanaged key '{unmanaged_key}' detected on account '{service_account}'. This key was created outside of Beam's service account management system. " + compliance_issues.append(msg) + self.logger.warning(msg) - managed_secrets = self._get_all_live_managed_secrets() extracted_secrets = [f"{self._denormalize_account_email(account['account_id'])}-key" for account in file_service_accounts] # Check for managed secrets that are not declared for secret in managed_secrets: if secret not in extracted_secrets: - msg = f"Managed secret '{secret}' is not declared in the service account keys file." + masked_secret = f"{secret[:4]}***{secret[-4:]}" if len(secret) >= 8 else "***" + msg = f"Managed secret '{masked_secret}' is not declared in the service account keys file." compliance_issues.append(msg) self.logger.warning(msg) @@ -307,23 +368,34 @@ def create_announcement(self, recipient: str) -> None: """ if not self.sending_client: raise ValueError("SendingClient is required for creating announcements") - + diff = self.check_compliance() if not diff: self.logger.info("No compliance issues found, no announcement will be created.") - return + return - title = f"Account Keys Compliance Issue Detected" - body = f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n" - for issue in diff: - body += f"- {issue}\n" + unmanaged_keys_issues = [issue for issue in diff if "SECURITY ALERT" in issue] + general_issues = [issue for issue in diff if "SECURITY ALERT" not in issue] - announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the Account Keys policy for project {self.project_id}.\n\n" - announcement += f"We found {len(diff)} compliance issue(s) that need your attention.\n" - announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations." + if general_issues: + self.logger.info(f"Found {len(general_issues)} general compliance issues. Triggering announcement...") + title = f"Account Keys Compliance Issue Detected" + body = f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n" + for issue in general_issues: + body += f"- {issue}\n" - self.sending_client.create_announcement(title, body, recipient, announcement) + announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the Account Keys policy for project {self.project_id}.\n\n" + announcement += f"We found {len(general_issues)} compliance issue(s) that need your attention.\n" + announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations." + + self.sending_client.create_announcement(title, body, recipient, announcement) + if unmanaged_keys_issues: + self.logger.info(f"Found {len(unmanaged_keys_issues)} unmanaged key security alerts. Dispatching to GitHub security issue...") + self.sending_client.report_unmanaged_keys(self.project_id, unmanaged_keys_issues) + else: + self.logger.info("No unmanaged key security alerts found, Checking if there are open security issues to auto-close...") + self.sending_client.resolve_unmanaged_keys() def print_announcement(self, recipient: str) -> None: """ @@ -382,7 +454,8 @@ def generate_compliance(self) -> None: # Check for managed secrets that are not declared, if not, add them for secret in managed_secrets: if secret not in extracted_secrets: - self.logger.info(f"Managed secret '{secret}' is not declared in the service account keys file, adding it") + masked_secret = f"{secret[:4]}***{secret[-4:]}" if len(secret) >= 8 else "***" + self.logger.info(f"Managed secret '{masked_secret}' is not declared in the service account keys file, adding it") file_service_accounts.append({ "account_id": secret.strip("-key"), "display_name": self._normalize_account_email(secret.strip("-key")), @@ -514,7 +587,7 @@ def main(): logger.error(f"Unknown action: {action}") return 1 except Exception as e: - logger.error(f"Error executing action '{action}': {e}") + logger.exception(f"Error executing action '{action}': {e}") return 1 return 0 diff --git a/infra/enforcement/sending.py b/infra/enforcement/sending.py index 961674ca2f17..37de025a207f 100644 --- a/infra/enforcement/sending.py +++ b/infra/enforcement/sending.py @@ -102,10 +102,21 @@ def _get_open_issues(self, title: str) -> List[GitHubIssue]: Args: title (str): The title of the GitHub issue. """ - endpoint = f"search/issues/?q=is:issue+repo:{self.github_repo}+in:title+{title}+is:open" + endpoint = f"search/issues?q=is:issue+repo:{self.github_repo}+in:title+{title}+is:open" response = self._make_github_request("GET", endpoint) issues = response.json().get('items', []) - return [GitHubIssue(**issue) for issue in issues] + parsed_issues = [] + for issue in issues: + parsed_issues.append(GitHubIssue( + number=issue.get("number"), + title=issue.get("title"), + body=issue.get("body"), + state=issue.get("state"), + html_url=issue.get("html_url"), + created_at=issue.get("created_at"), + updated_at=issue.get("updated_at") + )) + return parsed_issues def create_issue(self, title: str, body: str) -> GitHubIssue: """ @@ -119,7 +130,16 @@ def create_issue(self, title: str, body: str) -> GitHubIssue: payload = {"title": title, "body": body} response = self._make_github_request("POST", endpoint, json=payload) self.logger.info(f"Successfully created GitHub issue: {title}") - return GitHubIssue(**response.json()) + data = response.json() + return GitHubIssue( + number=data.get("number"), + title=data.get("title"), + body=data.get("body"), + state=data.get("state"), + html_url=data.get("html_url"), + created_at=data.get("created_at"), + updated_at=data.get("updated_at") + ) def update_issue_body(self, issue_number: int, new_body: str) -> None: """ @@ -134,6 +154,85 @@ def update_issue_body(self, issue_number: int, new_body: str) -> None: self._make_github_request("PATCH", endpoint, json=payload) self.logger.info(f"Successfully updated body on GitHub issue: #{issue_number}") + def create_issue_comment(self, issue_number: int, comment_body: str) -> None: + """ + Adds a new comment to an existing GitHub issue in the specified repository. + + Args: + issue_number (int): The number of the GitHub issue to comment on. + comment_body (str): The content of the comment to add to the GitHub issue. + """ + endpoint = f"repos/{self.github_repo}/issues/{issue_number}/comments" + payload = {"body": comment_body} + self._make_github_request("POST", endpoint, json=payload) + self.logger.info(f"Successfully added comment to GitHub issue: #{issue_number}") + + def report_unmanaged_keys(self, project_id: str, compilance_issues: List[str]) -> None: + """ + Report compliance issues regarding unmanaged keys into a single GitHub issue. + Creates a new issue if none exists. If it exists, updates the body with the newest + report and moves the previous content into a collapsed history section. + + Args: + project_id (str): The ID of the project associated with the unmanaged keys. + compilance_issues (List[str]): A list of compliance issues related to the unmanaged keys. + """ + if not compilance_issues: + self.logger.info("No compliance issues to report to Github.") + return + + issue_title = "[SECURITY] Action Required: Unmanaged Service Account Keys Detected" + #markdown body + timestamp = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + new_report = f"### Unmanaged Keys Audit Report ({timestamp})\n" + new_report += f"The following unauthorized or unmanaged keys were detected in `{project_id}`:\n\n" + + for issue_text in compilance_issues: + new_report += f"- {issue_text}\n" + + new_report += "\n*Please investigate and revoke these keys if they are not part of the official rotation system.*" + open_issues = self._get_open_issues(issue_title) + + if open_issues: + target_issue = open_issues[0] + self.logger.info(f"Appending report and archiving history to existing security issue #{target_issue.number}") + + old_body = target_issue.body or "" + history_marker = "### History\n
      \nClick to expand\n\n" + + if history_marker in old_body: + # If history already exists, append the new report to it + headed = old_body.split(history_marker) + last_report = headed[0].strip() + old_history = headed[1].replace("
      ", "").strip() + + combined_history = f"{last_report}\n\n---\n\n{old_history}" + else: + combined_history = old_body.strip() + + final_body = f"{new_report}\n\n{history_marker}{combined_history}\n" + self.update_issue_body(target_issue.number, final_body) + else: + self.logger.info("Creating new security issue for unmanaged keys report.") + new_issue = self.create_issue(issue_title, new_report) + self.logger.info(f"Created new security issue : {new_issue.html_url}.") + + def resolve_unmanaged_keys(self) -> None: + """ + Finds any open security issues regarding rogue keys and automatically closes them + if the infrastructure is now healthy. + """ + issue_title = "[SECURITY] Action Required: Unmanaged Service Account Keys Detected" + open_issues = self._get_open_issues(issue_title) + if open_issues: + target_issue = open_issues[0] + self.logger.info(f"All rogue keys resolved! Auto-closing issue #{target_issue.number}.") + self.create_issue_comment(target_issue.number, "All previously reported unmanaged keys have been resolved. Closing this issue.") + endpoint = f"repos/{self.github_repo}/issues/{target_issue.number}" + payload = {"state": "closed"} + self._make_github_request("PATCH", endpoint, json=payload) + + def create_announcement(self, title: str, body: str, recipient: str, announcement: str) -> None: """ This method sends an email with an announcement. The email will point to a GitHub issue. From 2e2091b4e592bb0c5992df99cf26b51cce9b19ca Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Thu, 25 Jun 2026 03:59:39 -0400 Subject: [PATCH 467/490] update changes log for yaml improvements etc (#39093) --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 6d7bad8c97f9..e5975afc56e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,6 +61,7 @@ * Python SDK now supports memory profiling with Memray ([#38853](https://github.com/apache/beam/issues/38853)). * (Python) Added [Qdrant](https://qdrant.tech/) VectorDatabaseWriteConfig implementation ([#38141](https://github.com/apache/beam/issues/38141)). +* (CodeQL) Enabled Code scanning alerts in GitHub repo. ([#38893](https://github.com/apache/beam/issues/38893)). ## I/Os @@ -68,6 +69,7 @@ * Support for reading from Delta Lake added (Java) ([#38551](https://github.com/apache/beam/issues/38551)). * Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * ClickHouseIO: support writing `DateTime64(precision[, 'timezone'])` columns with sub-second precision (Java) ([#38466](https://github.com/apache/beam/issues/38466)). +* Upgraded IO Expansion Service to Java 17 ([#38974](https://github.com/apache/beam/issues/38974)). ## New Features / Improvements @@ -75,6 +77,10 @@ * (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming Engine jobs. It can be disabled by passing `--experiments=disable_streaming_engine_state_tag_encoding_v2` or `--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag encoding version cannot change during a job update. Jobs using tag encoding v2 (enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam versions prior to 2.73.0, as only versions 2.73.0 and later support tag encoding v2. ([#38705](https://github.com/apache/beam/issues/38705)). * (Python) Added instrumentation to support off-the-shelf profiling agents when launching Python SDK Harness ([#38853](https://github.com/apache/beam/issues/38853)). * (Java) Added support to the FnApi Data stream protocol allowing runners to isolate bundles slowly processing input from other bundles. ([#39001](https://github.com/apache/beam/issues/39001)). +* (YAML) Switched js2py library to Quickjs ([#38473](https://github.com/apache/beam/issues/38473)). +* (YAML) Added HuggingFaceModelHandler for YAML usage ([#38696](https://github.com/apache/beam/issues/38696)). +* (YAML) Added WriteToMongoDB transform ([#38376](https://github.com/apache/beam/issues/38376)). +* (YAML) Added WriteToDatadog transform ([#38362](https://github.com/apache/beam/issues/38362)). * (Java) Flink 2.1 and 2.2 support is added ([#38947](https://github.com/apache/beam/issues/38947)) ([#38978](https://github.com/apache/beam/issues/38978)); Flink 1.17 and 1.18 support is dropped. * (Python) MqttIO is now supported in Python via cross-language ([#21060](https://github.com/apache/beam/issues/21060)). From 301833f53850086c37083c300bc6a17780dd240d Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 25 Jun 2026 04:42:51 -0400 Subject: [PATCH 468/490] Update beam-master container --- sdks/python/apache_beam/runners/dataflow/internal/names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/runners/dataflow/internal/names.py b/sdks/python/apache_beam/runners/dataflow/internal/names.py index 674658b8af71..e4a52501d053 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/names.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/names.py @@ -35,6 +35,6 @@ # Update this tag whenever there is a change that # requires changes to SDK harness container or SDK harness launcher. -BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260615' +BEAM_DEV_SDK_CONTAINER_TAG = 'beam-master-20260624' DATAFLOW_CONTAINER_IMAGE_REPOSITORY = 'gcr.io/cloud-dataflow/v1beta3' From ea687cb0b4542d5a4790823c75aa4c1992eab726 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 25 Jun 2026 05:03:09 -0400 Subject: [PATCH 469/490] Trigger failed postcommit tests. --- .github/trigger_files/beam_PostCommit_Python.json | 2 +- .../beam_PostCommit_Python_Examples_Dataflow.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 11064375d62e..c03ecf71f04d 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "37345", - "modification": 49 + "modification": 50 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Examples_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_Examples_Dataflow.json index b8513bdfc7b7..d7118310af8d 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Examples_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_Examples_Dataflow.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "37360", - "modification": 1 -} \ No newline at end of file + "modification": 2 +} From 684ec1548622516067d2e48118f46b1dd8041218 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 24 Jun 2026 15:55:31 -0400 Subject: [PATCH 470/490] Fix race condition in DirectRunner executor shutdown Previously, in ExecutorServiceParallelExecutor, if an exception occurred during registry cleanup (such as a timeout inside DoFn teardown), the pipeline state was transitioned to terminal before the exception was queued in `visibleUpdates`. This introduced a race condition where `waitUntilFinish()` could detect the terminal state and exit successfully before the exception was offered to the updates queue, swallowing the exception. This caused tests like `CallTest.givenTeardownTimeout_throwsError` to fail since they expected the pipeline to throw an exception. This change swaps the order so that the exception is posted to `visibleUpdates` before updating the pipeline state to terminal, ensuring the exception is always propagated. --- .../runners/direct/ExecutorServiceParallelExecutor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java index 95cadef7afdb..43c4356ee3b2 100644 --- a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java +++ b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java @@ -348,9 +348,9 @@ private void shutdownIfNecessary(State newState) { } catch (final Exception e) { errors.add(e); } - pipelineState.compareAndSet(State.RUNNING, newState); // ensure we hit a terminal node + IllegalStateException exception = null; if (!errors.isEmpty()) { - final IllegalStateException exception = + exception = new IllegalStateException( "Error" + (errors.size() == 1 ? "" : "s") @@ -359,6 +359,9 @@ private void shutdownIfNecessary(State newState) { .map(Exception::getMessage) .collect(Collectors.joining("\n- ", "- ", ""))); visibleUpdates.failed(exception); + } + pipelineState.compareAndSet(State.RUNNING, newState); // ensure we hit a terminal node + if (exception != null) { throw exception; } } From d1820379349f951f1012b6cf28603ce14964bc7e Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Wed, 24 Jun 2026 17:34:17 -0400 Subject: [PATCH 471/490] Apply suggested fix --- .../ExecutorServiceParallelExecutor.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java index 43c4356ee3b2..37cff06a267f 100644 --- a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java +++ b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java @@ -349,18 +349,21 @@ private void shutdownIfNecessary(State newState) { errors.add(e); } IllegalStateException exception = null; - if (!errors.isEmpty()) { - exception = - new IllegalStateException( - "Error" - + (errors.size() == 1 ? "" : "s") - + " during executor shutdown:\n" - + errors.stream() - .map(Exception::getMessage) - .collect(Collectors.joining("\n- ", "- ", ""))); - visibleUpdates.failed(exception); + try { + if (!errors.isEmpty()) { + exception = + new IllegalStateException( + "Error" + + (errors.size() == 1 ? "" : "s") + + " occurred during pipeline execution:\\n" + + errors.stream() + .map(e -> e.getMessage() == null ? e.getClass().getName() : e.getMessage()) + .collect(Collectors.joining("\\n- ", "- ", ""))); + visibleUpdates.failed(exception); + } + } finally { + pipelineState.compareAndSet(State.RUNNING, newState); // ensure we hit a terminal node } - pipelineState.compareAndSet(State.RUNNING, newState); // ensure we hit a terminal node if (exception != null) { throw exception; } From 400c0bf94df02af64016f16b8d9336c131c3a17b Mon Sep 17 00:00:00 2001 From: Utkarsh Parekh <145644114+utkarshparekh@users.noreply.github.com> Date: Thu, 25 Jun 2026 05:07:00 -0700 Subject: [PATCH 472/490] Fix JsonToRow swallowing downstream errors when runners fuse transforms. (#39098) * Fix JsonToRow swallowing downstream errors when runners fuse transforms. Separate JSON parsing from MultiOutputReceiver output in ParseWithError so exceptions from fused downstream consumers are not misreported as parse failures. Fixes #20935. * Address review: use MapElements in JsonToRow regression test. Avoid anonymous DoFn capturing the non-serializable test instance so the test is safe on runners that enforce DoFn serialization. * Fix JsonToRow regression test for runner integration suites. Use a static DoFn to avoid serialization issues, set row schema on the downstream transform, and tag the test with ValidatesRunner so it can run via Dataflow validatesRunner tasks. Co-authored-by: Cursor * Merge apache/beam master and fix JsonToRowTest for CI. Rebase onto current master (1141 commits behind) and remove ValidatesRunner category so the regression test only runs on DirectRunner needsRunnerTests. Suppress UnusedVariable on ThrowingDownstreamDoFn for Error Prone. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../apache/beam/sdk/transforms/JsonToRow.java | 8 +++--- .../beam/sdk/transforms/JsonToRowTest.java | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/JsonToRow.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/JsonToRow.java index 69667929dad3..9845af44a82e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/JsonToRow.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/JsonToRow.java @@ -309,12 +309,10 @@ public static ParseWithError create(JsonToRowWithErrFn jsonToRowWithErrFn) { @ProcessElement public void processElement(@Element String element, MultiOutputReceiver output) { + final Row parsedRow; try { - - output.get(PARSED_LINE).output(jsonToRow(objectMapper(), element)); - + parsedRow = jsonToRow(objectMapper(), element); } catch (Exception ex) { - if (getJsonToRowWithErrFn().getExtendedErrorInfo()) { output .get(PARSE_ERROR) @@ -328,7 +326,9 @@ public void processElement(@Element String element, MultiOutputReceiver output) .get(PARSE_ERROR) .output(Row.withSchema(ERROR_ROW_SCHEMA).addValue(element).build()); } + return; } + output.get(PARSED_LINE).output(parsedRow); } private ObjectMapper objectMapper() { diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/JsonToRowTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/JsonToRowTest.java index 490cb68ab9e4..79918930e098 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/JsonToRowTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/JsonToRowTest.java @@ -274,6 +274,24 @@ public void testParsesErrorWithErrorMsgWithRequireNullDeadLetter() throws Except pipeline.run(); } + @Test + @Category(NeedsRunner.class) + public void testDownstreamExceptionIsNotReportedAsParseError() { + PCollection jsonPersons = pipeline.apply("jsonPersons", Create.of(JSON_PERSON.get(0))); + + ParseResult results = jsonPersons.apply(JsonToRow.withExceptionReporting(PERSON_SCHEMA)); + + results + .getResults() + .apply("throwingDownstream", ParDo.of(new ThrowingDownstreamDoFn())) + .setRowSchema(PERSON_SCHEMA); + + thrown.expect(RuntimeException.class); + thrown.expectMessage("downstream failure"); + + pipeline.run(); + } + @Test @Category(NeedsRunner.class) public void testParsesErrorWithErrorMsgRowsDeadLetterWithCustomFieldNames() throws Exception { @@ -330,4 +348,12 @@ private static String jsonPerson(String name, String height) { private static Row row(Schema schema, Object... values) { return Row.withSchema(schema).addValues(values).build(); } + + private static class ThrowingDownstreamDoFn extends DoFn { + @ProcessElement + @SuppressWarnings("UnusedVariable") + public void processElement(ProcessContext context) { + throw new RuntimeException("downstream failure"); + } + } } From 3a8a82174fcc78301aab3cdf8a67b44985888349 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 25 Jun 2026 09:21:30 -0400 Subject: [PATCH 473/490] Enable playground tests triggered on cron schedule (#39069) --- .github/workflows/beam_Playground_Precommit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index da253cc11bfd..056118e7170d 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -34,6 +34,7 @@ jobs: if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Playground PreCommit' name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] From f2ee01b78338bb7fee2068966de6ca4016ee8f8a Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:45:19 -0400 Subject: [PATCH 474/490] [Iceberg CDC] Import BaseIncrementalChangelogScan (#38823) --- .../iceberg/BaseIncrementalChangelogScan.java | 1014 +++++++++++++++++ .../java/org/apache/iceberg/package-info.java | 20 + .../iceberg/catalog/IcebergCatalogBaseIT.java | 2 +- 3 files changed, 1035 insertions(+), 1 deletion(-) create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java new file mode 100644 index 000000000000..6b12e16690ba --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java @@ -0,0 +1,1014 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.iceberg; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Comparator; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.FluentIterable; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; +import org.apache.iceberg.ManifestGroup.CreateTasksFunction; +import org.apache.iceberg.ManifestGroup.TaskContext; +import org.apache.iceberg.expressions.Evaluator; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.expressions.ManifestEvaluator; +import org.apache.iceberg.expressions.Projections; +import org.apache.iceberg.expressions.ResidualEvaluator; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.util.ContentFileUtil; +import org.apache.iceberg.util.Pair; +import org.apache.iceberg.util.PartitionMap; +import org.apache.iceberg.util.PartitionSet; +import org.apache.iceberg.util.SnapshotUtil; +import org.apache.iceberg.util.SortedMerge; +import org.apache.iceberg.util.TableScanUtil; +import org.apache.iceberg.util.Tasks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Copied over from Iceberg PR #14264. + */ +@SuppressWarnings("nullness") +public class BaseIncrementalChangelogScan + extends BaseIncrementalScan< + IncrementalChangelogScan, ChangelogScanTask, ScanTaskGroup> + implements IncrementalChangelogScan { + private static final DeleteFileIndex EMPTY = createEmptyInstance(); + + private static DeleteFileIndex createEmptyInstance() { + try { + var constructor = + DeleteFileIndex.class.getDeclaredConstructor( + DeleteFileIndex.EqualityDeletes.class, + PartitionMap.class, + PartitionMap.class, + Map.class, + Map.class); + constructor.setAccessible(true); + return constructor.newInstance(null, null, null, null, null); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize EMPTY DeleteFileIndex", e); + } + } + + private static final Logger LOG = LoggerFactory.getLogger(BaseIncrementalChangelogScan.class); + + public BaseIncrementalChangelogScan(Table table) { + this(table, table.schema(), TableScanContext.empty()); + } + + private BaseIncrementalChangelogScan(Table table, Schema schema, TableScanContext context) { + super(table, schema, context); + } + + @Override + protected IncrementalChangelogScan newRefinedScan( + Table newTable, Schema newSchema, TableScanContext newContext) { + return new BaseIncrementalChangelogScan(newTable, newSchema, newContext); + } + + // Private fields to track build call count and cache (accessed via package-private methods for + // testing) + private int existingDeleteIndexBuildCallCount = 0; + // Cache for the built index (null if not built yet) + private DeleteFileIndex cachedExistingDeleteIndex = null; + + @Override + protected CloseableIterable doPlanFiles( + Long fromSnapshotIdExclusive, long toSnapshotIdInclusive) { + + Deque changelogSnapshots = + orderedChangelogSnapshots(fromSnapshotIdExclusive, toSnapshotIdInclusive); + + if (changelogSnapshots.isEmpty()) { + return CloseableIterable.empty(); + } + + Set changelogSnapshotIds = toSnapshotIds(changelogSnapshots); + + Set newDataManifests = + FluentIterable.from(changelogSnapshots) + .transformAndConcat(snapshot -> snapshot.dataManifests(table().io())) + .filter(manifest -> changelogSnapshotIds.contains(manifest.snapshotId())) + .toSet(); + + // Build per-snapshot delete file indexes for added deletes + Map addedDeletesBySnapshot = buildAddedDeleteIndexes(changelogSnapshots); + + // Check if existing delete index is needed for equality deletes + boolean hasEqualityDeletes = + addedDeletesBySnapshot.values().stream() + .anyMatch(index -> !index.isEmpty() && index.hasEqualityDeletes()); + + // Build existing index early if needed for equality deletes, otherwise use lazy initialization + DeleteFileIndex existingDeleteIndex = + hasEqualityDeletes ? buildExistingDeleteIndexTracked(fromSnapshotIdExclusive) : EMPTY; + + ManifestGroup manifestGroup = + new ManifestGroup(table().io(), newDataManifests, ImmutableList.of()) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .select(scanColumns()) + .filterData(filter()) + .filterManifestEntries(entry -> changelogSnapshotIds.contains(entry.snapshotId())) + .ignoreExisting() + .columnsToKeepStats(columnsToKeepStats()); + + if (shouldIgnoreResiduals()) { + manifestGroup = manifestGroup.ignoreResiduals(); + } + + if (newDataManifests.size() > 1 && shouldPlanWithExecutor()) { + manifestGroup = manifestGroup.planWith(planExecutor()); + } + + // Create a supplier that reuses already-built index or builds lazily when first DELETED entry + // is encountered + Supplier existingDeleteIndexSupplier = + () -> { + if (cachedExistingDeleteIndex != null) { + return cachedExistingDeleteIndex; + } + return buildExistingDeleteIndexTracked(fromSnapshotIdExclusive); + }; + + // Plan data file tasks (ADDED and DELETED) + Map> cumulativeDeletesMap = + buildCumulativeDeletesBySnapshot(changelogSnapshots, addedDeletesBySnapshot); + + CloseableIterable dataFileTasks = + manifestGroup.plan( + new CreateDataFileChangeTasks( + changelogSnapshots, + existingDeleteIndexSupplier, + addedDeletesBySnapshot, + cumulativeDeletesMap, + table().specs(), + isCaseSensitive())); + + // Find EXISTING data files affected by newly added delete files and create tasks for them + CloseableIterable deletedRowsTasks = + planDeletedRowsTasks( + changelogSnapshots, existingDeleteIndex, addedDeletesBySnapshot, changelogSnapshotIds); + + // Merge tasks from both iterables in order by changeOrdinal + Comparator byOrdinal = + Comparator.comparing(ChangelogScanTask::changeOrdinal) + .thenComparing(ChangelogScanTask::commitSnapshotId); + + return new SortedMerge<>(byOrdinal, ImmutableList.of(dataFileTasks, deletedRowsTasks)); + } + + @Override + public CloseableIterable> planTasks() { + return TableScanUtil.planTaskGroups( + planFiles(), targetSplitSize(), splitLookback(), splitOpenFileCost()); + } + + // builds a collection of changelog snapshots (oldest to newest) + // the order of the snapshots is important as it is used to determine change ordinals + private Deque orderedChangelogSnapshots(Long fromIdExcl, long toIdIncl) { + Deque changelogSnapshots = new ArrayDeque<>(); + + for (Snapshot snapshot : SnapshotUtil.ancestorsBetween(table(), toIdIncl, fromIdExcl)) { + if (!snapshot.operation().equals(DataOperations.REPLACE)) { + changelogSnapshots.addFirst(snapshot); + } + } + + return changelogSnapshots; + } + + private Set toSnapshotIds(Collection snapshots) { + return snapshots.stream().map(Snapshot::snapshotId).collect(Collectors.toSet()); + } + + private static Map computeSnapshotOrdinals(Deque snapshots) { + Map snapshotOrdinals = Maps.newHashMap(); + + int ordinal = 0; + + for (Snapshot snapshot : snapshots) { + snapshotOrdinals.put(snapshot.snapshotId(), ordinal++); + } + + return snapshotOrdinals; + } + + /** + * Builds a delete file index for existing deletes that were present before the start snapshot. + * These deletes should be applied to data files but should not generate DELETE changelog rows. + * Uses manifest pruning and caching to optimize performance. + */ + private DeleteFileIndex buildExistingDeleteIndex(Long fromSnapshotIdExclusive) { + if (fromSnapshotIdExclusive == null) { + return EMPTY; + } + Snapshot fromSnapshot = table().snapshot(fromSnapshotIdExclusive); + Preconditions.checkState( + fromSnapshot != null, "Cannot find starting snapshot: %s", fromSnapshotIdExclusive); + + List existingDeleteManifests = fromSnapshot.deleteManifests(table().io()); + if (existingDeleteManifests.isEmpty()) { + return EMPTY; + } + + // Prune manifests based on partition filter to avoid processing irrelevant manifests + List prunedManifests = pruneManifestsByPartition(existingDeleteManifests); + if (prunedManifests.isEmpty()) { + return EMPTY; + } + + // Load delete files from manifests + Iterable deleteFiles = loadDeleteFiles(prunedManifests, null); + + return DeleteFileIndex.builderFor(deleteFiles) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .build(); + } + + /** + * Wrapper method that tracks build calls and caches the result for reuse. This ensures we only + * build the index once even if called from multiple places. + */ + private DeleteFileIndex buildExistingDeleteIndexTracked(Long fromSnapshotIdExclusive) { + if (cachedExistingDeleteIndex != null) { + return cachedExistingDeleteIndex; + } + existingDeleteIndexBuildCallCount++; + cachedExistingDeleteIndex = buildExistingDeleteIndex(fromSnapshotIdExclusive); + return cachedExistingDeleteIndex; + } + + // Visible for testing + int getExistingDeleteIndexBuildCallCount() { + return existingDeleteIndexBuildCallCount; + } + + // Visible for testing + boolean wasExistingDeleteIndexBuilt() { + return existingDeleteIndexBuildCallCount > 0; + } + + /** + * Builds per-snapshot delete file indexes for newly added delete files in each changelog + * snapshot. These deletes should generate DELETE changelog rows. Uses caching to avoid re-parsing + * manifests. + */ + private Map buildAddedDeleteIndexes(Deque changelogSnapshots) { + Map addedDeletesBySnapshot = Maps.newConcurrentMap(); + Tasks.foreach(changelogSnapshots) + .retry(3) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .onFailure( + (snapshot, exc) -> + LOG.warn( + "Failed to build delete index for snapshot {}", snapshot.snapshotId(), exc)) + .run( + snapshot -> { + List snapshotDeleteManifests = snapshot.deleteManifests(table().io()); + if (snapshotDeleteManifests.isEmpty()) { + addedDeletesBySnapshot.put(snapshot.snapshotId(), EMPTY); + return; + } + + // Filter to only include delete files added in this snapshot + List addedDeleteManifests = + snapshotDeleteManifests.stream() + .filter(manifest -> manifest.snapshotId().equals(snapshot.snapshotId())) + .collect(Collectors.toUnmodifiableList()); + + if (addedDeleteManifests.isEmpty()) { + addedDeletesBySnapshot.put(snapshot.snapshotId(), EMPTY); + } else { + // Load delete files from manifests + Iterable deleteFiles = + loadDeleteFiles(addedDeleteManifests, snapshot.snapshotId()); + + DeleteFileIndex index = + DeleteFileIndex.builderFor(deleteFiles) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .build(); + addedDeletesBySnapshot.put(snapshot.snapshotId(), index); + } + }); + return addedDeletesBySnapshot; + } + + /** + * Plans tasks for EXISTING data files that are affected by newly added delete files. These files + * were not added or deleted in the changelog snapshot range, but have new delete files applied to + * them. + */ + private CloseableIterable planDeletedRowsTasks( + Deque changelogSnapshots, + DeleteFileIndex existingDeleteIndex, + Map addedDeletesBySnapshot, + Set changelogSnapshotIds) { + + Map snapshotOrdinals = computeSnapshotOrdinals(changelogSnapshots); + List tasks = Lists.newArrayList(); + + // Build a map of file statuses and collect affected partitions for each snapshot + Pair>, PartitionSet> fileStatusAndPartitions = + buildFileStatusBySnapshot(changelogSnapshots, changelogSnapshotIds); + Map> fileStatusBySnapshot = + fileStatusAndPartitions.first(); + PartitionSet affectedPartitions = fileStatusAndPartitions.second(); + + // Accumulate actual DeleteFile entries chronologically + List accumulatedDeletes = Lists.newArrayList(); + + // Start with deletes from before the changelog range + if (!existingDeleteIndex.isEmpty()) { + for (DeleteFile df : existingDeleteIndex.referencedDeleteFiles()) { + accumulatedDeletes.add(df); + } + } + + for (Snapshot snapshot : changelogSnapshots) { + DeleteFileIndex addedDeleteIndex = addedDeletesBySnapshot.get(snapshot.snapshotId()); + if (addedDeleteIndex.isEmpty()) { + continue; + } + + // Collect partitions of newly added delete files for pruning (important for the current + // snapshot) + for (DeleteFile df : addedDeleteIndex.referencedDeleteFiles()) { + affectedPartitions.add(df.specId(), df.partition()); + } + + DeleteFileIndex cumulativeDeleteIndex = + buildDeleteIndex(accumulatedDeletes, affectedPartitions); + + // Process data files for this snapshot + // Use a local set per snapshot to track processed files + Set alreadyProcessedPaths = Sets.newHashSet(); + processSnapshotForDeletedRowsTasks( + snapshot, + addedDeleteIndex, + cumulativeDeleteIndex, + fileStatusBySnapshot.get(snapshot.snapshotId()), + alreadyProcessedPaths, + snapshotOrdinals, + affectedPartitions, + tasks); + + // Accumulate this snapshot's added deletes for subsequent snapshots + for (DeleteFile df : addedDeleteIndex.referencedDeleteFiles()) { + accumulatedDeletes.add(df); + } + } + + return CloseableIterable.withNoopClose(tasks); + } + + /** + * Builds a map of file statuses for each snapshot, tracking which files were added or deleted in + * each snapshot. + */ + private Pair>, PartitionSet> + buildFileStatusBySnapshot( + Deque changelogSnapshots, Set changelogSnapshotIds) { + + Map> fileStatusBySnapshot = Maps.newConcurrentMap(); + java.util.Queue localPartitionsQueue = + new java.util.concurrent.ConcurrentLinkedQueue<>(); + + Tasks.foreach(changelogSnapshots) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .run( + snapshot -> { + Map fileStatuses = Maps.newHashMap(); + PartitionSet localAffected = PartitionSet.create(table().specs()); + + List changedDataManifests = + FluentIterable.from(snapshot.dataManifests(table().io())) + .filter(manifest -> manifest.snapshotId().equals(snapshot.snapshotId())) + .toList(); + + if (!changedDataManifests.isEmpty()) { + ManifestGroup changedGroup = + new ManifestGroup(table().io(), changedDataManifests, ImmutableList.of()) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .select(scanColumns()) + .filterData(filter()) + .ignoreExisting() + .columnsToKeepStats(columnsToKeepStats()); + + try (CloseableIterable> entries = changedGroup.entries()) { + for (ManifestEntry entry : entries) { + if (changelogSnapshotIds.contains(entry.snapshotId())) { + fileStatuses.put(entry.file().location(), entry.status()); + localAffected.add(entry.file().specId(), entry.file().partition()); + } + } + } catch (Exception e) { + throw new RuntimeException( + "Failed to collect file statuses for snapshot " + snapshot.snapshotId(), e); + } + } + + fileStatusBySnapshot.put(snapshot.snapshotId(), fileStatuses); + localPartitionsQueue.add(localAffected); + }); + + PartitionSet globalAffected = PartitionSet.create(table().specs()); + for (PartitionSet local : localPartitionsQueue) { + globalAffected.addAll(local); + } + + return Pair.of(fileStatusBySnapshot, globalAffected); + } + + private List pruneManifestsByAffectedPartitions( + List manifests, PartitionSet affectedPartitions) { + if (affectedPartitions.isEmpty()) { + return manifests; + } + + Expression affectedExpr = buildAffectedPartitionExpression(affectedPartitions); + if (affectedExpr == Expressions.alwaysFalse()) { + return manifests; + } + + List pruned = Lists.newArrayList(); + for (ManifestFile manifest : manifests) { + PartitionSpec spec = table().specs().get(manifest.partitionSpecId()); + if (spec == null || spec.isUnpartitioned()) { + pruned.add(manifest); + } else if (manifestOverlapsFilter(manifest, spec, affectedExpr)) { + pruned.add(manifest); + } + } + return pruned; + } + + private Expression buildAffectedPartitionExpression(PartitionSet affectedPartitions) { + Expression combined = null; + + for (Pair pair : affectedPartitions) { + int specId = pair.first(); + StructLike partition = pair.second(); + PartitionSpec spec = table().specs().get(specId); + if (spec == null) { + continue; + } else if (spec.isUnpartitioned()) { + return Expressions.alwaysTrue(); // FALLBACK: Global delete exists, include ALL manifests! + } + + Expression specExpr = null; + for (int i = 0; i < spec.fields().size(); i++) { + org.apache.iceberg.PartitionField field = spec.fields().get(i); + Object value = partition.get(i, Object.class); + if (value != null) { + String columnName = table().schema().findColumnName(field.sourceId()); + if (columnName != null) { + Expression equalExpr = Expressions.equal(columnName, value); + specExpr = (specExpr == null) ? equalExpr : Expressions.and(specExpr, equalExpr); + } + } + } + + if (specExpr != null) { + combined = (combined == null) ? specExpr : Expressions.or(combined, specExpr); + } + } + + return combined != null ? combined : Expressions.alwaysFalse(); + } + + /** + * Builds a map of snapshot ID -> all delete files that were added in the scan range up to that + * snapshot, PRUNING files that were removed in the middle. + */ + private Map> buildCumulativeDeletesBySnapshot( + Deque snapshots, Map addedDeletesBySnapshot) { + Map> result = Maps.newHashMap(); + List accumulatedDeletes = Lists.newArrayList(); + + for (Snapshot snapshot : snapshots) { + // Save state first, so that this snapshot's tasks can use any deletes active up to this point + result.put(snapshot.snapshotId(), Lists.newArrayList(accumulatedDeletes)); + + // Check for removed deletes and prune from accumulatedDeletes for FUTURE snapshots + List changedDeletes = + FluentIterable.from(snapshot.deleteManifests(table().io())) + .filter(manifest -> manifest.snapshotId().equals(snapshot.snapshotId())) + .toList(); + + if (!changedDeletes.isEmpty()) { + Iterable removedDeletes = + loadRemovedDeleteFiles(changedDeletes, snapshot.snapshotId()); + Set removedPaths = Sets.newHashSet(); + for (DeleteFile rdf : removedDeletes) { + removedPaths.add(rdf.location()); + } + accumulatedDeletes.removeIf(df -> removedPaths.contains(df.location())); + } + + // Add new deletes for FUTURE snapshots + DeleteFileIndex addedDeleteIndex = addedDeletesBySnapshot.get(snapshot.snapshotId()); + if (addedDeleteIndex != null && !addedDeleteIndex.isEmpty()) { + for (DeleteFile df : addedDeleteIndex.referencedDeleteFiles()) { + accumulatedDeletes.add(df); + } + } + } + + return result; + } + + /** + * Builds a delete index from the accumulated list of delete files, pruning by affected + * partitions. + */ + private DeleteFileIndex buildDeleteIndex( + List accumulatedDeletes, PartitionSet affectedPartitions) { + if (accumulatedDeletes.isEmpty()) { + return EMPTY; + } + + List filteredDeletes = accumulatedDeletes; + if (!affectedPartitions.isEmpty()) { + filteredDeletes = Lists.newArrayList(); + for (DeleteFile df : accumulatedDeletes) { + PartitionSpec spec = table().specs().get(df.specId()); + if (spec == null || spec.isUnpartitioned()) { + filteredDeletes.add(df); // Always include unpartitioned deletes + } else if (affectedPartitions.contains(df.specId(), df.partition())) { + filteredDeletes.add(df); + } + } + } + + return DeleteFileIndex.builderFor(filteredDeletes) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .build(); + } + + /** + * Processes data files for a snapshot to create DeletedRowsScanTask for existing files affected + * by new delete files. + */ + private void processSnapshotForDeletedRowsTasks( + Snapshot snapshot, + DeleteFileIndex addedDeleteIndex, + DeleteFileIndex cumulativeDeleteIndex, + Map currentSnapshotFiles, + Set alreadyProcessedPaths, + Map snapshotOrdinals, + PartitionSet affectedPartitions, + List tasks) { + + // Get all data files that exist in this snapshot, pruned by affected partitions + List allDataManifests = snapshot.dataManifests(table().io()); + List prunedManifests = + pruneManifestsByAffectedPartitions(allDataManifests, affectedPartitions); + + ManifestGroup allDataGroup = + new ManifestGroup(table().io(), prunedManifests, ImmutableList.of()) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .select(scanColumns()) + .filterData(filter()) + .ignoreDeleted() + .columnsToKeepStats(columnsToKeepStats()); + + if (shouldIgnoreResiduals()) { + allDataGroup = allDataGroup.ignoreResiduals(); + } + + String schemaString = SchemaParser.toJson(schema()); + + // Cache per specId - same for all files with same specId + Map specStringCache = Maps.newHashMap(); + Map residualCache = Maps.newHashMap(); + Expression residualFilter = shouldIgnoreResiduals() ? Expressions.alwaysTrue() : filter(); + + try (CloseableIterable> entries = allDataGroup.entries()) { + for (ManifestEntry entry : entries) { + DataFile dataFile = entry.file(); + String filePath = dataFile.location(); + + // Skip if this file was ADDED or DELETED in this snapshot + // (those are handled by CreateDataFileChangeTasks) + if (currentSnapshotFiles.containsKey(filePath)) { + continue; + } + + // Skip if we already created a task for this file in this snapshot + // Note: alreadyProcessedPaths is local to this snapshot's processing + if (alreadyProcessedPaths.contains(filePath)) { + continue; + } + + // Check if this data file is affected by newly added delete files + DeleteFile[] addedDeletes = addedDeleteIndex.forEntry(entry); + if (addedDeletes.length == 0) { + continue; + } + + // This data file was EXISTING but has new delete files applied + // Get existing deletes from before this snapshot (cumulative) + DeleteFile[] existingDeletes = + cumulativeDeleteIndex.isEmpty() + ? new DeleteFile[0] + : cumulativeDeleteIndex.forEntry(entry); + + // Create a DeletedRowsScanTask + int changeOrdinal = snapshotOrdinals.get(snapshot.snapshotId()); + + // Use cached values (calculate once per specId) + int specId = dataFile.specId(); + String specString = + specStringCache.computeIfAbsent( + specId, id -> PartitionSpecParser.toJson(table().specs().get(id))); + ResidualEvaluator residuals = + residualCache.computeIfAbsent( + specId, + id -> { + PartitionSpec spec = table().specs().get(id); + return ResidualEvaluator.of(spec, residualFilter, isCaseSensitive()); + }); + + tasks.add( + new BaseDeletedRowsScanTask( + changeOrdinal, + snapshot.snapshotId(), + dataFile.copy(shouldKeepStats()), + addedDeletes, + existingDeletes, + schemaString, + specString, + residuals)); + + // Mark this file as processed for this snapshot + alreadyProcessedPaths.add(filePath); + } + } catch (Exception e) { + throw new RuntimeException("Failed to plan deleted rows tasks", e); + } + } + + private boolean shouldKeepStats() { + Set columns = columnsToKeepStats(); + return columns != null && !columns.isEmpty(); + } + + /** + * Loads delete files from manifests by parsing each manifest. + * + * @param manifests the delete manifests to load + * @return list of delete files + */ + private Iterable loadDeleteFiles( + List manifests, Long targetSnapshotId) { + Queue allDeleteFiles = new ConcurrentLinkedQueue<>(); + + Tasks.foreach(manifests) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .run( + manifest -> { + List deleteFiles = + loadDeleteFilesFromManifest(manifest, targetSnapshotId); + allDeleteFiles.addAll(deleteFiles); + }); + + return allDeleteFiles; + } + + private Iterable loadRemovedDeleteFiles( + List manifests, Long targetSnapshotId) { + Queue allDeleteFiles = new ConcurrentLinkedQueue<>(); + + Tasks.foreach(manifests) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .run( + manifest -> { + List deleteFiles = + loadRemovedDeleteFilesFromManifest(manifest, targetSnapshotId); + allDeleteFiles.addAll(deleteFiles); + }); + + return allDeleteFiles; + } + + private List loadRemovedDeleteFilesFromManifest( + ManifestFile manifest, Long targetSnapshotId) { + List deleteFiles = Lists.newArrayList(); + + try (ManifestReader reader = + ManifestFiles.readDeleteManifest(manifest, table().io(), table().specs())) { + for (ManifestEntry entry : reader.entries()) { + if (entry.status() == ManifestEntry.Status.DELETED + && entry.snapshotId().equals(targetSnapshotId)) { + DeleteFile file = entry.file(); + + if (!partitionMatchesFilter(file)) { + continue; + } + + Set columns = + file.content() == FileContent.POSITION_DELETES + ? Set.of(MetadataColumns.DELETE_FILE_PATH.fieldId()) + : Set.copyOf(file.equalityFieldIds()); + deleteFiles.add(ContentFileUtil.copy(file, true, columns)); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to read delete manifest: " + manifest.path(), e); + } + + return deleteFiles; + } + + /** + * Prunes delete manifests based on partition filter to avoid processing irrelevant manifests. + * This significantly improves performance when only a subset of partitions are relevant to the + * scan. + * + * @param manifests all delete manifests to consider + * @return list of manifests that might contain relevant delete files + */ + private List pruneManifestsByPartition(List manifests) { + Expression currentFilter = filter(); + + // If there's no filter, return all manifests + if (currentFilter == null || currentFilter.equals(Expressions.alwaysTrue())) { + return manifests; + } + + List prunedManifests = Lists.newArrayList(); + + for (ManifestFile manifest : manifests) { + PartitionSpec spec = table().specs().get(manifest.partitionSpecId()); + if (spec == null || spec.isUnpartitioned()) { + // Include unpartitioned manifests + prunedManifests.add(manifest); + } else if (manifestOverlapsFilter(manifest, spec, currentFilter)) { + // Check if manifest partition range overlaps with filter + prunedManifests.add(manifest); + } + } + + return prunedManifests; + } + + /** + * Checks if a manifest's partition range overlaps with the given filter. + * + * @param manifest the manifest to check + * @param spec the partition spec for the manifest + * @param filter the scan filter + * @return true if the manifest might contain matching partitions, false otherwise + */ + private boolean manifestOverlapsFilter( + ManifestFile manifest, PartitionSpec spec, Expression filter) { + try { + // Use inclusive projection to transform row filter to partition filter + Expression partitionFilter = Projections.inclusive(spec, isCaseSensitive()).project(filter); + + // Create evaluator for the partition filter + ManifestEvaluator evaluator = + ManifestEvaluator.forPartitionFilter(partitionFilter, spec, isCaseSensitive()); + + // Check if manifest could contain matching partitions + return evaluator.eval(manifest); + } catch (Exception e) { + // If evaluation fails, be conservative and include the manifest + return true; + } + } + + /** + * Checks if a delete file's partition overlaps with the current scan filter. This enables + * partition pruning to reduce memory footprint and planning overhead by skipping delete files + * that cannot possibly match any rows in the scan. + * + * @param file the delete file to check + * @return true if the delete file's partition might contain matching rows, false otherwise + */ + private boolean partitionMatchesFilter(DeleteFile file) { + // If there's no filter, all partitions match + Expression currentFilter = filter(); + if (currentFilter == null || currentFilter.equals(Expressions.alwaysTrue())) { + return true; + } + + // Get the partition spec for this delete file + PartitionSpec spec = table().specs().get(file.specId()); + if (spec == null || spec.isUnpartitioned()) { + // If spec not found or table is unpartitioned, be conservative and include the file + return true; + } + + try { + // Project the row filter to partition space using inclusive projection + // This transforms expressions on source columns to expressions on partition columns + Expression partitionFilter = + Projections.inclusive(spec, isCaseSensitive()).project(currentFilter); + + // Evaluate the projected filter against the delete file's partition + Evaluator evaluator = new Evaluator(spec.partitionType(), partitionFilter, isCaseSensitive()); + return evaluator.eval(file.partition()); + } catch (Exception e) { + // If evaluation fails, be conservative and include the file + return true; + } + } + + /** + * Loads delete files from a single manifest, parsing the manifest entries. + * + * @param manifest the delete manifest to load + * @return list of delete files from this manifest + */ + private List loadDeleteFilesFromManifest( + ManifestFile manifest, Long targetSnapshotId) { + List deleteFiles = Lists.newArrayList(); + + try (ManifestReader reader = + ManifestFiles.readDeleteManifest(manifest, table().io(), table().specs())) { + for (ManifestEntry entry : reader.entries()) { + if (entry.status() != ManifestEntry.Status.DELETED + && (targetSnapshotId == null || entry.snapshotId().equals(targetSnapshotId))) { + // Only include live delete files, copy with minimal stats to save memory + DeleteFile file = entry.file(); + + // Apply partition pruning - skip delete files that cannot match the scan filter + if (!partitionMatchesFilter(file)) { + continue; + } + + Set columns = + file.content() == FileContent.POSITION_DELETES + ? Set.of(MetadataColumns.DELETE_FILE_PATH.fieldId()) + : Set.copyOf(file.equalityFieldIds()); + deleteFiles.add(ContentFileUtil.copy(file, true, columns)); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to read delete manifest: " + manifest.path(), e); + } + + return deleteFiles; + } + + private static class CreateDataFileChangeTasks implements CreateTasksFunction { + private static final DeleteFile[] NO_DELETES = new DeleteFile[0]; + + private final Map snapshotOrdinals; + private final Supplier existingDeleteIndexSupplier; + private final Map addedDeletesBySnapshot; + private final Map> cumulativeDeletesMap; + private final Map specsById; + private final boolean caseSensitive; + + CreateDataFileChangeTasks( + Deque snapshots, + Supplier existingDeleteIndexSupplier, + Map addedDeletesBySnapshot, + Map> cumulativeDeletesMap, + Map specsById, + boolean caseSensitive) { + this.snapshotOrdinals = computeSnapshotOrdinals(snapshots); + this.existingDeleteIndexSupplier = existingDeleteIndexSupplier; + this.addedDeletesBySnapshot = addedDeletesBySnapshot; + this.cumulativeDeletesMap = cumulativeDeletesMap; + this.specsById = specsById; + this.caseSensitive = caseSensitive; + } + + @Override + public CloseableIterable apply( + CloseableIterable> entries, TaskContext context) { + + return CloseableIterable.transform( + entries, + entry -> { + long commitSnapshotId = entry.snapshotId(); + int changeOrdinal = snapshotOrdinals.get(commitSnapshotId); + DataFile dataFile = entry.file().copy(context.shouldKeepStats()); + + switch (entry.status()) { + case ADDED: + // For ADDED data files, attach delete files added in this snapshot + DeleteFile[] addedFileDeletes = getDeletesForAddedFile(entry, commitSnapshotId); + return new BaseAddedRowsScanTask( + changeOrdinal, + commitSnapshotId, + dataFile, + addedFileDeletes, + context.schemaAsString(), + context.specAsString(), + context.residuals()); + + case DELETED: + // For DELETED data files, attach ALL deletes that were present up to deletion + // This includes existing deletes AND deletes added in the scan range + DeleteFile[] deletedFileDeletes = getDeletesForDeletedFile(entry, commitSnapshotId); + return new BaseDeletedDataFileScanTask( + changeOrdinal, + commitSnapshotId, + dataFile, + deletedFileDeletes, + context.schemaAsString(), + context.specAsString(), + context.residuals()); + + default: + throw new IllegalArgumentException("Unexpected entry status: " + entry.status()); + } + }); + } + + /** + * Gets delete files that apply to an ADDED data file. Only includes deletes added in the same + * snapshot as the file. + */ + private DeleteFile[] getDeletesForAddedFile( + ManifestEntry entry, long commitSnapshotId) { + DeleteFileIndex addedDeleteIndex = addedDeletesBySnapshot.get(commitSnapshotId); + return addedDeleteIndex == null || addedDeleteIndex.isEmpty() + ? NO_DELETES + : addedDeleteIndex.forEntry(entry); + } + + /** + * Gets all delete files that were applied to a DELETED data file up to the point it was + * deleted. This includes existing deletes and all deletes added in the scan range up to (but + * not including) the deletion snapshot. + */ + private DeleteFile[] getDeletesForDeletedFile( + ManifestEntry entry, long deletionSnapshotId) { + + List allDeletes = Lists.newArrayList(); + + // Build existing delete index lazily when first DELETED entry is encountered + DeleteFileIndex existingDeleteIndex = existingDeleteIndexSupplier.get(); + DeleteFile[] existingDeletes = + existingDeleteIndex.isEmpty() ? NO_DELETES : existingDeleteIndex.forEntry(entry); + for (DeleteFile df : existingDeletes) { + allDeletes.add(df); + } + + // Add all deletes from snapshots in the scan range BEFORE the deletion + List cumulativeDeletes = cumulativeDeletesMap.get(deletionSnapshotId); + if (cumulativeDeletes != null && !cumulativeDeletes.isEmpty()) { + DeleteFileIndex tempIndex = + DeleteFileIndex.builderFor(cumulativeDeletes) + .specsById(specsById) + .caseSensitive(caseSensitive) + .build(); + DeleteFile[] applicable = tempIndex.forEntry(entry); + for (DeleteFile deleteFile : applicable) { + allDeletes.add(deleteFile); + } + } + + return allDeletes.isEmpty() ? NO_DELETES : allDeletes.toArray(new DeleteFile[0]); + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java new file mode 100644 index 000000000000..a32b307b45d3 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Classes copied from unreleased Iceberg core. */ +package org.apache.iceberg; diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java index 74408d67ed86..fd24580e67c1 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java @@ -543,7 +543,7 @@ public void testStreamingReadWithColumnPruning_drop() throws Exception { List expectedRows = populateTable(table); - List fieldsToDrop = Arrays.asList("row", "str", "modulo_5", "nullable_long"); + List fieldsToDrop = Arrays.asList("row", "modulo_5", "nullable_long"); RowFilter rowFilter = new RowFilter(BEAM_SCHEMA).drop(fieldsToDrop); Map config = new HashMap<>(managedIcebergConfig(tableId())); From d957e8986e49c0c276eeb68cb53b46e732e6de0e Mon Sep 17 00:00:00 2001 From: Ahmed Abualsaud <65791736+ahmedabu98@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:08:54 -0400 Subject: [PATCH 475/490] [Iceberg CDC] Add utils base, delete reader, and serializable files (#38821) --- .../IO_Iceberg_Integration_Tests.json | 2 +- .../sdk/io/iceberg/IcebergScanConfig.java | 78 +- .../beam/sdk/io/iceberg/PartitionUtils.java | 100 ++- .../beam/sdk/io/iceberg/ReadFromTasks.java | 4 +- .../apache/beam/sdk/io/iceberg/ReadUtils.java | 137 ++-- .../beam/sdk/io/iceberg/ScanTaskReader.java | 4 +- .../sdk/io/iceberg/SerializableDataFile.java | 100 ++- .../io/iceberg/SerializableDeleteFile.java | 335 +++++++++ .../sdk/io/iceberg/cdc/CdcOutputUtils.java | 178 +++++ .../beam/sdk/io/iceberg/cdc/CdcReadUtils.java | 698 ++++++++++++++++++ .../beam/sdk/io/iceberg/cdc/DeleteReader.java | 309 ++++++++ .../cdc/IcebergCdcMetadataColumns.java | 93 +++ .../cdc/SerializableChangelogTask.java | 282 +++++++ .../beam/sdk/io/iceberg/cdc/package-info.java | 20 + ...ergCdcReadSchemaTransformProviderTest.java | 12 +- .../sdk/io/iceberg/IcebergIOReadTest.java | 83 ++- ...IcebergSchemaTransformTranslationTest.java | 33 +- .../sdk/io/iceberg/PartitionUtilsTest.java | 68 ++ .../beam/sdk/io/iceberg/ReadUtilsTest.java | 59 +- .../io/iceberg/SerializableDataFileTest.java | 3 + .../iceberg/SerializableDeleteFileTest.java | 222 ++++++ .../sdk/io/iceberg/TestDataWarehouse.java | 2 +- .../iceberg/catalog/IcebergCatalogBaseIT.java | 5 +- .../sdk/io/iceberg/cdc/CdcReadUtilsTest.java | 381 ++++++++++ .../sdk/io/iceberg/cdc/DeleteReaderTest.java | 419 +++++++++++ .../cdc/SerializableChangelogTaskTest.java | 256 +++++++ 26 files changed, 3711 insertions(+), 172 deletions(-) create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtils.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReader.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IcebergCdcMetadataColumns.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java create mode 100644 sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java create mode 100644 sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFileTest.java create mode 100644 sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java create mode 100644 sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java create mode 100644 sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index 7ab7bcd9a9c6..b73af5e61a43 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 2 + "modification": 1 } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java index a942c9804c93..cac4a015346c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java @@ -19,21 +19,26 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets.newHashSet; import com.google.auto.value.AutoValue; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; +import org.apache.beam.sdk.io.iceberg.cdc.IcebergCdcMetadataColumns; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.iceberg.Table; +import org.apache.iceberg.TableUtil; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.expressions.Evaluator; import org.apache.iceberg.expressions.Expression; @@ -48,7 +53,6 @@ public abstract class IcebergScanConfig implements Serializable { private transient @MonotonicNonNull Table cachedTable; private transient org.apache.iceberg.@MonotonicNonNull Schema cachedProjectedSchema; private transient org.apache.iceberg.@MonotonicNonNull Schema cachedRequiredSchema; - private transient @MonotonicNonNull Evaluator cachedEvaluator; private transient @MonotonicNonNull Expression cachedFilter; public enum ScanType { @@ -142,15 +146,12 @@ public org.apache.iceberg.Schema getRequiredSchema() { @Pure @Nullable - public Evaluator getEvaluator() { + public Evaluator getEvaluator(org.apache.iceberg.Schema requiredSchema) { @Nullable Expression filter = getFilter(); if (filter == null) { return null; } - if (cachedEvaluator == null) { - cachedEvaluator = new Evaluator(getRequiredSchema().asStruct(), filter); - } - return cachedEvaluator; + return new Evaluator(requiredSchema.asStruct(), filter); } @Pure @@ -225,6 +226,9 @@ public Expression getFilter() { @Pure public abstract @Nullable List getDropFields(); + @Pure + public abstract List getMetadataColumns(); + @Pure public static Builder builder() { return new AutoValue_IcebergScanConfig.Builder() @@ -247,7 +251,8 @@ public static Builder builder() { .setPollInterval(null) .setStartingStrategy(null) .setTag(null) - .setBranch(null); + .setBranch(null) + .setMetadataColumns(ImmutableList.of()); } @AutoValue.Builder @@ -310,6 +315,8 @@ public Builder setTableIdentifier(String... names) { public abstract Builder setDropFields(@Nullable List fields); + public abstract Builder setMetadataColumns(List metadataColumns); + public abstract IcebergScanConfig build(); } @@ -363,6 +370,9 @@ void validate(Table table) { if (getStartingStrategy() != null) { invalidOptions.add("starting_strategy"); } + if (!getMetadataColumns().isEmpty()) { + invalidOptions.add("metadata_columns"); + } if (!invalidOptions.isEmpty()) { throw new IllegalArgumentException( error( @@ -370,6 +380,19 @@ void validate(Table table) { + "reading with Managed.ICEBERG_CDC: " + invalidOptions)); } + } else { + Set primaryKeyIds = new HashSet<>(table.schema().identifierFieldIds()); + checkState( + !primaryKeyIds.isEmpty(), + "Cannot read CDC records as the table schema does not specified any primary key fields."); + Set projectedFieldIds = TypeUtil.getProjectedIds(getProjectedSchema()); + primaryKeyIds.removeAll(projectedFieldIds); + checkArgument( + primaryKeyIds.isEmpty(), + "When reading CDC records, the projected schema must not drop primary key fields. " + + "The specified configuration drops the following PK fields: %s", + primaryKeyIds); + validateMetadataColumns(table); } if (getStartingStrategy() != null) { @@ -392,6 +415,47 @@ void validate(Table table) { } } + private void validateMetadataColumns(Table table) { + List metadataColumns = getMetadataColumns(); + if (metadataColumns.isEmpty()) { + return; + } + + Set uniqueMetadataColumns = new LinkedHashSet<>(metadataColumns); + checkArgument( + uniqueMetadataColumns.size() == metadataColumns.size(), + error("metadata_columns contains duplicate entries: %s"), + metadataColumns); + + List unsupportedMetadataColumns = new ArrayList<>(); + for (String metadataColumn : metadataColumns) { + if (!IcebergCdcMetadataColumns.isSupportedColumn(metadataColumn)) { + unsupportedMetadataColumns.add(metadataColumn); + } + } + checkArgument( + unsupportedMetadataColumns.isEmpty(), + error("unsupported metadata_columns: %s. Supported values are: %s"), + unsupportedMetadataColumns, + IcebergCdcMetadataColumns.SUPPORTED_COLUMNS); + + for (String metadataColumn : metadataColumns) { + checkArgument( + getProjectedSchema().findField(metadataColumn) == null, + error("metadata column '%s' conflicts with a projected data column"), + metadataColumn); + } + + boolean includesRowLineage = + metadataColumns.stream().anyMatch(IcebergCdcMetadataColumns::isRowMetadataColumn); + if (includesRowLineage) { + checkArgument( + TableUtil.formatVersion(table) >= 3, + error("row lineage metadata columns %s are only available for Iceberg format v3+ tables"), + metadataColumns); + } + } + private String error(String message) { return "Invalid source configuration: " + message; } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java index 805cc0672940..32a25439d850 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.iceberg; import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.iceberg.data.IdentityPartitionConverters.convertConstant; import java.util.List; import java.util.Map; @@ -25,11 +26,20 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.ContentFile; +import org.apache.iceberg.ContentScanTask; +import org.apache.iceberg.MetadataColumns; +import org.apache.iceberg.PartitionField; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; import org.apache.iceberg.expressions.Expressions; import org.apache.iceberg.expressions.Term; +import org.apache.iceberg.types.Types; import org.checkerframework.checker.nullness.qual.Nullable; class PartitionUtils { @@ -48,23 +58,28 @@ class PartitionUtils { Pattern, BiFunction> TRANSFORMATIONS = ImmutableMap.of( - HOUR, (builder, matcher) -> builder.hour(checkStateNotNull(matcher.group(1))), - DAY, (builder, matcher) -> builder.day(checkStateNotNull(matcher.group(1))), - MONTH, (builder, matcher) -> builder.month(checkStateNotNull(matcher.group(1))), - YEAR, (builder, matcher) -> builder.year(checkStateNotNull(matcher.group(1))), + HOUR, + (builder, matcher) -> builder.hour(checkStateNotNull(matcher.group(1))), + DAY, + (builder, matcher) -> builder.day(checkStateNotNull(matcher.group(1))), + MONTH, + (builder, matcher) -> builder.month(checkStateNotNull(matcher.group(1))), + YEAR, + (builder, matcher) -> builder.year(checkStateNotNull(matcher.group(1))), TRUNCATE, - (builder, matcher) -> - builder.truncate( - checkStateNotNull(matcher.group(1)), - Integer.parseInt(checkStateNotNull(matcher.group(2)))), + (builder, matcher) -> + builder.truncate( + checkStateNotNull(matcher.group(1)), + Integer.parseInt(checkStateNotNull(matcher.group(2)))), BUCKET, - (builder, matcher) -> - builder.bucket( - checkStateNotNull(matcher.group(1)), - Integer.parseInt(checkStateNotNull(matcher.group(2)))), - VOID, (builder, matcher) -> builder.alwaysNull(checkStateNotNull(matcher.group(1))), + (builder, matcher) -> + builder.bucket( + checkStateNotNull(matcher.group(1)), + Integer.parseInt(checkStateNotNull(matcher.group(2)))), + VOID, + (builder, matcher) -> builder.alwaysNull(checkStateNotNull(matcher.group(1))), IDENTITY, - (builder, matcher) -> builder.identity(checkStateNotNull(matcher.group(1)))); + (builder, matcher) -> builder.identity(checkStateNotNull(matcher.group(1)))); static PartitionSpec toPartitionSpec( @Nullable List fields, org.apache.beam.sdk.schemas.Schema beamSchema) { @@ -130,4 +145,61 @@ static Term toIcebergTerm(String field) { throw new IllegalArgumentException("Could not find a partition term for '" + field + "'."); } + + /** + * Copied over from Apache Iceberg's PartitionUtil. + * + *

      Needed to accommodate CDC reads, where scans produce {@link ChangelogScanTask}s instead of + * {@link ContentScanTask}s. + */ + public static Map constantsMap( + PartitionSpec spec, ContentFile file, @Nullable Long fileSequenceNumber) { + Preconditions.checkState( + spec.specId() == file.specId(), + "File spec ID (%s) does not match PartitionSpec ID (%s)", + file.specId(), + spec.specId()); + StructLike partitionData = file.partition(); + + // use java.util.HashMap because partition data may contain null values + Map idToConstant = Maps.newHashMap(); + + // add first_row_id as _row_id + if (file.firstRowId() != null) { + idToConstant.put( + MetadataColumns.ROW_ID.fieldId(), + convertConstant(Types.LongType.get(), file.firstRowId())); + } + + // When reconstructing a DataFile, we lose the ability to attach its fileSequenceNumber, + // so we pipe it along the util methods to include it here. + fileSequenceNumber = + fileSequenceNumber != null ? fileSequenceNumber : file.fileSequenceNumber(); + idToConstant.put( + MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId(), + convertConstant(Types.LongType.get(), fileSequenceNumber)); + + // add _file + idToConstant.put( + MetadataColumns.FILE_PATH.fieldId(), + convertConstant(Types.StringType.get(), file.location())); + + // add _spec_id + idToConstant.put( + MetadataColumns.SPEC_ID.fieldId(), convertConstant(Types.IntegerType.get(), file.specId())); + + List partitionFields = spec.partitionType().fields(); + List fields = spec.fields(); + for (int pos = 0; pos < fields.size(); pos += 1) { + PartitionField field = fields.get(pos); + if (field.transform().isIdentity()) { + Object converted = + convertConstant(partitionFields.get(pos).type(), partitionData.get(pos, Object.class)); + idToConstant.put(field.sourceId(), converted); + } + } + + return idToConstant; + } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java index 5eeeacda48e3..71114437731c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java @@ -70,9 +70,7 @@ public void process( } FileScanTask task = fileScanTasks.get((int) l); Schema beamSchema = IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema()); - try (CloseableIterable fullIterable = - ReadUtils.createReader(task, table, scanConfig.getRequiredSchema())) { - CloseableIterable reader = ReadUtils.maybeApplyFilter(fullIterable, scanConfig); + try (CloseableIterable reader = ReadUtils.createReader(task, table, scanConfig)) { for (Record record : reader) { Row row = IcebergUtils.icebergRecordToBeamRow(beamSchema, record); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java index e7f50882f433..ea2dc7589c4e 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java @@ -20,25 +20,22 @@ import static org.apache.iceberg.util.SnapshotUtil.ancestorsOf; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.BiFunction; import java.util.stream.Collectors; import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; import org.apache.hadoop.conf.Configuration; -import org.apache.iceberg.FileScanTask; +import org.apache.iceberg.ContentFile; +import org.apache.iceberg.ContentScanTask; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; import org.apache.iceberg.Snapshot; import org.apache.iceberg.Table; import org.apache.iceberg.TableProperties; -import org.apache.iceberg.data.IdentityPartitionConverters; import org.apache.iceberg.data.InternalRecordWrapper; import org.apache.iceberg.data.Record; import org.apache.iceberg.data.parquet.GenericParquetReaders; @@ -46,17 +43,12 @@ import org.apache.iceberg.encryption.EncryptedInputFile; import org.apache.iceberg.expressions.Evaluator; import org.apache.iceberg.expressions.Expression; -import org.apache.iceberg.expressions.Expressions; import org.apache.iceberg.hadoop.HadoopInputFile; import org.apache.iceberg.io.CloseableIterable; import org.apache.iceberg.io.InputFile; -import org.apache.iceberg.mapping.MappingUtil; import org.apache.iceberg.mapping.NameMapping; import org.apache.iceberg.mapping.NameMappingParser; import org.apache.iceberg.parquet.ParquetReader; -import org.apache.iceberg.types.Type; -import org.apache.iceberg.types.TypeUtil; -import org.apache.iceberg.util.PartitionUtil; import org.apache.iceberg.util.SnapshotUtil; import org.apache.parquet.HadoopReadOptions; import org.apache.parquet.ParquetReadOptions; @@ -73,13 +65,34 @@ public class ReadUtils { "parquet.read.support.class", "parquet.crypto.factory.class"); - static ParquetReader createReader(FileScanTask task, Table table, Schema schema) { - String filePath = task.file().path().toString(); + public static CloseableIterable createReader( + ContentScanTask task, Table table, IcebergScanConfig scanConfig) { + return createReader( + table, + scanConfig, + scanConfig.getRequiredSchema(), + task.spec(), + task.file(), + null, + task.start(), + task.length(), + task.residual()); + } + + public static CloseableIterable createReader( + Table table, + IcebergScanConfig scanConfig, + Schema requiredSchema, + PartitionSpec spec, + ContentFile file, + @Nullable Long fileSequenceNumber, + long start, + long length, + Expression residual) { EncryptedInputFile encryptedInput = - EncryptedFiles.encryptedInput(table.io().newInputFile(filePath), task.file().keyMetadata()); + EncryptedFiles.encryptedInput(table.io().newInputFile(file.location()), file.keyMetadata()); InputFile inputFile = table.encryption().decrypt(encryptedInput); - Map idToConstants = - ReadUtils.constantsMap(task, IdentityPartitionConverters::convertConstant, table.schema()); + Map idToConstants = PartitionUtils.constantsMap(spec, file, fileSequenceNumber); ParquetReadOptions.Builder optionsBuilder; if (inputFile instanceof HadoopInputFile) { @@ -94,71 +107,30 @@ static ParquetReader createReader(FileScanTask task, Table table, Schema } optionsBuilder = optionsBuilder - .withRange(task.start(), task.start() + task.length()) + .withRange(start, start + length) .withMaxAllocationInBytes(MAX_FILE_BUFFER_SIZE); @Nullable String nameMapping = table.properties().get(TableProperties.DEFAULT_NAME_MAPPING); NameMapping mapping = nameMapping != null ? NameMappingParser.fromJson(nameMapping) : NameMapping.empty(); - return new ParquetReader<>( - inputFile, - schema, - optionsBuilder.build(), - // TODO(ahmedabu98): Implement a Parquet-to-Beam Row reader, bypassing conversion to Iceberg - // Record - fileSchema -> GenericParquetReaders.buildReader(schema, fileSchema, idToConstants), - mapping, - task.residual(), - false, - true); + ParquetReader records = + new ParquetReader<>( + inputFile, + requiredSchema, + optionsBuilder.build(), + // TODO(ahmedabu98): Implement a Parquet-to-Beam Row reader, bypassing conversion to + // Iceberg Record + fileSchema -> + GenericParquetReaders.buildReader(requiredSchema, fileSchema, idToConstants), + mapping, + residual, + false, + true); + return maybeApplyFilter(records, scanConfig, requiredSchema); } - static ParquetReader createReader(InputFile inputFile, Schema schema) { - ParquetReadOptions.Builder optionsBuilder; - if (inputFile instanceof HadoopInputFile) { - // remove read properties already set that may conflict with this read - Configuration conf = new Configuration(((HadoopInputFile) inputFile).getConf()); - for (String property : READ_PROPERTIES_TO_REMOVE) { - conf.unset(property); - } - optionsBuilder = HadoopReadOptions.builder(conf); - } else { - optionsBuilder = ParquetReadOptions.builder(); - } - optionsBuilder = - optionsBuilder - .withRange(0, inputFile.getLength()) - .withMaxAllocationInBytes(MAX_FILE_BUFFER_SIZE); - - return new ParquetReader<>( - inputFile, - schema, - optionsBuilder.build(), - fileSchema -> GenericParquetReaders.buildReader(schema, fileSchema), - MappingUtil.create(schema), - Expressions.alwaysTrue(), - false, - true); - } - - static Map constantsMap( - FileScanTask task, - BiFunction converter, - org.apache.iceberg.Schema schema) { - PartitionSpec spec = task.spec(); - Set idColumns = spec.identitySourceIds(); - org.apache.iceberg.Schema partitionSchema = TypeUtil.select(schema, idColumns); - boolean projectsIdentityPartitionColumns = !partitionSchema.columns().isEmpty(); - - if (projectsIdentityPartitionColumns) { - return PartitionUtil.constantsMap(task, converter); - } else { - return Collections.emptyMap(); - } - } - - static @Nullable Long getFromSnapshotExclusive(Table table, IcebergScanConfig scanConfig) { + public static @Nullable Long getFromSnapshotInclusive(Table table, IcebergScanConfig scanConfig) { @Nullable StartingStrategy startingStrategy = scanConfig.getStartingStrategy(); boolean isStreaming = MoreObjects.firstNonNull(scanConfig.getStreaming(), false); if (startingStrategy == null) { @@ -179,6 +151,13 @@ static ParquetReader createReader(InputFile inputFile, Schema schema) { fromSnapshot = currentSnapshot.snapshotId(); } } + + return fromSnapshot; + } + + public static @Nullable Long getFromSnapshotExclusive(Table table, IcebergScanConfig scanConfig) { + @Nullable Long fromSnapshot = getFromSnapshotInclusive(table, scanConfig); + // incremental append scan can only be configured with an *exclusive* starting snapshot, // so we need to provide this snapshot's parent id. if (fromSnapshot != null) { @@ -189,7 +168,7 @@ static ParquetReader createReader(InputFile inputFile, Schema schema) { return fromSnapshot; } - static @Nullable Long getToSnapshot(Table table, IcebergScanConfig scanConfig) { + public static @Nullable Long getToSnapshot(Table table, IcebergScanConfig scanConfig) { // 1. fetch from to_snapshot @Nullable Long toSnapshot = scanConfig.getToSnapshot(); // 2. fetch from to_timestamp @@ -205,7 +184,7 @@ static ParquetReader createReader(InputFile inputFile, Schema schema) { * Returns a list of snapshots in the range (fromSnapshotId, toSnapshotId], ordered * chronologically. */ - static List snapshotsBetween( + public static List snapshotsBetween( Table table, String tableIdentifier, @Nullable Long fromSnapshotId, long toSnapshotId) { long from = MoreObjects.firstNonNull(fromSnapshotId, -1L); @SuppressWarnings("return") @@ -225,10 +204,14 @@ static List snapshotsBetween( public static CloseableIterable maybeApplyFilter( CloseableIterable iterable, IcebergScanConfig scanConfig) { - InternalRecordWrapper wrapper = - new InternalRecordWrapper(scanConfig.getRequiredSchema().asStruct()); + return maybeApplyFilter(iterable, scanConfig, scanConfig.getRequiredSchema()); + } + + public static CloseableIterable maybeApplyFilter( + CloseableIterable iterable, IcebergScanConfig scanConfig, Schema requiredSchema) { + InternalRecordWrapper wrapper = new InternalRecordWrapper(requiredSchema.asStruct()); Expression filter = scanConfig.getFilter(); - Evaluator evaluator = scanConfig.getEvaluator(); + Evaluator evaluator = scanConfig.getEvaluator(requiredSchema); if (filter != null && evaluator != null && filter.op() != Expression.Operation.TRUE) { return CloseableIterable.filter(iterable, record -> evaluator.eval(wrapper.wrap(record))); } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java index b3485a7bcc4f..c9ad372a0751 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java @@ -36,7 +36,6 @@ import org.apache.iceberg.TableProperties; import org.apache.iceberg.avro.Avro; import org.apache.iceberg.data.GenericDeleteFilter; -import org.apache.iceberg.data.IdentityPartitionConverters; import org.apache.iceberg.data.Record; import org.apache.iceberg.data.avro.DataReader; import org.apache.iceberg.data.orc.GenericOrcReader; @@ -121,8 +120,7 @@ public boolean advance() throws IOException { DataFile file = fileTask.file(); InputFile input = decryptor.getInputFile(fileTask); Map idToConstants = - ReadUtils.constantsMap( - fileTask, IdentityPartitionConverters::convertConstant, requiredSchema); + PartitionUtils.constantsMap(fileTask.spec(), fileTask.file(), null); CloseableIterable iterable; switch (file.format()) { diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java index 9e75be0a1987..e1291601d149 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java @@ -54,13 +54,13 @@ */ @DefaultSchema(AutoValueSchema.class) @AutoValue -abstract class SerializableDataFile { +public abstract class SerializableDataFile { public static Builder builder() { return new AutoValue_SerializableDataFile.Builder(); } @SchemaFieldNumber("0") - abstract String getPath(); + public abstract String getPath(); @SchemaFieldNumber("1") abstract String getFileFormat(); @@ -69,10 +69,10 @@ public static Builder builder() { abstract long getRecordCount(); @SchemaFieldNumber("3") - abstract long getFileSizeInBytes(); + public abstract long getFileSizeInBytes(); @SchemaFieldNumber("4") - abstract String getPartitionPath(); + public abstract String getPartitionPath(); @SchemaFieldNumber("5") abstract int getPartitionSpecId(); @@ -96,13 +96,22 @@ public static Builder builder() { abstract @Nullable Map getNanValueCounts(); @SchemaFieldNumber("12") - abstract @Nullable Map getLowerBounds(); + public abstract @Nullable Map getLowerBounds(); @SchemaFieldNumber("13") - abstract @Nullable Map getUpperBounds(); + public abstract @Nullable Map getUpperBounds(); + + @SchemaFieldNumber("14") + public abstract @Nullable Long getDataSequenceNumber(); + + @SchemaFieldNumber("15") + public abstract @Nullable Long getFileSequenceNumber(); + + @SchemaFieldNumber("16") + public abstract @Nullable Long getFirstRowId(); @AutoValue.Builder - abstract static class Builder { + public abstract static class Builder { abstract Builder setPath(String path); abstract Builder setFileFormat(String fileFormat); @@ -131,31 +140,49 @@ abstract static class Builder { abstract Builder setUpperBounds(@Nullable Map upperBounds); + abstract Builder setDataSequenceNumber(@Nullable Long number); + + abstract Builder setFileSequenceNumber(@Nullable Long number); + + abstract Builder setFirstRowId(@Nullable Long id); + abstract SerializableDataFile build(); } + public static SerializableDataFile from(DataFile f, String partitionPath) { + return from(f, partitionPath, true); + } + /** * Create a {@link SerializableDataFile} from a {@link DataFile} and its associated {@link * PartitionKey}. */ - static SerializableDataFile from(DataFile f, String partitionPath) { - - return SerializableDataFile.builder() - .setPath(f.location().toString()) - .setFileFormat(f.format().toString()) - .setRecordCount(f.recordCount()) - .setFileSizeInBytes(f.fileSizeInBytes()) - .setPartitionPath(partitionPath) - .setPartitionSpecId(f.specId()) - .setKeyMetadata(f.keyMetadata()) - .setSplitOffsets(f.splitOffsets()) - .setColumnSizes(f.columnSizes()) - .setValueCounts(f.valueCounts()) - .setNullValueCounts(f.nullValueCounts()) - .setNanValueCounts(f.nanValueCounts()) - .setLowerBounds(toByteArrayMap(f.lowerBounds())) - .setUpperBounds(toByteArrayMap(f.upperBounds())) - .build(); + public static SerializableDataFile from( + DataFile f, String partitionPath, boolean includeMetrics) { + SerializableDataFile.Builder builder = + SerializableDataFile.builder() + .setPath(f.location()) + .setFileFormat(f.format().toString()) + .setRecordCount(f.recordCount()) + .setFileSizeInBytes(f.fileSizeInBytes()) + .setPartitionPath(partitionPath) + .setPartitionSpecId(f.specId()) + .setKeyMetadata(f.keyMetadata()) + .setSplitOffsets(f.splitOffsets()) + .setColumnSizes(f.columnSizes()) + .setValueCounts(f.valueCounts()) + .setNullValueCounts(f.nullValueCounts()) + .setNanValueCounts(f.nanValueCounts()) + .setDataSequenceNumber(f.dataSequenceNumber()) + .setFileSequenceNumber(f.fileSequenceNumber()) + .setFirstRowId(f.firstRowId()); + if (includeMetrics) { + builder = + builder + .setLowerBounds(toByteArrayMap(f.lowerBounds())) + .setUpperBounds(toByteArrayMap(f.upperBounds())); + } + return builder.build(); } /** @@ -165,7 +192,7 @@ static SerializableDataFile from(DataFile f, String partitionPath) { * it from Beam-compatible types. */ @SuppressWarnings("nullness") - DataFile createDataFile(Map partitionSpecs) { + public DataFile createDataFile(Map partitionSpecs) { PartitionSpec partitionSpec = checkStateNotNull( partitionSpecs.get(getPartitionSpecId()), @@ -192,14 +219,14 @@ DataFile createDataFile(Map partitionSpecs) { .withFileSizeInBytes(getFileSizeInBytes()) .withMetrics(dataFileMetrics) .withSplitOffsets(getSplitOffsets()) + .withFirstRowId(getFirstRowId()) .build(); } // ByteBuddyUtils has trouble converting Map value type ByteBuffer // to byte[] and back to ByteBuffer, so we perform these conversions manually // TODO(https://github.com/apache/beam/issues/32701) - private static @Nullable Map toByteArrayMap( - @Nullable Map input) { + static @Nullable Map toByteArrayMap(@Nullable Map input) { if (input == null) { return null; } @@ -222,8 +249,7 @@ private static byte[] toByteArray(ByteBuffer buf) { return bytes; } - private static @Nullable Map toByteBufferMap( - @Nullable Map input) { + static @Nullable Map toByteBufferMap(@Nullable Map input) { if (input == null) { return null; } @@ -256,10 +282,13 @@ && getPartitionSpecId() == that.getPartitionSpecId() && Objects.equals(getNullValueCounts(), that.getNullValueCounts()) && Objects.equals(getNanValueCounts(), that.getNanValueCounts()) && mapEquals(getLowerBounds(), that.getLowerBounds()) - && mapEquals(getUpperBounds(), that.getUpperBounds()); + && mapEquals(getUpperBounds(), that.getUpperBounds()) + && Objects.equals(getDataSequenceNumber(), that.getDataSequenceNumber()) + && Objects.equals(getFileSequenceNumber(), that.getFileSequenceNumber()) + && Objects.equals(getFirstRowId(), that.getFirstRowId()); } - private static boolean mapEquals( + static boolean mapEquals( @Nullable Map map1, @Nullable Map map2) { if (map1 == null && map2 == null) { return true; @@ -297,13 +326,16 @@ public final int hashCode() { getColumnSizes(), getValueCounts(), getNullValueCounts(), - getNanValueCounts()); + getNanValueCounts(), + getDataSequenceNumber(), + getFileSequenceNumber(), + getFirstRowId()); hashCode = 31 * hashCode + computeMapByteHashCode(getLowerBounds()); hashCode = 31 * hashCode + computeMapByteHashCode(getUpperBounds()); return hashCode; } - private static int computeMapByteHashCode(@Nullable Map map) { + static int computeMapByteHashCode(@Nullable Map map) { if (map == null) { return 0; } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java new file mode 100644 index 000000000000..ceb96d50f8aa --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.computeMapByteHashCode; +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.mapEquals; +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.toByteArrayMap; +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.toByteBufferMap; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import com.google.auto.value.AutoValue; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortOrder; +import org.checkerframework.checker.nullness.qual.Nullable; + +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class SerializableDeleteFile { + public static SerializableDeleteFile.Builder builder() { + return new AutoValue_SerializableDeleteFile.Builder(); + } + + @SchemaFieldNumber("0") + public abstract FileContent getContentType(); + + @SchemaFieldNumber("1") + public abstract String getLocation(); + + @SchemaFieldNumber("2") + public abstract String getFileFormat(); + + @SchemaFieldNumber("3") + public abstract long getRecordCount(); + + @SchemaFieldNumber("4") + public abstract long getFileSizeInBytes(); + + @SchemaFieldNumber("5") + public abstract String getPartitionPath(); + + @SchemaFieldNumber("6") + public abstract int getPartitionSpecId(); + + @SchemaFieldNumber("7") + public abstract @Nullable Integer getSortOrderId(); + + @SchemaFieldNumber("8") + public abstract @Nullable List getEqualityFieldIds(); + + @SchemaFieldNumber("9") + public abstract @Nullable ByteBuffer getKeyMetadata(); + + @SchemaFieldNumber("10") + public abstract @Nullable List getSplitOffsets(); + + @SchemaFieldNumber("11") + public abstract @Nullable Map getColumnSizes(); + + @SchemaFieldNumber("12") + public abstract @Nullable Map getValueCounts(); + + @SchemaFieldNumber("13") + public abstract @Nullable Map getNullValueCounts(); + + @SchemaFieldNumber("14") + public abstract @Nullable Map getNanValueCounts(); + + @SchemaFieldNumber("15") + public abstract @Nullable Map getLowerBounds(); + + @SchemaFieldNumber("16") + public abstract @Nullable Map getUpperBounds(); + + @SchemaFieldNumber("17") + public abstract @Nullable Long getContentOffset(); + + @SchemaFieldNumber("18") + public abstract @Nullable Long getContentSizeInBytes(); + + @SchemaFieldNumber("19") + public abstract @Nullable String getReferencedDataFile(); + + @SchemaFieldNumber("20") + public abstract @Nullable Long getDataSequenceNumber(); + + @SchemaFieldNumber("21") + public abstract @Nullable Long getFileSequenceNumber(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setContentType(FileContent content); + + abstract Builder setLocation(String path); + + abstract Builder setFileFormat(String fileFormat); + + abstract Builder setRecordCount(long recordCount); + + abstract Builder setFileSizeInBytes(long fileSizeInBytes); + + abstract Builder setPartitionPath(String partitionPath); + + abstract Builder setPartitionSpecId(int partitionSpec); + + abstract Builder setSortOrderId(@Nullable Integer sortOrderId); + + abstract Builder setEqualityFieldIds(List equalityFieldIds); + + abstract Builder setKeyMetadata(ByteBuffer keyMetadata); + + abstract Builder setSplitOffsets(List splitOffsets); + + abstract Builder setColumnSizes(Map columnSizes); + + abstract Builder setValueCounts(Map valueCounts); + + abstract Builder setNullValueCounts(Map nullValueCounts); + + abstract Builder setNanValueCounts(Map nanValueCounts); + + abstract Builder setLowerBounds(@Nullable Map lowerBounds); + + abstract Builder setUpperBounds(@Nullable Map upperBounds); + + abstract Builder setContentOffset(@Nullable Long offset); + + abstract Builder setContentSizeInBytes(@Nullable Long sizeInBytes); + + abstract Builder setReferencedDataFile(@Nullable String dataFile); + + abstract Builder setDataSequenceNumber(@Nullable Long number); + + abstract Builder setFileSequenceNumber(@Nullable Long number); + + abstract SerializableDeleteFile build(); + } + + public static SerializableDeleteFile from( + DeleteFile deleteFile, String partitionPath, boolean includeMetrics) { + + SerializableDeleteFile.Builder builder = + SerializableDeleteFile.builder() + .setLocation(deleteFile.location()) + .setFileFormat(deleteFile.format().name()) + .setFileSizeInBytes(deleteFile.fileSizeInBytes()) + .setPartitionPath(partitionPath) + .setPartitionSpecId(deleteFile.specId()) + .setRecordCount(deleteFile.recordCount()) + .setColumnSizes(deleteFile.columnSizes()) + .setValueCounts(deleteFile.valueCounts()) + .setNullValueCounts(deleteFile.nullValueCounts()) + .setNanValueCounts(deleteFile.nanValueCounts()) + .setSplitOffsets(deleteFile.splitOffsets()) + .setKeyMetadata(deleteFile.keyMetadata()) + .setEqualityFieldIds(deleteFile.equalityFieldIds()) + .setSortOrderId(deleteFile.sortOrderId()) + .setContentOffset(deleteFile.contentOffset()) + .setContentSizeInBytes(deleteFile.contentSizeInBytes()) + .setReferencedDataFile(deleteFile.referencedDataFile()) + .setContentType(deleteFile.content()) + .setDataSequenceNumber(deleteFile.dataSequenceNumber()) + .setFileSequenceNumber(deleteFile.fileSequenceNumber()); + + if (includeMetrics) { + builder = + builder + .setLowerBounds(toByteArrayMap(deleteFile.lowerBounds())) + .setUpperBounds(toByteArrayMap(deleteFile.upperBounds())); + } + + return builder.build(); + } + + @SuppressWarnings("nullness") + public DeleteFile createDeleteFile( + Map partitionSpecs, @Nullable Map sortOrders) { + PartitionSpec partitionSpec = + checkStateNotNull( + partitionSpecs.get(getPartitionSpecId()), + "This DeleteFile was originally created with spec id '%s', " + + "but table only has spec ids: %s.", + getPartitionSpecId(), + partitionSpecs.keySet()); + + Metrics metrics = + new Metrics( + getRecordCount(), + getColumnSizes(), + getValueCounts(), + getNullValueCounts(), + getNanValueCounts(), + toByteBufferMap(getLowerBounds()), + toByteBufferMap(getUpperBounds())); + + FileMetadata.Builder deleteFileBuilder = + FileMetadata.deleteFileBuilder(partitionSpec) + .withPath(getLocation()) + .withFormat(getFileFormat()) + .withFileSizeInBytes(getFileSizeInBytes()) + .withRecordCount(getRecordCount()) + .withMetrics(metrics) + .withSplitOffsets(getSplitOffsets()) + .withEncryptionKeyMetadata(getKeyMetadata()) + .withPartitionPath(getPartitionPath()); + + switch (getContentType()) { + case POSITION_DELETES: + deleteFileBuilder = deleteFileBuilder.ofPositionDeletes(); + break; + case EQUALITY_DELETES: + List fieldIds = getEqualityFieldIds(); + int[] equalityFieldIds = new int[fieldIds != null ? fieldIds.size() : 0]; + if (fieldIds != null) { + for (int i = 0; i < fieldIds.size(); i++) { + equalityFieldIds[i] = fieldIds.get(i); + } + } + SortOrder sortOrder = SortOrder.unsorted(); + if (sortOrders != null) { + sortOrder = + checkStateNotNull( + sortOrders.get(getSortOrderId()), + "This DeleteFile was originally created with sort order id '%s', " + + "but table only has sort order ids: %s.", + getSortOrderId(), + sortOrders.keySet()); + } + deleteFileBuilder = + deleteFileBuilder.ofEqualityDeletes(equalityFieldIds).withSortOrder(sortOrder); + break; + default: + throw new IllegalStateException( + "Unexpected content type for DeleteFile: " + getContentType()); + } + + // needed for puffin files + if (getFileFormat().equalsIgnoreCase(FileFormat.PUFFIN.name())) { + deleteFileBuilder = + deleteFileBuilder + .withContentOffset(checkStateNotNull(getContentOffset())) + .withContentSizeInBytes(checkStateNotNull(getContentSizeInBytes())) + .withReferencedDataFile(checkStateNotNull(getReferencedDataFile())); + } + return deleteFileBuilder.build(); + } + + @Override + public final boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SerializableDeleteFile)) { + return false; + } + SerializableDeleteFile that = (SerializableDeleteFile) o; + return getContentType().equals(that.getContentType()) + && getLocation().equals(that.getLocation()) + && getFileFormat().equals(that.getFileFormat()) + && getRecordCount() == that.getRecordCount() + && getFileSizeInBytes() == that.getFileSizeInBytes() + && getPartitionPath().equals(that.getPartitionPath()) + && getPartitionSpecId() == that.getPartitionSpecId() + && Objects.equals(getSortOrderId(), that.getSortOrderId()) + && Objects.equals(getEqualityFieldIds(), that.getEqualityFieldIds()) + && Objects.equals(getKeyMetadata(), that.getKeyMetadata()) + && Objects.equals(getSplitOffsets(), that.getSplitOffsets()) + && Objects.equals(getColumnSizes(), that.getColumnSizes()) + && Objects.equals(getValueCounts(), that.getValueCounts()) + && Objects.equals(getNullValueCounts(), that.getNullValueCounts()) + && Objects.equals(getNanValueCounts(), that.getNanValueCounts()) + && mapEquals(getLowerBounds(), that.getLowerBounds()) + && mapEquals(getUpperBounds(), that.getUpperBounds()) + && Objects.equals(getContentOffset(), that.getContentOffset()) + && Objects.equals(getContentSizeInBytes(), that.getContentSizeInBytes()) + && Objects.equals(getReferencedDataFile(), that.getReferencedDataFile()) + && Objects.equals(getDataSequenceNumber(), that.getDataSequenceNumber()) + && Objects.equals(getFileSequenceNumber(), that.getFileSequenceNumber()); + } + + @Override + public final int hashCode() { + int hashCode = + Objects.hash( + getContentType(), + getLocation(), + getFileFormat(), + getRecordCount(), + getFileSizeInBytes(), + getPartitionPath(), + getPartitionSpecId(), + getSortOrderId(), + getEqualityFieldIds(), + getKeyMetadata(), + getSplitOffsets(), + getColumnSizes(), + getValueCounts(), + getNullValueCounts(), + getNanValueCounts(), + getContentOffset(), + getContentSizeInBytes(), + getReferencedDataFile(), + getDataSequenceNumber(), + getFileSequenceNumber()); + hashCode = 31 * hashCode + computeMapByteHashCode(getLowerBounds()); + hashCode = 31 * hashCode + computeMapByteHashCode(getUpperBounds()); + return hashCode; + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java new file mode 100644 index 000000000000..147e2adda1a7 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import java.util.ArrayList; +import java.util.List; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.types.Types; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Helpers for CDC schemas and output row construction. + * + *

      CDC metadata is handled in two phases. Row metadata, such as {@code _row_id} and {@code + * _last_updated_sequence_number}, is added to intermediate read schemas so Iceberg readers can + * populate those values. Commit metadata, such as {@code _commit_snapshot_id} and {@code + * _commit_snapshot_sequence_number}, is carried separately in CDC descriptors. The {@code + * _change_type} metadata column comes from the resolved Beam output {@link ValueKind}. + * + *

      The public output shape is assembled only when final Beam {@link Row}s are emitted. This keeps + * the read path table-shaped while still exposing all requested metadata as top-level output + * fields. + */ +final class CdcOutputUtils { + /** + * Returns the public CDC output schema: projected data fields followed by requested metadata + * columns in user-configured order. + */ + static Schema outputSchema(IcebergScanConfig scanConfig, Schema dataSchema) { + if (scanConfig.getMetadataColumns().isEmpty()) { + return dataSchema; + } + + Schema.Builder builder = Schema.builder().addFields(dataSchema.getFields()); + for (String metadataColumn : scanConfig.getMetadataColumns()) { + builder.addField(IcebergCdcMetadataColumns.beamField(metadataColumn)); + } + return builder.build(); + } + + /** + * Returns an Iceberg read schema that includes row metadata columns. + * + *

      Commit metadata columns are not added here because Iceberg readers cannot populate them; + * those values are taken from {@link ChangelogDescriptor} or {@link CdcRowDescriptor} when output + * rows are built. + */ + static org.apache.iceberg.Schema readSchemaWithRowMetadata( + List metadataColumns, org.apache.iceberg.Schema dataSchema) { + List fields = new ArrayList<>(dataSchema.columns()); + for (String metadataColumn : metadataColumns) { + Types.NestedField rowMetadataField = + IcebergCdcMetadataColumns.icebergRowMetadataField(metadataColumn); + if (rowMetadataField != null && dataSchema.findField(rowMetadataField.fieldId()) == null) { + fields.add(rowMetadataField); + } + } + return new org.apache.iceberg.Schema(fields, dataSchema.identifierFieldIds()); + } + + /** + * Beam-schema equivalent of {@link #readSchemaWithRowMetadata(List, org.apache.iceberg.Schema)}. + */ + static Schema readBeamSchemaWithRowMetadata(List metadataColumns, Schema dataSchema) { + if (metadataColumns.stream().noneMatch(IcebergCdcMetadataColumns::isRowMetadataColumn)) { + return dataSchema; + } + + Schema.Builder builder = Schema.builder().addFields(dataSchema.getFields()); + for (String metadataColumn : metadataColumns) { + if (IcebergCdcMetadataColumns.isRowMetadataColumn(metadataColumn) + && !dataSchema.hasField(metadataColumn)) { + builder.addField(IcebergCdcMetadataColumns.beamField(metadataColumn)); + } + } + return builder.build(); + } + + /** + * Builds the final public Beam row. + * + *

      {@code dataAndRowMetadata} may already include row metadata read from Iceberg. This method + * copies only data fields first, then appends every requested metadata column at the top level. + * That preserves configured column order and avoids exposing row metadata twice. + */ + static Row outputRow( + List metadataColumns, + Schema outputSchema, + long commitSnapshotId, + long snapshotSequentNumber, + ValueKind valueKind, + Row dataAndRowMetadata) { + if (metadataColumns.isEmpty() + || metadataColumns.stream().allMatch(IcebergCdcMetadataColumns::isRowMetadataColumn)) { + return dataAndRowMetadata; + } + + List<@Nullable Object> values = new ArrayList<>(outputSchema.getFieldCount()); + for (Schema.Field field : dataAndRowMetadata.getSchema().getFields()) { + if (!metadataColumns.contains(field.getName())) { + values.add(dataAndRowMetadata.getValue(field.getName())); + } + } + + for (String metadataColumn : metadataColumns) { + values.add( + metadataValue( + metadataColumn, + commitSnapshotId, + snapshotSequentNumber, + valueKind, + dataAndRowMetadata)); + } + return Row.withSchema(outputSchema).addValues(values).build(); + } + + static Schema readBeamSchemaWithRowMetadata( + List metadataColumns, org.apache.iceberg.Schema dataSchema) { + return IcebergUtils.icebergSchemaToBeamSchema( + readSchemaWithRowMetadata(metadataColumns, dataSchema)); + } + + private static @Nullable Object metadataValue( + String metadataColumn, + long commitSnapshotId, + long commitSnapshotSequenceNumber, + ValueKind valueKind, + Row dataAndRowMetadata) { + if (IcebergCdcMetadataColumns.CHANGE_TYPE.equals(metadataColumn)) { + return changelogOperation(valueKind).name(); + } + if (IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID.equals(metadataColumn)) { + return commitSnapshotId; + } + if (IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER.equals(metadataColumn)) { + return commitSnapshotSequenceNumber; + } + if (dataAndRowMetadata.getSchema().hasField(metadataColumn)) { + return dataAndRowMetadata.getValue(metadataColumn); + } + return null; + } + + private static ChangelogOperation changelogOperation(ValueKind valueKind) { + switch (valueKind) { + case INSERT: + return ChangelogOperation.INSERT; + case DELETE: + return ChangelogOperation.DELETE; + case UPDATE_BEFORE: + return ChangelogOperation.UPDATE_BEFORE; + case UPDATE_AFTER: + return ChangelogOperation.UPDATE_AFTER; + default: + throw new IllegalArgumentException("Unsupported CDC ValueKind: " + valueKind); + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtils.java new file mode 100644 index 000000000000..daa0a2c73fb8 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtils.java @@ -0,0 +1,698 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.ReadUtils; +import org.apache.beam.sdk.io.iceberg.SerializableDeleteFile; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.data.BaseDeleteLoader; +import org.apache.iceberg.data.DeleteFilter; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.data.InternalRecordWrapper; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.io.InputFile; +import org.apache.iceberg.io.SeekableInputStream; +import org.apache.iceberg.parquet.ParquetMetricsRowGroupFilter; +import org.apache.iceberg.types.TypeUtil; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.StructLikeSet; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.metadata.BlockMetaData; +import org.apache.parquet.hadoop.metadata.ParquetMetadata; +import org.apache.parquet.io.DelegatingSeekableInputStream; +import org.apache.parquet.schema.MessageType; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Read-side helpers specific to the CDC source. Keeps {@link ReadUtils} focused on the + * general-purpose append-only read path; everything that takes a {@link SerializableChangelogTask}, + * references {@link DeleteReader}, or implements the delete-pushdown row-group skipping lives here. + */ +public final class CdcReadUtils { + private static final Logger LOG = LoggerFactory.getLogger(CdcReadUtils.class); + + /** + * Maximum size of an equality delete set to push down as a Parquet residual {@code IN} + * expression. Matches {@link ParquetMetricsRowGroupFilter#IN_PREDICATE_LIMIT}. + */ + private static final int IN_PREDICATE_LIMIT = 200; + + public static CloseableIterable createReader( + SerializableChangelogTask task, + Table table, + IcebergScanConfig scanConfig, + Schema outputSchema) { + return createReader(task, table, scanConfig, outputSchema, Expressions.alwaysTrue()); + } + + /** + * Same as {@link #createReader(SerializableChangelogTask, Table, IcebergScanConfig, Schema)} but + * ANDs {@code extraResidual} into the task's residual expression. The combined expression is + * passed to Iceberg's Parquet reader, which uses it as a row-group-level filter (skips row groups + * whose column statistics cannot match). The caller is still responsible for applying the + * residual at the row level. + * + *

      This is used to push extra predicates (e.g. an equality-delete {@code IN} expression) down + * to the reader for cheap row-group skipping. + */ + public static CloseableIterable createReader( + SerializableChangelogTask task, + Table table, + IcebergScanConfig scanConfig, + Schema outputSchema, + Expression extraResidual) { + return createReader( + task, table, scanConfig, outputSchema, extraResidual, task.getStart(), task.getLength()); + } + + /** + * Same as {@link #createReader(SerializableChangelogTask, Table, IcebergScanConfig, Schema, + * Expression)} but reads the byte range {@code [start, start + length)} of the DataFile. + * Iceberg's Parquet reader selects the row groups whose starting offset falls within this range, + * allowing us to prune row-groups by byte-range. + * + *

      Callers are responsible for ensuring the requested range stays within the task's assigned + * range, to avoid reading a section that is meant for another worker. + */ + public static CloseableIterable createReader( + SerializableChangelogTask task, + Table table, + IcebergScanConfig scanConfig, + Schema outputSchema, + Expression extraResidual, + long start, + long length) { + Expression baseResidual = task.getExpression(table.schema()); + Expression combined = + extraResidual.op() == Expression.Operation.TRUE + ? baseResidual + : Expressions.and(baseResidual, extraResidual); + return ReadUtils.createReader( + table, + scanConfig, + outputSchema, + checkStateNotNull(table.specs().get(task.getSpecId())), + task.getDataFile().createDataFile(table.specs()), + task.getDataFile().getFileSequenceNumber(), + start, + length, + combined); + } + + /** Returns a filter that skips records marked for deletion. */ + public static DeleteFilter genericDeleteFilter( + Table table, Schema outputSchema, String dataFilePath, List deletes) { + return new GenericDeleteFilter( + table.io(), + dataFilePath, + table.schema(), + outputSchema, + deletes.stream() + .map(sdf -> sdf.createDeleteFile(table.specs(), table.sortOrders())) + .collect(Collectors.toList())); + } + + /** Returns a delete reader that reuses delete structures already loaded by CDC planning. */ + public static DeleteReader genericDeleteReader( + Table table, + Schema outputSchema, + String dataFilePath, + List deletes, + DeleteReader.PreloadedDeletes preloadedDeletes) { + return new GenericDeleteReader( + table.io(), + dataFilePath, + table.schema(), + outputSchema, + deletes.stream() + .map(sdf -> sdf.createDeleteFile(table.specs(), table.sortOrders())) + .collect(Collectors.toList()), + preloadedDeletes); + } + + /** + * Opens the records that a CDC reader should process for a single {@link + * SerializableChangelogTask}, applying the appropriate delete-filter / delete-reader chain for + * the task's type: + * + *

        + *
      • {@code ADDED_ROWS}: Collect and return the records that became live in this commit: + *
          + *
        • 1. Iterate over records in the added DataFile + *
        • 2. Filter out records matched by any added deletes + *
        + *
      • {@code DELETED_ROWS}: Return records in the DataFile that are marked for deletion by new + * DeleteFiles, making sure to first ignore records that have already been marked by + * previous DeleteFiles: + *
          + *
        • 1. Iterate over records in the referenced DataFile + *
        • 2. Filter out records matched from existing deletes. + *
        • 3. Filter out records NOT matched from added deletes + *
        + *
      • {@code DELETED_FILE} — every record in the DataFile that wasn't already deleted by {@code + * existingDeletes}. + *
          + *
        • 1. Iterate over records in the referenced DataFile + *
        • 2. Filter out records matched from existing deletes. + *
        + *
      + * + *

      Projection pushdown should not be used when reading bi-directional tasks because we need to + * compare all record columns to accurately identify updates. Otherwise, user-configured + * projection may drop a column that contains real updates. If this happens, the downstream + * resolver will mistakenly determine the (delete, insert) pair to be a duplicate. + * + *

      If CDC metadata columns are requested, this method only adds row-sourced metadata columns + * ({@code _row_id}, {@code _last_updated_sequence_number}) to the Iceberg read schema. Changelog + * context columns are added later by {@link CdcOutputUtils#outputRow}. + */ + public static CloseableIterable changelogRecordsForTask( + SerializableChangelogTask task, + Table table, + IcebergScanConfig scanConfig, + boolean useProjectedSchema) { + String dataFilePath = task.getDataFile().getPath(); + Schema outputSchema = + CdcOutputUtils.readSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), + useProjectedSchema ? scanConfig.getRequiredSchema() : table.schema()); + switch (task.getType()) { + case ADDED_ROWS: + DeleteFilter addedDeletesFilter = + genericDeleteFilter(table, outputSchema, dataFilePath, task.getAddedDeletes()); + return addedDeletesFilter.filter( + createReader(task, table, scanConfig, addedDeletesFilter.requiredSchema())); + case DELETED_FILE: + DeleteFilter existingDeletesFilter = + genericDeleteFilter(table, outputSchema, dataFilePath, task.getExistingDeletes()); + return existingDeletesFilter.filter( + createReader(task, table, scanConfig, existingDeletesFilter.requiredSchema())); + case DELETED_ROWS: + return deletedRowsForTask(task, table, scanConfig, outputSchema); + default: + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getType()); + } + } + + /** + * Builds the reader chain for a {@code DELETED_ROWS} task with row-group pushdown when possible. + * This helps the reader skip entire row groups. For unskipped row groups, the reader should still + * apply per-record position + equality checks at the row level. + * + *

      We use two pushdown strategies, depending on the type of {@link DeleteFile} in the task + * (Position Delete vs. Equality Delete). The two strategies can be combined if both {@link + * DeleteFile} types are present. + * + *

        + *
      1. Byte-range pushdown for Position Deletes: pre-load the {@link + * PositionDeleteIndex}, read the Parquet footer, and compute a single contiguous byte range + * covering the row groups that contain at least one deleted position. + *
      2. IN-expression pushdown for Equality Deletes: build an Iceberg {@code IN} + * expression and pass it as a Parquet residual so the metrics row-group filter can skip + * non-matching row groups. + *
      + * + *

      If Position and Equality deletes are both present, both strategies are used to get one + * contiguous range. We read only that range, skipping leading and trailing row groups that + * contain no deletions. + * + *

      Note: Equality pushdown is only used when all delete files share a single equality field. + * Multi-column equality requires an exploded OR expression that Parquet's metrics filter handles + * poorly. + */ + private static CloseableIterable deletedRowsForTask( + SerializableChangelogTask task, + Table table, + IcebergScanConfig scanConfig, + Schema outputSchema) { + String dataFilePath = task.getDataFile().getPath(); + List addedDeletes = task.getAddedDeletes(); + + // Split into position vs equality. + List posFiles = new ArrayList<>(); + List eqFiles = new ArrayList<>(); + for (SerializableDeleteFile sd : addedDeletes) { + DeleteFile df = sd.createDeleteFile(table.specs(), table.sortOrders()); + if (df.content() == FileContent.POSITION_DELETES) { + posFiles.add(df); + } else if (df.content() == FileContent.EQUALITY_DELETES) { + eqFiles.add(df); + } + } + + // Strategy 1: byte-range pushdown around row groups with position deletes (+ eq + // matches). + DeleteReader.PreloadedDeletes preloadedDeletes = DeleteReader.PreloadedDeletes.empty(); + if (!posFiles.isEmpty()) { + @Nullable + PositionPushdownResult pushdown = + tryPositionByteRangePushdown( + task, table, scanConfig, outputSchema, posFiles, eqFiles, addedDeletes); + if (pushdown != null) { + if (pushdown.deletedRecords != null) { + return pushdown.deletedRecords; + } + preloadedDeletes = pushdown.preloadedDeletes; + } + // fall through to the default chain on failure + } + + // Strategy 2: equality IN-expression pushdown applied as a reader residual. + // Only safe when no position deletes are present. when both exist, the + // byte-range path above already incorporates the eq filter + Expression eqResidual = Expressions.alwaysTrue(); + if (posFiles.isEmpty() && !eqFiles.isEmpty()) { + EqualityPushdownResult eqPushdown = buildEqualityDeletePushdown(table, eqFiles); + eqResidual = eqPushdown.applicable ? eqPushdown.residual : Expressions.alwaysTrue(); + preloadedDeletes = eqPushdown.preloadedDeletes(null); + } + + DeleteFilter existingDeletesFilter = + genericDeleteFilter(table, outputSchema, dataFilePath, task.getExistingDeletes()); + DeleteReader addedDeletesReader = + genericDeleteReader(table, outputSchema, dataFilePath, addedDeletes, preloadedDeletes); + Schema requiredSchema = + TypeUtil.join(existingDeletesFilter.requiredSchema(), addedDeletesReader.requiredSchema()); + + CloseableIterable records = + createReader(task, table, scanConfig, requiredSchema, eqResidual); + CloseableIterable liveRecords = existingDeletesFilter.filter(records); + return addedDeletesReader.read(liveRecords); + } + + /** + * Path-A byte-range position-delete pushdown. Returns {@code null} if pushdown isn't applicable + * or any step fails, signaling to the caller to fall back. Returns an empty iterable if every row + * group is pruned. + */ + private static @Nullable PositionPushdownResult tryPositionByteRangePushdown( + SerializableChangelogTask task, + Table table, + IcebergScanConfig scanConfig, + Schema outputSchema, + List posFiles, + List eqFiles, + List addedDeletes) { + String dataFilePath = task.getDataFile().getPath(); + + // 1. pre-load the position index for this data file. + PositionDeleteIndex posIndex; + try { + DeleteLoader loader = new BaseDeleteLoader(df -> table.io().newInputFile(df.location())); + posIndex = loader.loadPositionDeletes(posFiles, dataFilePath); + } catch (RuntimeException e) { + LOG.info( + "Failed to pre-load position deletes for {}; falling back to default reader chain.", + dataFilePath, + e); + return null; + } + if (posIndex.isEmpty()) { + // the pos-delete files don't actually target this data file (rare but possible + // after metadata operations). Fall back so the eq pushdown does not run here either. + return PositionPushdownResult.fallback( + DeleteReader.PreloadedDeletes.of(posIndex, Collections.emptyMap())); + } + + // 2. optional equality filter (used to extend the byte range to include row groups + // whose stats match the equality IN values). + @Nullable ParquetMetricsRowGroupFilter eqFilter = null; + EqualityPushdownResult eqPushdown = EqualityPushdownResult.notApplicable(); + if (!eqFiles.isEmpty()) { + eqPushdown = buildEqualityDeletePushdown(table, eqFiles); + if (!eqPushdown.applicable) { + // eq deletes are present but we can't safely identify which row groups they target. + // A narrowed position-only range could drop eq-deleted rows, so fall back to the + // default full-range reader. DeleteReader will still apply residual per record. + return PositionPushdownResult.fallback(eqPushdown.preloadedDeletes(posIndex)); + } + eqFilter = new ParquetMetricsRowGroupFilter(table.schema(), eqPushdown.residual); + } + DeleteReader.PreloadedDeletes preloadedDeletes = eqPushdown.preloadedDeletes(posIndex); + + // 3. read the footer and compute the task byte range covering every row group that + // contains a position delete or matches the eq filter. + long taskStart = task.getStart(); + long taskEnd = taskStart + task.getLength(); + long minStart = Long.MAX_VALUE; + long maxEnd = Long.MIN_VALUE; + + try { + long[] sortedDeletePositions = sortedDeletePositions(posIndex); + InputFile inputFile = table.io().newInputFile(dataFilePath); + try (ParquetFileReader reader = ParquetFileReader.open(asParquetInputFile(inputFile))) { + ParquetMetadata footer = reader.getFooter(); + MessageType parquetSchema = footer.getFileMetaData().getSchema(); + + // track cumulative row count ourselves. not all Parquet writers will include + // it in BlockMetaData.getRowIndexOffset + long cumulativeRows = 0; + for (BlockMetaData rowGroup : footer.getBlocks()) { + long rgStartPos = cumulativeRows; + long rgEndPos = cumulativeRows + rowGroup.getRowCount(); + cumulativeRows = rgEndPos; + + long rgByteStart = rowGroup.getStartingPos(); + long rgByteEnd = rgByteStart + rowGroup.getCompressedSize(); + + // skip row groups outside this task's range. + if (rgByteEnd <= taskStart || rgByteStart >= taskEnd) { + continue; + } + + // if row group has a position and/or an equality delete, include it in the global range + boolean rowGroupHasPosDelete = anyInRange(sortedDeletePositions, rgStartPos, rgEndPos); + boolean rowGroupMatchesEq = + eqFilter != null && eqFilter.shouldRead(parquetSchema, rowGroup); + + if (rowGroupHasPosDelete || rowGroupMatchesEq) { + minStart = Math.min(minStart, rgByteStart); + maxEnd = Math.max(maxEnd, rgByteEnd); + } + } + } + } catch (IOException | RuntimeException e) { + LOG.info( + "Failed to read Parquet footer for {}; falling back to default reader chain.", + dataFilePath, + e); + return PositionPushdownResult.fallback(preloadedDeletes); + } + + long readStart = Math.max(minStart, taskStart); + long readEnd = Math.min(maxEnd, taskEnd); + if (readStart >= readEnd) { + // deletes don't target the portion of the DataFile covered by this read task. + return PositionPushdownResult.of(CloseableIterable.empty(), preloadedDeletes); + } + + // 4. Open the reader with the narrowed byte range. This range represents the union + // of "has position delete" + "matches eq stats" + DeleteFilter existingDeletesFilter = + genericDeleteFilter(table, outputSchema, dataFilePath, task.getExistingDeletes()); + DeleteReader addedDeletesReader = + genericDeleteReader(table, outputSchema, dataFilePath, addedDeletes, preloadedDeletes); + Schema requiredSchema = + TypeUtil.join(existingDeletesFilter.requiredSchema(), addedDeletesReader.requiredSchema()); + CloseableIterable records = + createReader( + task, + table, + scanConfig, + requiredSchema, + Expressions.alwaysTrue(), + readStart, + readEnd - readStart); + CloseableIterable liveRecords = existingDeletesFilter.filter(records); + return PositionPushdownResult.of(addedDeletesReader.read(liveRecords), preloadedDeletes); + } + + /** Materializes a sorted long[] of the positions in {@code posIndex} for binary-search lookup. */ + private static long[] sortedDeletePositions(PositionDeleteIndex posIndex) { + long cardinality = posIndex.cardinality(); + if (cardinality > Integer.MAX_VALUE) { + throw new IllegalStateException( + "Position delete index cardinality exceeds Integer.MAX_VALUE: " + cardinality); + } + long[] arr = new long[(int) cardinality]; + int[] idx = {0}; + posIndex.forEach(p -> arr[idx[0]++] = p); + // forEach is ordered for the bitmap-backed implementation, but the interface doesn't + // promise it, so sort defensively. Cheap relative to the I/O it gates. + Arrays.sort(arr); + return arr; + } + + /** Returns true iff {@code sortedDeletes} contains any value in {@code [start, end)}. */ + private static boolean anyInRange(long[] sortedDeletes, long startInclusive, long endExclusive) { + if (sortedDeletes.length == 0) { + return false; + } + int i = Arrays.binarySearch(sortedDeletes, startInclusive); + if (i < 0) { + i = -i - 1; // insertion point + } + return i < sortedDeletes.length && sortedDeletes[i] < endExclusive; + } + + /** + * Returns an {@code IN} expression suitable as a Parquet residual for the given equality-delete + * files, or {@link Expressions#alwaysTrue()} if pushdown is not applicable. See {@link + * #deletedRowsForTask} for the applicability rules. + */ + private static EqualityPushdownResult buildEqualityDeletePushdown( + Table table, List eqFiles) { + // All eq delete files in this task must share a single equality field id. + Set sharedIds = null; + for (DeleteFile df : eqFiles) { + Set ids = new HashSet<>(df.equalityFieldIds()); + if (sharedIds == null) { + sharedIds = ids; + } else if (!sharedIds.equals(ids)) { + return EqualityPushdownResult.notApplicable(); + } + } + if (sharedIds == null || sharedIds.size() != 1) { + return EqualityPushdownResult.notApplicable(); + } + + int fieldId = Iterables.getOnlyElement(sharedIds); + Types.NestedField field = table.schema().findField(fieldId); + if (field == null) { + return EqualityPushdownResult.notApplicable(); + } + Schema deleteSchema = TypeUtil.select(table.schema(), sharedIds); + + DeleteLoader loader = new BaseDeleteLoader(df -> table.io().newInputFile(df.location())); + StructLikeSet set; + try { + set = loader.loadEqualityDeletes(eqFiles, deleteSchema); + } catch (RuntimeException e) { + LOG.info( + "Failed to pre-load equality deletes for pushdown; falling back to per-record check.", e); + return EqualityPushdownResult.notApplicable(); + } + + Map, StructLikeSet> preloadedSets = new HashMap<>(); + preloadedSets.put(sharedIds, set); + + if (set.size() > IN_PREDICATE_LIMIT) { + return EqualityPushdownResult.notApplicable(preloadedSets); + } + Class javaClass = field.type().typeId().javaClass(); + List values = new ArrayList<>(set.size()); + for (StructLike s : set) { + @Nullable Object v = s.get(0, javaClass); + if (v == null) { + // Nulls don't match an IN-expression. pushing down would drop those deletions. + return EqualityPushdownResult.notApplicable(preloadedSets); + } + values.add(v); + } + if (values.isEmpty()) { + return EqualityPushdownResult.notApplicable(preloadedSets); + } + return EqualityPushdownResult.applicable(Expressions.in(field.name(), values), preloadedSets); + } + + private static final class PositionPushdownResult { + private final @Nullable CloseableIterable deletedRecords; + private final DeleteReader.PreloadedDeletes preloadedDeletes; + + private static PositionPushdownResult of( + CloseableIterable deletedRecords, DeleteReader.PreloadedDeletes preloadedDeletes) { + return new PositionPushdownResult(deletedRecords, preloadedDeletes); + } + + private static PositionPushdownResult fallback(DeleteReader.PreloadedDeletes preloadedDeletes) { + return new PositionPushdownResult(null, preloadedDeletes); + } + + private PositionPushdownResult( + @Nullable CloseableIterable records, + DeleteReader.PreloadedDeletes preloadedDeletes) { + this.deletedRecords = records; + this.preloadedDeletes = preloadedDeletes; + } + } + + private static final class EqualityPushdownResult { + private static final EqualityPushdownResult NOT_APPLICABLE = + new EqualityPushdownResult(Expressions.alwaysTrue(), Collections.emptyMap(), false); + + private final Expression residual; + private final Map, StructLikeSet> preloadedSets; + private final boolean applicable; + + private static EqualityPushdownResult applicable( + Expression residual, Map, StructLikeSet> preloadedSets) { + return new EqualityPushdownResult(residual, preloadedSets, true); + } + + private static EqualityPushdownResult notApplicable() { + return NOT_APPLICABLE; + } + + private static EqualityPushdownResult notApplicable( + Map, StructLikeSet> preloadedSets) { + if (preloadedSets.isEmpty()) { + return NOT_APPLICABLE; + } + return new EqualityPushdownResult(Expressions.alwaysTrue(), preloadedSets, false); + } + + private EqualityPushdownResult( + Expression residual, Map, StructLikeSet> preloadedSets, boolean applicable) { + this.residual = residual; + this.preloadedSets = preloadedSets; + this.applicable = applicable; + } + + private DeleteReader.PreloadedDeletes preloadedDeletes( + @Nullable PositionDeleteIndex positionDeleteIndex) { + return DeleteReader.PreloadedDeletes.of(positionDeleteIndex, preloadedSets); + } + } + + public static class GenericDeleteFilter extends DeleteFilter { + private final FileIO io; + private final InternalRecordWrapper asStructLike; + + @SuppressWarnings("method.invocation") + public GenericDeleteFilter( + FileIO io, + String dataFilePath, + Schema tableSchema, + Schema requiredSchema, + List deleteFiles) { + super(dataFilePath, deleteFiles, tableSchema, requiredSchema); + this.io = io; + this.asStructLike = new InternalRecordWrapper(requiredSchema().asStruct()); + } + + @Override + protected StructLike asStructLike(Record record) { + return asStructLike.wrap(record); + } + + @Override + protected InputFile getInputFile(String location) { + return io.newInputFile(location); + } + } + + public static class GenericDeleteReader extends DeleteReader { + private final FileIO io; + private final InternalRecordWrapper asStructLike; + + @SuppressWarnings("method.invocation") + public GenericDeleteReader( + FileIO io, + String dataFilePath, + Schema tableSchema, + Schema requiredSchema, + List deleteFiles, + DeleteReader.PreloadedDeletes preloadedDeletes) { + super(dataFilePath, deleteFiles, tableSchema, requiredSchema, true, preloadedDeletes); + this.io = io; + this.asStructLike = new InternalRecordWrapper(requiredSchema().asStruct()); + } + + @Override + protected StructLike asStructLike(Record record) { + return asStructLike.wrap(record); + } + + @Override + protected InputFile getInputFile(String location) { + return io.newInputFile(location); + } + } + + /** + * Adapter from Iceberg's {@link InputFile} to Parquet's {@link org.apache.parquet.io.InputFile}, + * for callers that need to open a Parquet file directly (e.g. to read the footer for row-group + * pruning decisions). Iceberg has an equivalent internal {@code ParquetIO} but it's + * package-private. + */ + public static org.apache.parquet.io.InputFile asParquetInputFile(InputFile icebergFile) { + return new IcebergParquetInputFile(icebergFile); + } + + private static final class IcebergParquetInputFile implements org.apache.parquet.io.InputFile { + private final InputFile delegate; + + IcebergParquetInputFile(InputFile delegate) { + this.delegate = delegate; + } + + @Override + public long getLength() { + return delegate.getLength(); + } + + @Override + public org.apache.parquet.io.SeekableInputStream newStream() { + return new IcebergParquetSeekableStream(delegate.newStream()); + } + } + + private static final class IcebergParquetSeekableStream extends DelegatingSeekableInputStream { + private final SeekableInputStream delegate; + + IcebergParquetSeekableStream(SeekableInputStream delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override + public long getPos() throws java.io.IOException { + return delegate.getPos(); + } + + @Override + public void seek(long newPos) throws java.io.IOException { + delegate.seek(newPos); + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReader.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReader.java new file mode 100644 index 000000000000..e1b9a9c98583 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReader.java @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Multimap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Multimaps; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; +import org.apache.iceberg.Accessor; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.MetadataColumns; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.data.BaseDeleteLoader; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.InputFile; +import org.apache.iceberg.types.TypeUtil; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.StructLikeSet; +import org.apache.iceberg.util.StructProjection; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Reads a {@link org.apache.iceberg.DataFile} and returns records marked deleted by the given + * {@link DeleteFile}s. + * + *

      This is mostly a copy of {@link org.apache.iceberg.data.DeleteFilter}, but flipping the logic + * to output deleted records instead of filtering them out. + */ +public abstract class DeleteReader { + private static final Logger LOG = LoggerFactory.getLogger(DeleteReader.class); + + private final String filePath; + private final List posDeletes; + private final List eqDeletes; + private final PreloadedDeletes preloadedDeletes; + private final Schema requiredSchema; + private final Accessor posAccessor; + private volatile @Nullable DeleteLoader deleteLoader = null; + private @Nullable PositionDeleteIndex deleteRowPositions = null; + private @Nullable List> isInDeleteSets = null; + + protected DeleteReader( + String filePath, + List deletes, + Schema tableSchema, + Schema expectedSchema, + boolean needRowPosCol, + PreloadedDeletes preloadedDeletes) { + this.filePath = filePath; + this.preloadedDeletes = preloadedDeletes; + + ImmutableList.Builder posDeleteBuilder = ImmutableList.builder(); + ImmutableList.Builder eqDeleteBuilder = ImmutableList.builder(); + for (DeleteFile delete : deletes) { + switch (delete.content()) { + case POSITION_DELETES: + LOG.debug("Adding position delete file {} to reader", delete.location()); + posDeleteBuilder.add(delete); + break; + case EQUALITY_DELETES: + LOG.debug("Adding equality delete file {} to reader", delete.location()); + eqDeleteBuilder.add(delete); + break; + default: + throw new UnsupportedOperationException( + "Unknown delete file content: " + delete.content()); + } + } + + this.posDeletes = posDeleteBuilder.build(); + this.eqDeletes = eqDeleteBuilder.build(); + this.requiredSchema = + fileProjection(tableSchema, expectedSchema, posDeletes, eqDeletes, needRowPosCol); + this.posAccessor = requiredSchema.accessorForField(MetadataColumns.ROW_POSITION.fieldId()); + } + + public Schema requiredSchema() { + return requiredSchema; + } + + protected abstract StructLike asStructLike(T record); + + protected abstract InputFile getInputFile(String location); + + protected InputFile loadInputFile(DeleteFile deleteFile) { + return getInputFile(deleteFile.location()); + } + + protected long pos(T record) { + return (Long) posAccessor.get(asStructLike(record)); + } + + protected DeleteLoader newDeleteLoader() { + return new BaseDeleteLoader(this::loadInputFile); + } + + private DeleteLoader deleteLoader() { + if (deleteLoader == null) { + synchronized (this) { + if (deleteLoader == null) { + this.deleteLoader = newDeleteLoader(); + } + } + } + + return deleteLoader; + } + + /** + * Returns records that are deleted by either the position deletes or the equality + * deletes attached to this reader — i.e. the union of the two delete predicates. + * + *

      Each delete-type predicate is built independently and defaults to "false" (no contribution + * to the union) when its side has no delete files. Both predicates are then OR-combined and + * applied in a single pass over {@code records}. This guarantees that: + * + *

        + *
      • A task with only position deletes emits all records whose position is in the index. + *
      • A task with only equality deletes emits all records matching any equality delete value. + *
      • A task with both emits the union of the two (without duplication). + *
      + */ + public CloseableIterable read(CloseableIterable records) { + Predicate isPosDeleted = + posDeletes.isEmpty() ? t -> false : positionDeletePredicate(deletedRowPositions()); + Predicate isEqDeleted = applyEqDeletes().stream().reduce(Predicate::or).orElse(t -> false); + return CloseableIterable.filter(records, isPosDeleted.or(isEqDeleted)); + } + + private Predicate positionDeletePredicate(PositionDeleteIndex positionIndex) { + return record -> positionIndex.isDeleted(pos(record)); + } + + private List> applyEqDeletes() { + if (isInDeleteSets != null) { + return isInDeleteSets; + } + + isInDeleteSets = Lists.newArrayList(); + if (eqDeletes.isEmpty()) { + return isInDeleteSets; + } + + Multimap, DeleteFile> filesByDeleteIds = + Multimaps.newMultimap(Maps.newHashMap(), Lists::newArrayList); + for (DeleteFile delete : eqDeletes) { + filesByDeleteIds.put(Sets.newHashSet(delete.equalityFieldIds()), delete); + } + + for (Map.Entry, Collection> entry : + filesByDeleteIds.asMap().entrySet()) { + Set ids = entry.getKey(); + Iterable deletes = entry.getValue(); + + Schema deleteSchema = TypeUtil.select(requiredSchema, ids); + + // a projection to select and reorder fields of the file schema to match the delete rows + StructProjection projectRow = StructProjection.create(requiredSchema, deleteSchema); + + StructLikeSet deleteSet = preloadedDeletes.equalityDeleteSet(ids); + if (deleteSet == null) { + deleteSet = deleteLoader().loadEqualityDeletes(deletes, deleteSchema); + } + StructLikeSet deleteSetForPredicate = deleteSet; + Predicate isInDeleteSet = + record -> deleteSetForPredicate.contains(projectRow.wrap(asStructLike(record))); + checkStateNotNull(isInDeleteSets).add(isInDeleteSet); + } + + return checkStateNotNull(isInDeleteSets); + } + + public PositionDeleteIndex deletedRowPositions() { + if (deleteRowPositions == null) { + deleteRowPositions = preloadedDeletes.positionDeleteIndex(); + if (deleteRowPositions == null && !posDeletes.isEmpty()) { + deleteRowPositions = deleteLoader().loadPositionDeletes(posDeletes, filePath); + } + } + + return checkStateNotNull(deleteRowPositions); + } + + /** Delete data already loaded by a planning/pushdown path for one task read. */ + public static final class PreloadedDeletes { + private static final PreloadedDeletes EMPTY = + new PreloadedDeletes(null, Collections.emptyMap()); + + private final @Nullable PositionDeleteIndex positionDeleteIndex; + private final Map, StructLikeSet> equalityDeleteSets; + + public static PreloadedDeletes empty() { + return EMPTY; + } + + public static PreloadedDeletes of( + @Nullable PositionDeleteIndex positionDeleteIndex, + Map, StructLikeSet> equalityDeleteSets) { + if (positionDeleteIndex == null && equalityDeleteSets.isEmpty()) { + return EMPTY; + } + return new PreloadedDeletes(positionDeleteIndex, equalityDeleteSets); + } + + private PreloadedDeletes( + @Nullable PositionDeleteIndex positionDeleteIndex, + Map, StructLikeSet> equalityDeleteSets) { + this.positionDeleteIndex = positionDeleteIndex; + Map, StructLikeSet> copied = new HashMap<>(); + for (Map.Entry, StructLikeSet> entry : equalityDeleteSets.entrySet()) { + copied.put(Collections.unmodifiableSet(Sets.newHashSet(entry.getKey())), entry.getValue()); + } + this.equalityDeleteSets = Collections.unmodifiableMap(copied); + } + + public @Nullable PositionDeleteIndex positionDeleteIndex() { + return positionDeleteIndex; + } + + public @Nullable StructLikeSet equalityDeleteSet(Set equalityFieldIds) { + return equalityDeleteSets.get(equalityFieldIds); + } + } + + private static Schema fileProjection( + Schema tableSchema, + Schema requestedSchema, + List posDeletes, + List eqDeletes, + boolean needRowPosCol) { + if (posDeletes.isEmpty() && eqDeletes.isEmpty()) { + return requestedSchema; + } + + Set requiredIds = Sets.newLinkedHashSet(); + if (needRowPosCol && !posDeletes.isEmpty()) { + requiredIds.add(MetadataColumns.ROW_POSITION.fieldId()); + } + + for (DeleteFile eqDelete : eqDeletes) { + requiredIds.addAll(eqDelete.equalityFieldIds()); + } + + Set missingIds = + Sets.newLinkedHashSet( + Sets.difference(requiredIds, TypeUtil.getProjectedIds(requestedSchema))); + + if (missingIds.isEmpty()) { + return requestedSchema; + } + + // TODO: support adding nested columns. this will currently fail when finding nested columns to + // add + List columns = Lists.newArrayList(requestedSchema.columns()); + for (int fieldId : missingIds) { + if (fieldId == MetadataColumns.ROW_POSITION.fieldId() + || fieldId == MetadataColumns.IS_DELETED.fieldId()) { + continue; // add _pos and _deleted at the end + } + + Types.NestedField field = tableSchema.asStruct().field(fieldId); + Preconditions.checkArgument(field != null, "Cannot find required field for ID %s", fieldId); + + columns.add(field); + } + + if (missingIds.contains(MetadataColumns.ROW_POSITION.fieldId())) { + columns.add(MetadataColumns.ROW_POSITION); + } + + if (missingIds.contains(MetadataColumns.IS_DELETED.fieldId())) { + columns.add(MetadataColumns.IS_DELETED); + } + + return new Schema(columns); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IcebergCdcMetadataColumns.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IcebergCdcMetadataColumns.java new file mode 100644 index 000000000000..407934fb188d --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IcebergCdcMetadataColumns.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import org.apache.beam.sdk.annotations.Internal; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.iceberg.MetadataColumns; +import org.apache.iceberg.types.Types; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Supported top-level metadata columns for Beam Iceberg CDC reads. + * + *

      The supported columns come from two sources: + * + *

        + *
      • Iceberg row metadata: {@code _row_id} and {@code _last_updated_sequence_number}. These are + * requested from the physical Iceberg reader and are only available for row-lineage tables + * (v3+). + *
      • Changelog context metadata: {@code _change_type}, {@code _commit_snapshot_id}, and {@code + * _commit_snapshot_sequence_number}. These are known from the changelog snapshot/task context + * and are appended when Beam output rows are built. + *
      + */ +@Internal +public final class IcebergCdcMetadataColumns { + public static final String CHANGE_TYPE = MetadataColumns.CHANGE_TYPE.name(); + public static final String COMMIT_SNAPSHOT_SEQUENCE_NUMBER = "_commit_snapshot_sequence_number"; + public static final String COMMIT_SNAPSHOT_ID = MetadataColumns.COMMIT_SNAPSHOT_ID.name(); + public static final String ROW_ID = MetadataColumns.ROW_ID.name(); + public static final String LAST_UPDATED_SEQUENCE_NUMBER = + MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.name(); + + public static final ImmutableList SUPPORTED_COLUMNS = + ImmutableList.of( + CHANGE_TYPE, + COMMIT_SNAPSHOT_ID, + COMMIT_SNAPSHOT_SEQUENCE_NUMBER, + ROW_ID, + LAST_UPDATED_SEQUENCE_NUMBER); + + private static final ImmutableSet ROW_METADATA_COLUMNS = + ImmutableSet.of(ROW_ID, LAST_UPDATED_SEQUENCE_NUMBER); + + public static boolean isSupportedColumn(String name) { + return SUPPORTED_COLUMNS.contains(name); + } + + public static boolean isRowMetadataColumn(String name) { + return ROW_METADATA_COLUMNS.contains(name); + } + + public static Schema.Field beamField(String name) { + if (CHANGE_TYPE.equals(name)) { + return Schema.Field.of(name, Schema.FieldType.STRING); + } + if (COMMIT_SNAPSHOT_ID.equals(name) || COMMIT_SNAPSHOT_SEQUENCE_NUMBER.equals(name)) { + return Schema.Field.of(name, Schema.FieldType.INT64); + } + if (ROW_ID.equals(name) || LAST_UPDATED_SEQUENCE_NUMBER.equals(name)) { + return Schema.Field.nullable(name, Schema.FieldType.INT64); + } + throw new IllegalArgumentException("Unsupported CDC metadata column: " + name); + } + + /** Returns the Iceberg reader field for row-sourced metadata, or null for commit metadata. */ + public static Types.@Nullable NestedField icebergRowMetadataField(String name) { + if (ROW_ID.equals(name)) { + return MetadataColumns.ROW_ID; + } + if (LAST_UPDATED_SEQUENCE_NUMBER.equals(name)) { + return MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER; + } + return null; + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java new file mode 100644 index 000000000000..9b6955d9e4a5 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.SerializableDataFile; +import org.apache.beam.sdk.io.iceberg.SerializableDeleteFile; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.SchemaCoder; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; +import org.apache.beam.sdk.schemas.annotations.SchemaIgnore; +import org.apache.iceberg.AddedRowsScanTask; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.ContentScanTask; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.DeletedDataFileScanTask; +import org.apache.iceberg.DeletedRowsScanTask; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.ExpressionParser; + +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class SerializableChangelogTask { + public enum Type { + ADDED_ROWS, + DELETED_ROWS, + DELETED_FILE + } + + public static SchemaCoder coder() { + try { + return SchemaRegistry.createDefault().getSchemaCoder(SerializableChangelogTask.class); + } catch (NoSuchSchemaException e) { + throw new RuntimeException(e); + } + } + + public static SerializableChangelogTask.Builder builder() { + return new AutoValue_SerializableChangelogTask.Builder() + .setExistingDeletes(Collections.emptyList()) + .setAddedDeletes(Collections.emptyList()); + } + + @SchemaFieldNumber("0") + public abstract Type getType(); + + @SchemaFieldNumber("1") + public abstract SerializableDataFile getDataFile(); + + @SchemaFieldNumber("2") + public abstract List getExistingDeletes(); + + @SchemaFieldNumber("3") + public abstract List getAddedDeletes(); + + @SchemaFieldNumber("4") + public abstract int getSpecId(); + + @SchemaFieldNumber("5") + public abstract ChangelogOperation getOperation(); + + @SchemaFieldNumber("6") + public abstract int getOrdinal(); + + @SchemaFieldNumber("7") + public abstract long getCommitSnapshotId(); + + @SchemaFieldNumber("8") + public abstract long getStart(); + + @SchemaFieldNumber("9") + public abstract long getLength(); + + @SchemaFieldNumber("10") + public abstract String getJsonExpression(); + + @SchemaIgnore + public Expression getExpression(Schema schema) { + return ExpressionParser.fromJson(getJsonExpression(), schema); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + abstract Builder setType(Type type); + + abstract Builder setDataFile(SerializableDataFile dataFile); + + @SchemaIgnore + public Builder setDataFile(DataFile df, String partitionPath, boolean includeMetrics) { + return setDataFile(SerializableDataFile.from(df, partitionPath, includeMetrics)); + } + + abstract Builder setExistingDeletes(List existingDeletes); + + abstract Builder setAddedDeletes(List addedDeletes); + + abstract Builder setSpecId(int specId); + + abstract Builder setOperation(ChangelogOperation operation); + + abstract Builder setOrdinal(int ordinal); + + abstract Builder setCommitSnapshotId(long commitSnapshotId); + + abstract Builder setStart(long start); + + abstract Builder setLength(long length); + + abstract Builder setJsonExpression(String expression); + + abstract SerializableChangelogTask build(); + } + + public static SerializableChangelogTask from( + ChangelogScanTask task, Map specs) { + return from(task, specs, false); + } + + public static SerializableChangelogTask from( + ChangelogScanTask task, Map specs, boolean includeMetrics) { + checkState( + task instanceof ContentScanTask, "Expected ChangelogScanTask to also be a ContentScanTask"); + ContentScanTask contentScanTask = (ContentScanTask) task; + PartitionSpec spec = contentScanTask.spec(); + SerializableChangelogTask.Builder builder = + SerializableChangelogTask.builder() + .setOperation(task.operation()) + .setOrdinal(task.changeOrdinal()) + .setCommitSnapshotId(task.commitSnapshotId()) + .setDataFile( + contentScanTask.file(), + spec.partitionToPath(contentScanTask.partition()), + includeMetrics) + .setSpecId(spec.specId()) + .setStart(contentScanTask.start()) + .setLength(contentScanTask.length()) + .setJsonExpression(ExpressionParser.toJson(contentScanTask.residual())); + + if (task instanceof AddedRowsScanTask) { + AddedRowsScanTask addedRowsTask = (AddedRowsScanTask) task; + builder = + builder + .setType(Type.ADDED_ROWS) + .setAddedDeletes( + toSerializableDeletes(addedRowsTask.deletes(), specs, includeMetrics)); + } else if (task instanceof DeletedRowsScanTask) { + DeletedRowsScanTask deletedRowsTask = (DeletedRowsScanTask) task; + builder = + builder + .setType(Type.DELETED_ROWS) + .setAddedDeletes( + toSerializableDeletes(deletedRowsTask.addedDeletes(), specs, includeMetrics)) + .setExistingDeletes( + toSerializableDeletes(deletedRowsTask.existingDeletes(), specs, includeMetrics)); + } else if (task instanceof DeletedDataFileScanTask) { + DeletedDataFileScanTask deletedFileTask = (DeletedDataFileScanTask) task; + builder = + builder + .setType(Type.DELETED_FILE) + .setExistingDeletes( + toSerializableDeletes(deletedFileTask.existingDeletes(), specs, includeMetrics)); + } else { + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + return builder.build(); + } + + static Type getType(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return Type.ADDED_ROWS; + } else if (task instanceof DeletedRowsScanTask) { + return Type.DELETED_ROWS; + } else if (task instanceof DeletedDataFileScanTask) { + return Type.DELETED_FILE; + } else { + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + } + + static long getTotalLength(List tasks) { + return tasks.stream().mapToLong(SerializableChangelogTask::getLength).sum(); + } + + static long getLength(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).length(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).length(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).length(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static StructLike getPartition(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).partition(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).partition(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).partition(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static PartitionSpec getSpec(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).spec(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).spec(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).spec(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static DataFile getDataFile(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).file(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).file(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).file(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static List getAddedDeleteFiles(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).deletes(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).addedDeletes(); + } else if (task instanceof DeletedDataFileScanTask) { + return Collections.emptyList(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + private static List toSerializableDeletes( + List dfs, Map specs, boolean includeMetrics) { + return dfs.stream() + .map( + df -> + SerializableDeleteFile.from( + df, + checkStateNotNull(specs.get(df.specId())).partitionToPath(df.partition()), + includeMetrics)) + .collect(Collectors.toList()); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java new file mode 100644 index 000000000000..8285d91689be --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Iceberg CDC connectors. */ +package org.apache.beam.sdk.io.iceberg.cdc; diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java index 9b08e1ff86e1..5849cbd00774 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java @@ -35,6 +35,7 @@ import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.iceberg.CatalogUtil; import org.apache.iceberg.Snapshot; @@ -52,6 +53,9 @@ public class IcebergCdcReadSchemaTransformProviderTest { @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + private static final org.apache.iceberg.Schema CDC_SCHEMA = + new org.apache.iceberg.Schema(TestFixtures.SCHEMA.columns(), ImmutableSet.of(1)); + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); @Rule public TestPipeline testPipeline = TestPipeline.create(); @@ -83,8 +87,8 @@ public void testSimpleScan() throws Exception { String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); TableIdentifier tableId = TableIdentifier.parse(identifier); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, CDC_SCHEMA); + final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -122,8 +126,8 @@ public void testStreamingReadUsingManagedTransform() throws Exception { String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); TableIdentifier tableId = TableIdentifier.parse(identifier); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, CDC_SCHEMA); + final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable).subList(3, 9); List snapshots = Lists.newArrayList(simpleTable.snapshots()); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java index f79991cee571..d7c97efa19f3 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java @@ -54,6 +54,7 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; @@ -119,6 +120,14 @@ public static Iterable data() { @Parameter(0) public boolean useIncrementalScan; + private org.apache.iceberg.Schema schemaForMode( + org.apache.iceberg.Schema schema, Integer... identifierFieldIds) { + if (!useIncrementalScan) { + return schema; + } + return new org.apache.iceberg.Schema(schema.columns(), ImmutableSet.copyOf(identifierFieldIds)); + } + static class PrintRow extends PTransform, PCollection> { @Override @@ -143,7 +152,7 @@ public void process(@Element Row row, OutputReceiver output) { public void testFailWhenBothStartingSnapshotAndTimestampAreSet() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .from(tableId) @@ -161,7 +170,7 @@ public void testFailWhenBothStartingSnapshotAndTimestampAreSet() { public void testFailWhenBothEndingSnapshotAndTimestampAreSet() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .withCdc() @@ -179,7 +188,7 @@ public void testFailWhenBothEndingSnapshotAndTimestampAreSet() { public void testFailWhenStartingPointAndStartingStrategyAreSet() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .withCdc() @@ -197,7 +206,7 @@ public void testFailWhenStartingPointAndStartingStrategyAreSet() { public void testFailWhenPollIntervalIsSetOnBatchRead() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .withCdc() @@ -210,6 +219,32 @@ public void testFailWhenPollIntervalIsSetOnBatchRead() { read.expand(PBegin.in(testPipeline)); } + @Test + public void testCdcFailsWhenTableHasNoIdentifierFields() { + assumeTrue(useIncrementalScan); + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + warehouse.createTable(tableId, TestFixtures.SCHEMA); + IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()).from(tableId).withCdc(); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Cannot read CDC records"); + thrown.expectMessage("primary key fields"); + read.expand(PBegin.in(testPipeline)); + } + + @Test + public void testCdcFailsWhenProjectionDropsIdentifierFields() { + assumeTrue(useIncrementalScan); + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + IcebergIO.ReadRows read = + IcebergIO.readRows(catalogConfig()).from(tableId).withCdc().dropping(singletonList("id")); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("projected schema must not drop primary key fields"); + read.expand(PBegin.in(testPipeline)); + } + @Test public void testFailWhenDropAndKeepAreSet() { TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); @@ -307,8 +342,8 @@ public void testProjectedSchemaWithNestedFields() { public void testSimpleScan() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -339,8 +374,8 @@ public void testSimpleScan() throws Exception { public void testScanSelectedFields() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -368,6 +403,11 @@ public void testScanSelectedFields() throws Exception { return null; }); + if (useIncrementalScan) { + testPipeline.run(); + return; + } + // test drop fields read = read.keeping(null).dropping(singletonList("id")); PCollection outputDrop = @@ -387,7 +427,7 @@ public void testScanSelectedFields() throws Exception { public void testScanWithFilter() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); List> expectedRecords = warehouse.commitData(simpleTable); @@ -440,6 +480,7 @@ public void testReadSchemaWithRandomlyOrderedIds() throws IOException { required(1, "a", Types.IntegerType.get()), required(2, "b", StructType.of(nestedSchema.columns())), required(5, "c", StringType.get())); + schema = schemaForMode(schema, 1); // hadoop catalog will re-order by breadth-first ordering Table simpleTable = warehouse.createTable(tableId, schema); @@ -484,7 +525,8 @@ public void testReadSchemaWithRandomlyOrderedIds() throws IOException { public void testIdentityColumnScan() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); + org.apache.iceberg.Schema baseSchema = schemaForMode(TestFixtures.SCHEMA, 1); + Table simpleTable = warehouse.createTable(tableId, baseSchema); String identityColumnName = "identity"; String identityColumnValue = "some-value"; @@ -499,11 +541,7 @@ public void testIdentityColumnScan() throws Exception { .newFastAppend() .appendFile( warehouse.writeRecords( - "file1s1.parquet", - TestFixtures.SCHEMA, - spec, - partitionKey, - TestFixtures.FILE1SNAPSHOT1)) + "file1s1.parquet", baseSchema, spec, partitionKey, TestFixtures.FILE1SNAPSHOT1)) .commit(); final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); @@ -609,9 +647,10 @@ public void testNameMappingScan() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); + org.apache.iceberg.Schema tableSchema = schemaForMode(TestFixtures.NESTED_SCHEMA, 1); Table simpleTable = warehouse - .buildTable(tableId, TestFixtures.NESTED_SCHEMA) + .buildTable(tableId, tableSchema) .withProperties(tableProperties) .withPartitionSpec(PartitionSpec.unpartitioned()) .create(); @@ -625,7 +664,7 @@ public void testNameMappingScan() throws Exception { .withMetrics(metrics) .build(); - final Schema beamSchema = icebergSchemaToBeamSchema(TestFixtures.NESTED_SCHEMA); + final Schema beamSchema = icebergSchemaToBeamSchema(simpleTable.schema()); simpleTable.newFastAppend().appendFile(dataFile).commit(); @@ -637,7 +676,7 @@ public void testNameMappingScan() throws Exception { final Row[] expectedRows = recordData.stream() - .map(data -> icebergGenericRecord(TestFixtures.NESTED_SCHEMA.asStruct(), data)) + .map(data -> icebergGenericRecord(simpleTable.schema().asStruct(), data)) .map(record -> IcebergUtils.icebergRecordToBeamRow(beamSchema, record)) .toArray(Row[]::new); @@ -695,8 +734,8 @@ public void runWithStartingStrategy(@Nullable StartingStrategy strategy, boolean throws IOException { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); if ((strategy == StartingStrategy.LATEST) || (streaming && strategy == null)) { @@ -731,8 +770,8 @@ public void runReadWithBoundary(boolean useSnapshotBoundary, boolean streaming) throws IOException { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); // only read data committed in the second and third snapshots List> expectedRecords = warehouse.commitData(simpleTable).subList(3, 9); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java index a3217503564c..1319efa7229a 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java @@ -46,9 +46,14 @@ import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.InvalidProtocolBufferException; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.types.Types; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -77,6 +82,13 @@ public class IcebergSchemaTransformTranslationTest { .build(); private static final Map CONFIG_PROPERTIES = ImmutableMap.builder().put("key", "value").put("key2", "value2").build(); + private static final org.apache.iceberg.Schema CDC_TRANSLATION_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get()), + Types.NestedField.required(3, "event_micros", Types.LongType.get())), + ImmutableSet.of(1)); private static final Row WRITE_CONFIG_ROW = Row.withSchema(WRITE_PROVIDER.configurationSchema()) .withFieldValue("table", "test_table_identifier") @@ -104,6 +116,8 @@ public class IcebergSchemaTransformTranslationTest { .withFieldValue("to_timestamp", 456L) .withFieldValue("poll_interval_seconds", 123) .withFieldValue("streaming", true) + .withFieldValue("keep", ImmutableList.of("id", "event_micros")) + .withFieldValue("filter", "\"data\" = 'keep'") .build(); @Test @@ -269,7 +283,15 @@ public void testCdcReadTransformProtoTranslation() // First build a pipeline Pipeline p = Pipeline.create(); String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); - warehouse.createTable(TableIdentifier.parse(identifier), TestFixtures.SCHEMA); + Table table = warehouse.createTable(TableIdentifier.parse(identifier), CDC_TRANSLATION_SCHEMA); + table + .newFastAppend() + .appendFile( + warehouse.writeRecords( + "cdc-translation.parquet", + table.schema(), + Collections.singletonList(record(1L, "keep", 123L)))) + .commit(); Map properties = new HashMap<>(CATALOG_PROPERTIES); properties.put("warehouse", warehouse.location); @@ -278,6 +300,9 @@ public void testCdcReadTransformProtoTranslation() Row.fromRow(READ_CDC_CONFIG_ROW) .withFieldValue("table", identifier) .withFieldValue("catalog_properties", properties) + .withFieldValue("from_snapshot", table.currentSnapshot().snapshotId()) + .withFieldValue("to_snapshot", table.currentSnapshot().snapshotId()) + .withFieldValue("to_timestamp", null) .build(); IcebergCdcReadSchemaTransform readTransform = @@ -320,4 +345,10 @@ public void testCdcReadTransformProtoTranslation() assertEquals(transformConfigRow, readTransformFromSpec.getConfigurationRow()); } + + private static Record record(long id, String data, long eventMicros) { + return TestFixtures.createRecord( + CDC_TRANSLATION_SCHEMA, + ImmutableMap.of("id", id, "data", data, "event_micros", eventMicros)); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java index d80ec4f95310..740ede55811b 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java @@ -23,17 +23,25 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.MetadataColumns; import org.apache.iceberg.PartitionField; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.transforms.Days; import org.apache.iceberg.transforms.Hours; import org.apache.iceberg.transforms.Months; import org.apache.iceberg.transforms.Transform; +import org.apache.iceberg.types.Types; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Test; @@ -166,6 +174,66 @@ public void testAll() { assertEquals(expectedSpec, spec); } + @Test + public void testConstantsMapIncludesCdcMetadataAndIdentityConstants() throws Exception { + org.apache.iceberg.Schema icebergSchema = + new org.apache.iceberg.Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "category", Types.StringType.get())); + PartitionSpec spec = PartitionSpec.builderFor(icebergSchema).identity("category").build(); + DataFile file = + DataFiles.builder(spec) + .withFormat(FileFormat.PARQUET) + .withPath("file:///tmp/table/category=A/data.parquet") + .withPartitionPath("category=A") + .withFileSizeInBytes(100L) + .withRecordCount(2L) + .withFirstRowId(99L) + .build(); + setFileSequenceNumber(file, 42L); + + Map constants = PartitionUtils.constantsMap(spec, file, null); + + assertEquals(99L, constants.get(MetadataColumns.ROW_ID.fieldId())); + assertEquals(42L, constants.get(MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId())); + assertEquals(file.location(), constants.get(MetadataColumns.FILE_PATH.fieldId())); + assertEquals(file.specId(), constants.get(MetadataColumns.SPEC_ID.fieldId())); + assertEquals("A", constants.get(2)); + } + + @Test + public void testConstantsMapUsesExplicitSequenceNumberWhenFileSequenceIsUnavailable() { + org.apache.iceberg.Schema icebergSchema = + new org.apache.iceberg.Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "category", Types.StringType.get())); + PartitionSpec spec = PartitionSpec.builderFor(icebergSchema).identity("category").build(); + DataFile file = + DataFiles.builder(spec) + .withFormat(FileFormat.PARQUET) + .withPath("file:///tmp/table/category=B/data.parquet") + .withPartitionPath("category=B") + .withFileSizeInBytes(100L) + .withRecordCount(2L) + .build(); + + Map constants = PartitionUtils.constantsMap(spec, file, 123L); + + assertEquals(123L, constants.get(MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId())); + assertEquals("B", constants.get(2)); + } + + private static void setFileSequenceNumber(DataFile dataFile, long fileSequenceNumber) + throws Exception { + Method method = dataFile.getClass().getMethod("setFileSequenceNumber", Long.class); + method.setAccessible(true); + try { + method.invoke(dataFile, fileSequenceNumber); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + } + static class TestCase { private final String field; private @Nullable String name; diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java index 73a0fd19e893..df9c44b7b4f3 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java @@ -30,6 +30,7 @@ import java.util.Objects; import java.util.stream.Collectors; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Splitter; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; @@ -40,7 +41,6 @@ import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.data.Record; import org.apache.iceberg.io.CloseableIterable; -import org.apache.iceberg.parquet.ParquetReader; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.ClassRule; import org.junit.Rule; @@ -75,14 +75,25 @@ public void testCreateReader() throws IOException { .commit(); } + IcebergScanConfig scanConfig = + IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema())) + .build(); + int numFiles = 0; try (CloseableIterable iterable = simpleTable.newScan().planTasks()) { for (CombinedScanTask combinedScanTask : iterable) { for (FileScanTask fileScanTask : combinedScanTask.tasks()) { String fileName = Iterables.getLast(Splitter.on("/").split(fileScanTask.file().path())); List recordsRead = new ArrayList<>(); - try (ParquetReader reader = - ReadUtils.createReader(fileScanTask, simpleTable, simpleTable.schema())) { + try (CloseableIterable reader = + ReadUtils.createReader(fileScanTask, simpleTable, scanConfig)) { reader.forEach(recordsRead::add); } @@ -94,6 +105,36 @@ public void testCreateReader() throws IOException { assertEquals(data.size(), numFiles); } + @Test + public void testMaybeApplyFilterUsesRequiredSchemaWithFilterOnlyField() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); + + IcebergScanConfig scanConfig = + IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema())) + .setKeepFields(ImmutableList.of("data")) + .setFilterString("id = 2") + .build(); + + assertEquals(ImmutableList.of("data"), fieldNames(scanConfig.getProjectedSchema())); + assertEquals(ImmutableList.of("id", "data"), fieldNames(scanConfig.getRequiredSchema())); + + CloseableIterable filtered = + ReadUtils.maybeApplyFilter( + CloseableIterable.withNoopClose(TestFixtures.FILE1SNAPSHOT1), + scanConfig, + scanConfig.getRequiredSchema()); + + assertEquals(ImmutableList.of("falafel"), dataOf(filtered)); + } + @Test public void testSnapshotsBetween() throws IOException { TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); @@ -239,4 +280,16 @@ static TestCase of( return new TestCase(scanConfig, expectedSnapshotId, description); } } + + private static List fieldNames(org.apache.iceberg.Schema schema) { + return schema.columns().stream() + .map(org.apache.iceberg.types.Types.NestedField::name) + .collect(Collectors.toList()); + } + + private static List dataOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(record -> (String) record.getField("data")) + .collect(Collectors.toList()); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java index d4e7793718d8..5126822c06f6 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java @@ -61,6 +61,9 @@ public class SerializableDataFileTest { .add("nanValueCounts") .add("lowerBounds") .add("upperBounds") + .add("dataSequenceNumber") + .add("fileSequenceNumber") + .add("firstRowId") .build(); @Test diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFileTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFileTest.java new file mode 100644 index 000000000000..29ef30c97efb --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFileTest.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortOrder; +import org.apache.iceberg.types.Types; +import org.junit.Test; + +/** Tests for {@link SerializableDeleteFile}. */ +public class SerializableDeleteFileTest { + private static final org.apache.iceberg.Schema SCHEMA = + new org.apache.iceberg.Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "category", Types.StringType.get())); + private static final PartitionSpec SPEC = + PartitionSpec.builderFor(SCHEMA).identity("category").build(); + + @Test + public void testPositionDeleteRoundTripPreservesMetadataUsedByCdcReads() throws Exception { + Map columnSizes = new HashMap<>(); + columnSizes.put(1, 11L); + Map valueCounts = new HashMap<>(); + valueCounts.put(1, 3L); + Map nullValueCounts = new HashMap<>(); + nullValueCounts.put(1, 0L); + Map nanValueCounts = new HashMap<>(); + nanValueCounts.put(1, 0L); + Map lowerBounds = new HashMap<>(); + lowerBounds.put(1, ByteBuffer.wrap(new byte[] {0x01})); + Map upperBounds = new HashMap<>(); + upperBounds.put(1, ByteBuffer.wrap(new byte[] {0x05})); + Metrics metrics = + new Metrics( + 3L, + columnSizes, + valueCounts, + nullValueCounts, + nanValueCounts, + lowerBounds, + upperBounds); + DeleteFile deleteFile = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/deletes/category=A/pos.parquet") + .withFormat(FileFormat.PARQUET) + .withPartitionPath("category=A") + .withFileSizeInBytes(256L) + .withMetrics(metrics) + .withSplitOffsets(Arrays.asList(4L, 128L)) + .withEncryptionKeyMetadata(ByteBuffer.wrap(new byte[] {0x0A, 0x0B})) + .build(); + setSequenceNumbers(deleteFile, 44L, 45L); + + SerializableDeleteFile serialized = SerializableDeleteFile.from(deleteFile, "category=A", true); + DeleteFile reconstructed = + serialized.createDeleteFile( + singletonMap(SPEC.specId(), SPEC), singletonMap(0, SortOrder.unsorted())); + + assertEquals(deleteFile.content(), reconstructed.content()); + assertEquals(deleteFile.location(), reconstructed.location()); + assertEquals(deleteFile.format(), reconstructed.format()); + assertEquals(deleteFile.recordCount(), reconstructed.recordCount()); + assertEquals(deleteFile.fileSizeInBytes(), reconstructed.fileSizeInBytes()); + assertEquals(deleteFile.partition(), reconstructed.partition()); + assertEquals(deleteFile.specId(), reconstructed.specId()); + assertEquals(deleteFile.keyMetadata(), reconstructed.keyMetadata()); + assertEquals(deleteFile.splitOffsets(), reconstructed.splitOffsets()); + assertEquals(deleteFile.columnSizes(), reconstructed.columnSizes()); + assertEquals(deleteFile.valueCounts(), reconstructed.valueCounts()); + assertEquals(deleteFile.nullValueCounts(), reconstructed.nullValueCounts()); + assertEquals(deleteFile.nanValueCounts(), reconstructed.nanValueCounts()); + assertEquals(deleteFile.lowerBounds(), reconstructed.lowerBounds()); + assertEquals(deleteFile.upperBounds(), reconstructed.upperBounds()); + assertEquals(Long.valueOf(44L), serialized.getDataSequenceNumber()); + assertEquals(Long.valueOf(45L), serialized.getFileSequenceNumber()); + assertNull(reconstructed.dataSequenceNumber()); + assertNull(reconstructed.fileSequenceNumber()); + } + + @Test + public void testEqualityDeleteRoundTripPreservesFieldIdsAndSortOrder() { + SortOrder sortOrder = SortOrder.builderFor(SCHEMA).asc("id").withOrderId(7).build(); + DeleteFile deleteFile = + FileMetadata.deleteFileBuilder(SPEC) + .ofEqualityDeletes(1, 2) + .withSortOrder(sortOrder) + .withPath("gs://bucket/deletes/category=A/eq.parquet") + .withFormat(FileFormat.PARQUET) + .withPartitionPath("category=A") + .withFileSizeInBytes(256L) + .withRecordCount(2L) + .build(); + + SerializableDeleteFile serialized = SerializableDeleteFile.from(deleteFile, "category=A", true); + DeleteFile reconstructed = + serialized.createDeleteFile(singletonMap(SPEC.specId(), SPEC), singletonMap(7, sortOrder)); + + assertEquals(FileContent.EQUALITY_DELETES, reconstructed.content()); + assertEquals(Arrays.asList(1, 2), reconstructed.equalityFieldIds()); + assertEquals(Integer.valueOf(7), reconstructed.sortOrderId()); + } + + @Test + public void testPuffinDeleteRoundTripPreservesDeletionVectorMetadata() { + DeleteFile deleteFile = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/deletes/category=A/dv.puffin") + .withFormat(FileFormat.PUFFIN) + .withPartitionPath("category=A") + .withFileSizeInBytes(512L) + .withRecordCount(1L) + .withContentOffset(64L) + .withContentSizeInBytes(128L) + .withReferencedDataFile("gs://bucket/data/category=A/data.parquet") + .build(); + + SerializableDeleteFile serialized = SerializableDeleteFile.from(deleteFile, "category=A", true); + DeleteFile reconstructed = + serialized.createDeleteFile( + singletonMap(SPEC.specId(), SPEC), singletonMap(0, SortOrder.unsorted())); + + assertEquals(FileFormat.PUFFIN, reconstructed.format()); + assertEquals(Long.valueOf(64L), reconstructed.contentOffset()); + assertEquals(Long.valueOf(128L), reconstructed.contentSizeInBytes()); + assertEquals("gs://bucket/data/category=A/data.parquet", reconstructed.referencedDataFile()); + } + + @Test + public void testCreateDeleteFileFailsClearlyForMissingPartitionSpec() { + DeleteFile deleteFile = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/deletes/category=A/pos.parquet") + .withFormat(FileFormat.PARQUET) + .withPartitionPath("category=A") + .withFileSizeInBytes(256L) + .withRecordCount(2L) + .build(); + SerializableDeleteFile serialized = SerializableDeleteFile.from(deleteFile, "category=A", true); + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, () -> serialized.createDeleteFile(emptyMap(), null)); + + assertTrue(thrown.getMessage().contains("created with spec id '" + SPEC.specId() + "'")); + } + + @Test + public void testCreateEqualityDeleteFileFailsClearlyForMissingSortOrder() { + SortOrder sortOrder = SortOrder.builderFor(SCHEMA).asc("id").withOrderId(7).build(); + DeleteFile deleteFile = + FileMetadata.deleteFileBuilder(SPEC) + .ofEqualityDeletes(1) + .withSortOrder(sortOrder) + .withPath("gs://bucket/deletes/category=A/eq.parquet") + .withFormat(FileFormat.PARQUET) + .withPartitionPath("category=A") + .withFileSizeInBytes(256L) + .withRecordCount(2L) + .build(); + SerializableDeleteFile serialized = SerializableDeleteFile.from(deleteFile, "category=A", true); + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, + () -> serialized.createDeleteFile(singletonMap(SPEC.specId(), SPEC), emptyMap())); + + assertTrue(thrown.getMessage().contains("sort order id '7'")); + } + + private static void setSequenceNumbers( + DeleteFile deleteFile, long dataSequenceNumber, long fileSequenceNumber) throws Exception { + invoke(deleteFile, "setDataSequenceNumber", dataSequenceNumber); + invoke(deleteFile, "setFileSequenceNumber", fileSequenceNumber); + } + + private static void invoke(DeleteFile deleteFile, String methodName, Long value) + throws Exception { + Method method = deleteFile.getClass().getMethod(methodName, Long.class); + method.setAccessible(true); + try { + method.invoke(deleteFile, value); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TestDataWarehouse.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TestDataWarehouse.java index dcb2d804d2e6..2e711219349c 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TestDataWarehouse.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/TestDataWarehouse.java @@ -64,7 +64,7 @@ public class TestDataWarehouse extends ExternalResource { protected final Configuration hadoopConf; - protected String location; + public String location; protected Catalog catalog; protected boolean someTableHasBeenCreated = false; diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java index fd24580e67c1..f0c7ae925df7 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java @@ -318,7 +318,8 @@ public Row apply(Long num) { }; protected static final org.apache.iceberg.Schema ICEBERG_SCHEMA = - beamSchemaToIcebergSchema(BEAM_SCHEMA); + new org.apache.iceberg.Schema( + beamSchemaToIcebergSchema(BEAM_SCHEMA).columns(), Collections.singleton(1)); protected static final SimpleFunction RECORD_FUNC = new SimpleFunction() { @Override @@ -450,7 +451,7 @@ public void testReadWithColumnPruning_keep() throws Exception { List expectedRows = populateTable(table); - List fieldsToKeep = Arrays.asList("row", "str", "modulo_5", "nullable_long"); + List fieldsToKeep = Arrays.asList("row", "modulo_5", "nullable_long"); RowFilter rowFilter = new RowFilter(BEAM_SCHEMA).keep(fieldsToKeep); Map config = new HashMap<>(managedIcebergConfig(tableId())); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java new file mode 100644 index 000000000000..546386073ff6 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.IcebergCatalogConfig; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.SerializableDeleteFile; +import org.apache.beam.sdk.io.iceberg.TestDataWarehouse; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.GenericAppenderFactory; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.deletes.EqualityDeleteWriter; +import org.apache.iceberg.deletes.PositionDelete; +import org.apache.iceberg.deletes.PositionDeleteWriter; +import org.apache.iceberg.encryption.EncryptedFiles; +import org.apache.iceberg.expressions.ExpressionParser; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.types.Types; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CdcReadUtils}. */ +@RunWith(JUnit4.class) +public class CdcReadUtilsTest { + private static final org.apache.iceberg.Schema CDC_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get())), + ImmutableSet.of(1)); + + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); + @Rule public TestName testName = new TestName(); + + @Test + public void addedRowsFiltersPositionAndEqualityDeletesWithUnprojectedEqualityColumn() + throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-added.parquet", + table.schema(), + ImmutableList.of( + record(0L, "keep-0"), + record(1L, "drop-by-pos"), + record(2L, "drop-by-data"), + record(3L, "keep-3"))); + + DeleteFile positionDelete = + writePositionDelete(table, dataFile, "cdc-added-pos-delete.parquet", 1L); + DeleteFile equalityDelete = + writeEqualityDelete(table, dataFile, "cdc-added-eq-delete.parquet", "drop-by-data"); + + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.ADDED_ROWS, + dataFile, + ImmutableList.of(positionDelete, equalityDelete), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(0L, 3L), idsOf(records)); + } + + @Test + public void addedRowsUsesFullTableSchemaWhenProjectionDisabled() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-added-full-schema.parquet", + table.schema(), + ImmutableList.of(record(0L, "non-projected-value"))); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.ADDED_ROWS, + dataFile, + ImmutableList.of(), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), false); + + // with projection disabled, all columns should be returned + assertEquals(ImmutableList.of("non-projected-value"), dataValuesOf(records)); + } + + @Test + public void deletedRowsExcludesRowsAlreadyHiddenByExistingDeletes() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-rows.parquet", + table.schema(), + ImmutableList.of( + record(0L, "already-hidden-by-position"), + record(1L, "new-position-delete"), + record(2L, "already-hidden-by-equality"), + record(3L, "new-equality-delete"), + record(4L, "still-live"))); + + DeleteFile existingPositionDelete = + writePositionDelete(table, dataFile, "cdc-existing-pos-delete.parquet", 0L); + DeleteFile existingEqualityDelete = + writeEqualityDelete( + table, dataFile, "cdc-existing-eq-delete.parquet", "already-hidden-by-equality"); + DeleteFile addedPositionDelete = + writePositionDelete(table, dataFile, "cdc-added-pos-delete.parquet", 1L); + DeleteFile addedEqualityDelete = + writeEqualityDelete(table, dataFile, "cdc-added-eq-delete.parquet", "new-equality-delete"); + + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_ROWS, + dataFile, + ImmutableList.of(addedPositionDelete, addedEqualityDelete), + ImmutableList.of(existingPositionDelete, existingEqualityDelete), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(1L, 3L), idsOf(records)); + } + + @Test + public void deletedRowsUsesFullTableSchemaWhenProjectionDisabled() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-rows-full-schema.parquet", + table.schema(), + ImmutableList.of(record(0L, "position-deleted-value"), record(1L, "still-live-value"))); + DeleteFile positionDelete = + writePositionDelete(table, dataFile, "cdc-deleted-rows-full-schema-pos-delete.parquet", 0L); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_ROWS, + dataFile, + ImmutableList.of(positionDelete), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), false); + + // with projection disabled, all columns should be returned + assertEquals(ImmutableList.of("position-deleted-value"), dataValuesOf(records)); + } + + @Test + public void deletedFileEmitsOnlyRowsNotAlreadyHiddenByExistingDeletes() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-file.parquet", + table.schema(), + ImmutableList.of( + record(0L, "already-hidden-by-position"), + record(1L, "still-live-one"), + record(2L, "already-hidden-by-equality"), + record(3L, "still-live-two"))); + + DeleteFile existingPositionDelete = + writePositionDelete(table, dataFile, "cdc-deleted-file-pos-delete.parquet", 0L); + DeleteFile existingEqualityDelete = + writeEqualityDelete( + table, dataFile, "cdc-deleted-file-eq-delete.parquet", "already-hidden-by-equality"); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_FILE, + dataFile, + ImmutableList.of(), + ImmutableList.of(existingPositionDelete, existingEqualityDelete), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(1L, 3L), idsOf(records)); + } + + @Test + public void deletedFileUsesFullTableSchemaWhenProjectionDisabled() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-file-full-schema.parquet", + table.schema(), + ImmutableList.of(record(0L, "deleted-file-value"))); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_FILE, + dataFile, + ImmutableList.of(), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), false); + + // with projection disabled, all columns should be returned + assertEquals(ImmutableList.of("deleted-file-value"), dataValuesOf(records)); + } + + @Test + public void deletedRowsHandlesNullEqualityDeletesWithoutPushdown() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-null-equality-delete.parquet", + table.schema(), + ImmutableList.of(record(0L, "live"), record(1L, null), record(2L, "also-live"))); + DeleteFile nullEqualityDelete = + writeEqualityDelete(table, dataFile, "cdc-null-eq-delete.parquet", null); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_ROWS, + dataFile, + ImmutableList.of(nullEqualityDelete), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(1L), idsOf(records)); + } + + private static Record record(long id, String data) { + GenericRecord record = GenericRecord.create(CDC_SCHEMA); + record.setField("id", id); + record.setField("data", data); + return record; + } + + private static DeleteFile writePositionDelete( + Table table, DataFile dataFile, String filename, long... positions) throws IOException { + GenericAppenderFactory appenderFactory = + new GenericAppenderFactory(table.schema(), table.spec()); + PositionDeleteWriter writer = + appenderFactory.newPosDeleteWriter( + EncryptedFiles.plainAsEncryptedOutput( + table.io().newOutputFile(dataFile.location() + "." + filename)), + FileFormat.PARQUET, + null); + try (writer) { + for (long position : positions) { + writer.write(PositionDelete.create().set(dataFile.location(), position)); + } + } + return writer.toDeleteFile(); + } + + private static DeleteFile writeEqualityDelete( + Table table, DataFile dataFile, String filename, @Nullable String data) throws IOException { + org.apache.iceberg.Schema deleteSchema = table.schema().select("data"); + GenericAppenderFactory appenderFactory = + new GenericAppenderFactory(table.schema(), table.spec(), new int[] {2}, deleteSchema, null); + EqualityDeleteWriter writer = + appenderFactory.newEqDeleteWriter( + EncryptedFiles.plainAsEncryptedOutput( + table.io().newOutputFile(dataFile.location() + "." + filename)), + FileFormat.PARQUET, + null); + try (writer) { + GenericRecord deleteRecord = GenericRecord.create(deleteSchema); + deleteRecord.setField("data", data); + writer.write(deleteRecord); + } + return writer.toDeleteFile(); + } + + private IcebergScanConfig scanConfig(Table table, TableIdentifier tableId) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setKeepFields(ImmutableList.of("id")) + .build(); + } + + private static SerializableChangelogTask task( + SerializableChangelogTask.Type type, + DataFile dataFile, + List addedDeletes, + List existingDeletes, + Table table) { + return SerializableChangelogTask.builder() + .setType(type) + .setDataFile(dataFile, table.spec().partitionToPath(dataFile.partition()), true) + .setAddedDeletes(serializableDeletes(addedDeletes, table)) + .setExistingDeletes(serializableDeletes(existingDeletes, table)) + .setSpecId(table.spec().specId()) + .setOperation( + type == SerializableChangelogTask.Type.ADDED_ROWS + ? ChangelogOperation.INSERT + : ChangelogOperation.DELETE) + .setOrdinal(0) + .setCommitSnapshotId(1L) + .setStart(0L) + .setLength(dataFile.fileSizeInBytes()) + .setJsonExpression(ExpressionParser.toJson(Expressions.alwaysTrue())) + .build(); + } + + private static List serializableDeletes( + List deletes, Table table) { + return deletes.stream() + .map( + delete -> + SerializableDeleteFile.from( + delete, table.spec().partitionToPath(delete.partition()), true)) + .collect(Collectors.toList()); + } + + private static List idsOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(record -> (Long) record.getField("id")) + .collect(Collectors.toList()); + } + + private static List dataValuesOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(record -> (String) record.getField("data")) + .collect(Collectors.toList()); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java new file mode 100644 index 000000000000..bf3aaf414579 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.InputFile; +import org.apache.iceberg.types.TypeUtil; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.StructLikeSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Verifies that {@link DeleteReader#read} returns the union of records matched by position + * and equality deletes. + * + *

      The tests stub the {@link DeleteLoader} so we exercise the predicate-composition logic + * directly without writing real delete files. End-to-end is covered by other tests. + */ +@RunWith(JUnit4.class) +public class DeleteReaderTest { + private static final Schema TABLE_SCHEMA = + new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.required(2, "name", Types.StringType.get())); + + private static final DeleteFile POS_FILE = + FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned()) + .ofPositionDeletes() + .withPath("/test/pos.parquet") + .withFileSizeInBytes(100) + .withRecordCount(3) + .build(); + + private static final DeleteFile EQ_FILE_ID = + FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned()) + .ofEqualityDeletes(1) + .withPath("/test/eq.parquet") + .withFileSizeInBytes(100) + .withRecordCount(2) + .build(); + + private static final DeleteFile EQ_FILE_NAME = + FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned()) + .ofEqualityDeletes(2) + .withPath("/test/eq-name.parquet") + .withFileSizeInBytes(100) + .withRecordCount(2) + .build(); + + /** {@link DeleteReader} that returns a stubbed {@link DeleteLoader} for tests. */ + private static class StubDeleteReader extends DeleteReader { + private final DeleteLoader stub; + + StubDeleteReader(List deletes, DeleteLoader stub) { + super( + "/test/data.parquet", + deletes, + TABLE_SCHEMA, + TABLE_SCHEMA, + true, + PreloadedDeletes.empty()); + this.stub = stub; + } + + StubDeleteReader( + List deletes, + DeleteLoader stub, + DeleteReader.PreloadedDeletes preloadedDeletes) { + super("/test/data.parquet", deletes, TABLE_SCHEMA, TABLE_SCHEMA, true, preloadedDeletes); + this.stub = stub; + } + + StubDeleteReader( + List deletes, + DeleteLoader stub, + Schema requestedSchema, + boolean needRowPosCol) { + super( + "/test/data.parquet", + deletes, + TABLE_SCHEMA, + requestedSchema, + needRowPosCol, + PreloadedDeletes.empty()); + this.stub = stub; + } + + @Override + protected StructLike asStructLike(Record record) { + return record; + } + + @Override + protected InputFile getInputFile(String location) { + throw new UnsupportedOperationException("not used with a stubbed DeleteLoader"); + } + + @Override + protected DeleteLoader newDeleteLoader() { + return stub; + } + } + + /** {@link DeleteLoader} that returns pre-built indexes. */ + private static class StubLoader implements DeleteLoader { + private final PositionDeleteIndex posIndex; + private final Map, StructLikeSet> eqSets; + private int posLoadCount = 0; + private int eqLoadCount = 0; + + StubLoader(PositionDeleteIndex posIndex, StructLikeSet eqSet) { + this(posIndex, Collections.singletonMap(Collections.singleton(1), eqSet)); + } + + StubLoader(PositionDeleteIndex posIndex, Map, StructLikeSet> eqSets) { + this.posIndex = posIndex; + this.eqSets = eqSets; + } + + @Override + public PositionDeleteIndex loadPositionDeletes(Iterable files, CharSequence path) { + posLoadCount++; + return posIndex; + } + + @Override + public StructLikeSet loadEqualityDeletes(Iterable files, Schema schema) { + eqLoadCount++; + return eqSets.getOrDefault( + Sets.newHashSet(TypeUtil.getProjectedIds(new Schema(schema.asStruct().fields()))), + StructLikeSet.create(schema.asStruct())); + } + } + + /** A minimal HashSet-backed {@link PositionDeleteIndex} for tests. */ + private static PositionDeleteIndex posIndexOf(long... positions) { + Set backing = new HashSet<>(); + for (long p : positions) { + backing.add(p); + } + return new PositionDeleteIndex() { + @Override + public void delete(long pos) { + backing.add(pos); + } + + @Override + public void delete(long from, long to) { + for (long p = from; p < to; p++) { + backing.add(p); + } + } + + @Override + public boolean isDeleted(long pos) { + return backing.contains(pos); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public long cardinality() { + return backing.size(); + } + }; + } + + private static StructLikeSet eqSetOfIds(int... ids) { + Schema idSchema = TABLE_SCHEMA.select("id"); + StructLikeSet set = StructLikeSet.create(idSchema.asStruct()); + for (int id : ids) { + GenericRecord r = GenericRecord.create(idSchema); + r.setField("id", id); + set.add(r); + } + return set; + } + + private static StructLikeSet eqSetOfNames(String... names) { + Schema nameSchema = TABLE_SCHEMA.select("name"); + StructLikeSet set = StructLikeSet.create(nameSchema.asStruct()); + for (String name : names) { + GenericRecord r = GenericRecord.create(nameSchema); + r.setField("name", name); + set.add(r); + } + return set; + } + + /** Builds N records (id=0..N-1, name="v0".."vN-1") matching {@code readSchema}. */ + private static List records(Schema readSchema, int n) { + boolean hasPos = readSchema.findField("_pos") != null; + List recs = new ArrayList<>(n); + for (long i = 0; i < n; i++) { + GenericRecord r = GenericRecord.create(readSchema); + r.setField("id", (int) i); + r.setField("name", "v" + i); + if (hasPos) { + r.setField("_pos", i); + } + recs.add(r); + } + return recs; + } + + /** Sorted list of "id" values from the output, for stable assertions. */ + private static List idsOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(r -> (Integer) r.getField("id")) + .sorted() + .collect(Collectors.toList()); + } + + /** With no delete files at all, {@code read()} emits nothing. */ + @Test + public void noDeletesEmitsNothing() { + DeleteLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + DeleteReader reader = new StubDeleteReader(Collections.emptyList(), loader); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(Collections.emptyList(), idsOf(output)); + } + + /** Pos-only emits only the pos-deleted records. */ + @Test + public void posOnlyEmitsPosDeletedRecords() { + DeleteLoader loader = new StubLoader(posIndexOf(1L, 3L), eqSetOfIds()); + DeleteReader reader = new StubDeleteReader(ImmutableList.of(POS_FILE), loader); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(1, 3), idsOf(output)); + } + + /** Only equality deletes, emits records matching the eq set. */ + @Test + public void eqOnlyEmitsEqDeletedRecords() { + DeleteLoader loader = new StubLoader(posIndexOf(), eqSetOfIds(2, 4)); + DeleteReader reader = new StubDeleteReader(ImmutableList.of(EQ_FILE_ID), loader); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(2, 4), idsOf(output)); + } + + /** Pos-deletes plus equality deletes, emit the union without duplication. */ + @Test + public void posAndEqEmitUnion() { + DeleteLoader loader = new StubLoader(posIndexOf(0L, 4L), eqSetOfIds(2, 4)); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(POS_FILE, EQ_FILE_ID), loader); + List input = records(reader.requiredSchema(), 6); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + // id 4 is in both sides; it must appear exactly once. + assertEquals(ImmutableList.of(0, 2, 4), idsOf(output)); + } + + /** Preloaded position deletes are reused instead of loading the same delete files again. */ + @Test + public void preloadedPositionDeletesAvoidSecondLoad() { + StubLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + PositionDeleteIndex preloadedPosIndex = posIndexOf(1L, 3L); + DeleteReader reader = + new StubDeleteReader( + ImmutableList.of(POS_FILE), + loader, + DeleteReader.PreloadedDeletes.of(preloadedPosIndex, Collections.emptyMap())); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(1, 3), idsOf(output)); + assertEquals(0, loader.posLoadCount); + } + + /** Preloaded equality delete sets are reused instead of loading the same delete files again. */ + @Test + public void preloadedEqualityDeletesAvoidSecondLoad() { + StubLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + Map, StructLikeSet> preloadedEqSets = new HashMap<>(); + preloadedEqSets.put(Collections.singleton(1), eqSetOfIds(2, 4)); + DeleteReader reader = + new StubDeleteReader( + ImmutableList.of(EQ_FILE_ID), + loader, + DeleteReader.PreloadedDeletes.of(null, preloadedEqSets)); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(2, 4), idsOf(output)); + assertEquals(0, loader.eqLoadCount); + } + + @Test + public void requiredSchemaAddsUnprojectedEqualityDeleteField() { + Schema requestedSchema = TABLE_SCHEMA.select("id"); + DeleteLoader loader = + new StubLoader( + posIndexOf(), Collections.singletonMap(Collections.singleton(2), eqSetOfNames("v2"))); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_NAME), loader, requestedSchema, true); + + assertEquals( + ImmutableList.of("id", "name"), + reader.requiredSchema().columns().stream() + .map(Types.NestedField::name) + .collect(Collectors.toList())); + + List input = records(reader.requiredSchema(), 4); + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(2), idsOf(output)); + } + + @Test + public void rowPositionColumnIsOnlyAddedWhenRequiredForPositionDeletes() { + DeleteLoader loader = new StubLoader(posIndexOf(1L), eqSetOfIds()); + + DeleteReader posReaderNeedsPos = + new StubDeleteReader(ImmutableList.of(POS_FILE), loader, TABLE_SCHEMA, true); + DeleteReader posReaderDoesNotNeedPos = + new StubDeleteReader(ImmutableList.of(POS_FILE), loader, TABLE_SCHEMA, false); + DeleteReader eqReader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_ID), loader, TABLE_SCHEMA, true); + + assertNotNull(posReaderNeedsPos.requiredSchema().findField("_pos")); + assertNull(posReaderDoesNotNeedPos.requiredSchema().findField("_pos")); + assertNull(eqReader.requiredSchema().findField("_pos")); + } + + @Test + public void multipleEqualityDeleteGroupsAreOrCombined() { + Map, StructLikeSet> eqSets = new HashMap<>(); + eqSets.put(Collections.singleton(1), eqSetOfIds(1)); + eqSets.put(Collections.singleton(2), eqSetOfNames("v3")); + StubLoader loader = new StubLoader(posIndexOf(), eqSets); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_ID, EQ_FILE_NAME), loader); + + CloseableIterable output = + reader.read(CloseableIterable.withNoopClose(records(reader.requiredSchema(), 5))); + + assertEquals(ImmutableList.of(1, 3), idsOf(output)); + assertEquals(2, loader.eqLoadCount); + } + + @Test + public void preloadedEqualityDeleteKeysAreDefensivelyCopied() { + StructLikeSet idDeletes = eqSetOfIds(2); + Set mutableKey = new HashSet<>(Collections.singleton(1)); + Map, StructLikeSet> preloadedEqSets = new HashMap<>(); + preloadedEqSets.put(mutableKey, idDeletes); + + DeleteReader.PreloadedDeletes preloadedDeletes = + DeleteReader.PreloadedDeletes.of(null, preloadedEqSets); + mutableKey.add(2); + + assertEquals(idDeletes, preloadedDeletes.equalityDeleteSet(Collections.singleton(1))); + + StubLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_ID), loader, preloadedDeletes); + CloseableIterable output = + reader.read(CloseableIterable.withNoopClose(records(reader.requiredSchema(), 4))); + + assertEquals(ImmutableList.of(2), idsOf(output)); + assertEquals(0, loader.eqLoadCount); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java new file mode 100644 index 000000000000..aa77d37af7af --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; + +import java.util.Collections; +import java.util.List; +import org.apache.beam.sdk.io.iceberg.SerializableDataFile; +import org.apache.beam.sdk.util.CoderUtils; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.iceberg.AddedRowsScanTask; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.ContentScanTask; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.DeletedDataFileScanTask; +import org.apache.iceberg.DeletedRowsScanTask; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.ExpressionParser; +import org.apache.iceberg.expressions.Expressions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link SerializableChangelogTask}. */ +@RunWith(JUnit4.class) +public class SerializableChangelogTaskTest { + private static final PartitionSpec SPEC = PartitionSpec.unpartitioned(); + private static final DataFile DATA_FILE = + DataFiles.builder(SPEC) + .withFormat(FileFormat.PARQUET) + .withPath("gs://bucket/data/file.parquet") + .withFileSizeInBytes(512L) + .withMetrics(new Metrics(3L, null, null, null, null, null, null)) + .build(); + private static final DeleteFile ADDED_DELETE = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/delete/added.parquet") + .withFileSizeInBytes(32L) + .withRecordCount(1L) + .build(); + private static final DeleteFile EXISTING_DELETE = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/delete/existing.parquet") + .withFileSizeInBytes(64L) + .withRecordCount(2L) + .build(); + + @Test + public void coderRoundTripPreservesTaskBasics() throws Exception { + SerializableChangelogTask task = + SerializableChangelogTask.builder() + .setType(SerializableChangelogTask.Type.ADDED_ROWS) + .setDataFile(SerializableDataFile.from(DATA_FILE, "", false)) + .setSpecId(SPEC.specId()) + .setOperation(ChangelogOperation.INSERT) + .setOrdinal(7) + .setCommitSnapshotId(123L) + .setStart(5L) + .setLength(99L) + .setJsonExpression(ExpressionParser.toJson(Expressions.alwaysTrue())) + .build(); + + SerializableChangelogTask decoded = CoderUtils.clone(SerializableChangelogTask.coder(), task); + + assertEquals(SerializableChangelogTask.Type.ADDED_ROWS, decoded.getType()); + assertEquals(ChangelogOperation.INSERT, decoded.getOperation()); + assertEquals(7, decoded.getOrdinal()); + assertEquals(123L, decoded.getCommitSnapshotId()); + assertEquals(5L, decoded.getStart()); + assertEquals(99L, decoded.getLength()); + assertEquals(DATA_FILE.location(), decoded.getDataFile().getPath()); + assertEquals(DATA_FILE.fileSizeInBytes(), decoded.getDataFile().getFileSizeInBytes()); + assertEquals(Collections.emptyList(), decoded.getExistingDeletes()); + assertEquals(Collections.emptyList(), decoded.getAddedDeletes()); + assertEquals(Expressions.alwaysTrue().toString(), decoded.getExpression(null).toString()); + } + + @Test + public void helperMethodsReadSupportedTaskTypes() { + FakeAddedRowsTask added = new FakeAddedRowsTask(ImmutableList.of(ADDED_DELETE)); + FakeDeletedRowsTask deletedRows = + new FakeDeletedRowsTask(ImmutableList.of(ADDED_DELETE), ImmutableList.of(EXISTING_DELETE)); + FakeDeletedDataFileTask deletedFile = + new FakeDeletedDataFileTask(ImmutableList.of(EXISTING_DELETE)); + + assertEquals( + SerializableChangelogTask.Type.ADDED_ROWS, SerializableChangelogTask.getType(added)); + assertEquals( + SerializableChangelogTask.Type.DELETED_ROWS, + SerializableChangelogTask.getType(deletedRows)); + assertEquals( + SerializableChangelogTask.Type.DELETED_FILE, + SerializableChangelogTask.getType(deletedFile)); + + assertEquals(22L, SerializableChangelogTask.getLength(added)); + assertEquals( + 44L, SerializableChangelogTask.getTotalLength(ImmutableList.of(added, deletedRows))); + assertSame(DATA_FILE, SerializableChangelogTask.getDataFile(deletedRows)); + assertSame(SPEC, SerializableChangelogTask.getSpec(deletedFile)); + assertSame(DATA_FILE.partition(), SerializableChangelogTask.getPartition(added)); + assertThat(SerializableChangelogTask.getAddedDeleteFiles(added), contains(ADDED_DELETE)); + assertThat(SerializableChangelogTask.getAddedDeleteFiles(deletedRows), contains(ADDED_DELETE)); + assertEquals( + Collections.emptyList(), SerializableChangelogTask.getAddedDeleteFiles(deletedFile)); + } + + @Test + public void unsupportedTaskTypeFailsClearly() { + ChangelogScanTask unsupported = + new ChangelogScanTask() { + @Override + public ChangelogOperation operation() { + return ChangelogOperation.INSERT; + } + + @Override + public int changeOrdinal() { + return 0; + } + + @Override + public long commitSnapshotId() { + return 0L; + } + }; + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, () -> SerializableChangelogTask.getLength(unsupported)); + + assertThat( + thrown.getMessage(), + containsString("Unknown ChangelogScanTask type: " + unsupported.getClass())); + } + + private abstract static class FakeContentTask + implements ChangelogScanTask, ContentScanTask { + @Override + public DataFile file() { + return DATA_FILE; + } + + @Override + public PartitionSpec spec() { + return SPEC; + } + + @Override + public StructLike partition() { + return DATA_FILE.partition(); + } + + @Override + public long start() { + return 11L; + } + + @Override + public long length() { + return 22L; + } + + @Override + public Expression residual() { + return Expressions.alwaysTrue(); + } + + @Override + public int changeOrdinal() { + return 2; + } + + @Override + public long commitSnapshotId() { + return 101L; + } + } + + private static class FakeAddedRowsTask extends FakeContentTask implements AddedRowsScanTask { + private final List deletes; + + FakeAddedRowsTask(List deletes) { + this.deletes = deletes; + } + + @Override + public List deletes() { + return deletes; + } + } + + private static class FakeDeletedRowsTask extends FakeContentTask implements DeletedRowsScanTask { + private final List addedDeletes; + private final List existingDeletes; + + FakeDeletedRowsTask(List addedDeletes, List existingDeletes) { + this.addedDeletes = addedDeletes; + this.existingDeletes = existingDeletes; + } + + @Override + public List addedDeletes() { + return addedDeletes; + } + + @Override + public List existingDeletes() { + return existingDeletes; + } + } + + private static class FakeDeletedDataFileTask extends FakeContentTask + implements DeletedDataFileScanTask { + private final List existingDeletes; + + FakeDeletedDataFileTask(List existingDeletes) { + this.existingDeletes = existingDeletes; + } + + @Override + public List existingDeletes() { + return existingDeletes; + } + } +} From fddd83c96a02f723cd45de7eb7c5028bd19d1b86 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ibrahim Date: Thu, 25 Jun 2026 20:58:44 +0300 Subject: [PATCH 476/490] Fix pip ResolutionTooDeep (#39017) * Fix pip ResolutionTooDeep * added constraints file * added dill * Add usage comment --- .../beam_CloudML_Benchmarks_Dataflow.json | 2 +- .../benchmarks/cloudml/constraints.txt | 42 +++++++++++++++++++ .../benchmarks/cloudml/requirements.txt | 17 ++++---- .../python/test-suites/dataflow/common.gradle | 10 ++--- 4 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt diff --git a/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json b/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json index 37dd25bf9029..5d04b2c0a8c7 100644 --- a/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json +++ b/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 3 + "modification": 5 } diff --git a/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt b/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt new file mode 100644 index 000000000000..b2f76d200850 --- /dev/null +++ b/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt @@ -0,0 +1,42 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Constraints file to pin versions and avoid pip ResolutionTooDeep. +# This file is used with: pip install -c constraints.txt -r requirements.txt + +# Core dependencies +tfx_bsl==1.15.1 +tensorflow-transform==1.15.0 + +# TensorFlow ecosystem +tensorflow==2.15.1 +tensorflow-metadata==1.15.0 +tf-keras==2.15.1 + +# NumPy and data handling +numpy==1.26.4 +pyarrow==10.0.1 + +# Google Cloud (pin to avoid transitive resolution) +google-cloud-aiplatform==1.60.0 +google-api-core==2.19.1 + +# Note: google-auth is NOT constrained - let pip resolve it to satisfy +# apache-beam's google-genai requirement (>=2.48.1) + +# Note: tensorflow-serving-api is NOT constrained - let pip resolve it within +# the range specified in requirements.txt (>=2.15,<2.16) diff --git a/sdks/python/apache_beam/testing/benchmarks/cloudml/requirements.txt b/sdks/python/apache_beam/testing/benchmarks/cloudml/requirements.txt index ab94ec5e9acf..5f754e27148e 100644 --- a/sdks/python/apache_beam/testing/benchmarks/cloudml/requirements.txt +++ b/sdks/python/apache_beam/testing/benchmarks/cloudml/requirements.txt @@ -15,12 +15,15 @@ # limitations under the License. # -dill==0.4.1 -tfx_bsl==1.16.1 -tensorflow-transform==1.16.0 -tensorflow>=2.16,<2.17 +# Core TFT dependencies with version bounds. +# Note: To avoid pip ResolutionTooDeep errors, always install using the constraints file: +# pip install -c constraints.txt -r requirements.txt +dill>=0.3,<0.5 +tfx_bsl>=1.15,<1.17 +tensorflow-transform>=1.15,<1.17 +tensorflow>=2.15,<2.16 numpy>=1.22.0,<2.0 -tensorflow-metadata>=1.16.1,<1.17.0 +tensorflow-metadata>=1.15,<1.16 pyarrow>=10,<11 -tensorflow-serving-api>=2.16.1,<2.20 -tf-keras>=2.16.0,<2.17 +tensorflow-serving-api>=2.15,<2.16 +tf-keras>=2.15,<2.16 diff --git a/sdks/python/test-suites/dataflow/common.gradle b/sdks/python/test-suites/dataflow/common.gradle index 7c84700e29fa..480e2a62a2ef 100644 --- a/sdks/python/test-suites/dataflow/common.gradle +++ b/sdks/python/test-suites/dataflow/common.gradle @@ -573,13 +573,9 @@ task installTFTRequirements { exec { workingDir "$rootProject.projectDir/sdks/python/apache_beam/testing/benchmarks/cloudml/" executable 'sh' - // installGcpTest already installed apache-beam[gcp]. tensorflow-transform also - // lists that dependency, so a plain pip install -r can re-resolve the GCP extra - // and hit ResolutionTooDeep. Install TFT with --no-deps instead. - args '-c', ". ${envdir}/bin/activate && " + - "grep -v '^tensorflow-transform' requirements.txt > /tmp/cloudml_tft_base_requirements.txt && " + - "pip install -r /tmp/cloudml_tft_base_requirements.txt && " + - "pip install --no-deps tensorflow-transform==1.16.0" + // Use constraints file to pin versions while allowing pip to + // resolve compatible versions within the specified ranges in requirements.txt + args '-c', ". ${envdir}/bin/activate && pip install -c constraints.txt -r requirements.txt" } } } From 75ae1e6d5ed62e294c9ae034fde4a5e36710e807 Mon Sep 17 00:00:00 2001 From: Elia Liu Date: Fri, 26 Jun 2026 04:47:06 +1000 Subject: [PATCH 477/490] [Python] Add UnboundedSource SDF wrapper (#19137) (#38724) * [Python] Add UnboundedSource SDF wrapper (#19137) Brings Java's ``UnboundedSource`` / ``UnboundedReader`` / ``CheckpointMark`` abstractions to the Python SDK as a Splittable-DoFn wrapper runnable on the portable Fn API (DirectRunner / FnApiRunner). Wires the new source type into ``iobase.Read.expand()`` so ``p | beam.io.Read(my_unbounded_source)`` dispatches alongside the existing ``BoundedSource`` branch. Loosely inspired by Java's ``Read.UnboundedSourceAsSDFWrapperFn``; the streaming-SDF template followed for the process loop / watermark / defer plumbing is ``apache_beam.transforms.periodicsequence``. --- sdks/python/apache_beam/io/iobase.py | 14 +- sdks/python/apache_beam/io/iobase_test.py | 49 +- .../python/apache_beam/io/unbounded_source.py | 996 ++++++++++++ .../apache_beam/io/unbounded_source_test.py | 1380 +++++++++++++++++ 4 files changed, 2434 insertions(+), 5 deletions(-) create mode 100644 sdks/python/apache_beam/io/unbounded_source.py create mode 100644 sdks/python/apache_beam/io/unbounded_source_test.py diff --git a/sdks/python/apache_beam/io/iobase.py b/sdks/python/apache_beam/io/iobase.py index afc977406af0..aa03280050fa 100644 --- a/sdks/python/apache_beam/io/iobase.py +++ b/sdks/python/apache_beam/io/iobase.py @@ -918,7 +918,10 @@ def __init__(self, source: SourceBase) -> None: """Initializes a Read transform. Args: - source: Data source to read from. + source: the data source to read from. May be a ``BoundedSource``, an + ``UnboundedSource``, or a ``PTransform`` (which is applied directly). + For any other source ``Read`` is treated as a primitive and relayed to + the runner implementation. """ super().__init__() self.source = source @@ -944,6 +947,11 @@ def expand(self, pbegin): | 'EmitSource' >> core.Map(lambda _: self.source).with_output_types(BoundedSource) | SDFBoundedSourceReader(display_data)) + # Local import to avoid a circular dependency. + from apache_beam.io.unbounded_source import ReadFromUnboundedSource + from apache_beam.io.unbounded_source import UnboundedSource + if isinstance(self.source, UnboundedSource): + return pbegin | ReadFromUnboundedSource(self.source) elif isinstance(self.source, ptransform.PTransform): # The Read transform can also admit a full PTransform as an input # rather than an anctual source. If the input is a PTransform, then @@ -993,6 +1001,10 @@ def to_runner_api_parameter( is_bounded=beam_runner_api_pb2.IsBounded.BOUNDED if self.source.is_bounded() else beam_runner_api_pb2.IsBounded.UNBOUNDED)) + # Local import to avoid a circular dependency. + from apache_beam.io.unbounded_source import UnboundedSource + if isinstance(self.source, UnboundedSource): + return super().to_runner_api_parameter(context) elif isinstance(self.source, ptransform.PTransform): return self.source.to_runner_api_parameter(context) raise NotImplementedError( diff --git a/sdks/python/apache_beam/io/iobase_test.py b/sdks/python/apache_beam/io/iobase_test.py index c3b0be8e781d..3e3b8d50dab5 100644 --- a/sdks/python/apache_beam/io/iobase_test.py +++ b/sdks/python/apache_beam/io/iobase_test.py @@ -21,15 +21,16 @@ import unittest -import mock - import apache_beam as beam -from apache_beam.io.concat_source import ConcatSource -from apache_beam.io.concat_source_test import RangeSource +import mock from apache_beam.io import iobase from apache_beam.io import range_trackers +from apache_beam.io.concat_source import ConcatSource +from apache_beam.io.concat_source_test import RangeSource from apache_beam.io.iobase import SourceBundle from apache_beam.options.pipeline_options import DebugOptions +from apache_beam.portability import common_urns +from apache_beam.portability import python_urns from apache_beam.testing.util import assert_that from apache_beam.testing.util import equal_to @@ -213,5 +214,45 @@ def test_sdf_wrap_range_source(self): self._run_sdf_wrapper_pipeline(RangeSource(0, 4), [0, 1, 2, 3]) +class UseSdfUnboundedSourcesTests(unittest.TestCase): + """Covers the UnboundedSource branch in + ``iobase.Read.expand()``. Uses ``UnboundedCountingSource`` from + ``unbounded_source_test`` as a finite fake source (no network). + """ + def test_read_end_to_end_unbounded(self): + from apache_beam.io.unbounded_source_test import UnboundedCountingSource + with beam.Pipeline() as p: + out = p | beam.io.Read(UnboundedCountingSource(5)) + assert_that(out, equal_to([0, 1, 2, 3, 4])) + + def test_read_unbounded_pcollection_is_unbounded(self): + from apache_beam.io.unbounded_source_test import UnboundedCountingSource + p = beam.Pipeline() + out = p | beam.io.Read(UnboundedCountingSource(3)) + self.assertFalse(out.is_bounded) + + def test_read_unbounded_serializes_as_expanded_composite(self): + from apache_beam.io.unbounded_source_test import UnboundedCountingSource + p = beam.Pipeline() + p | 'ReadIt' >> beam.io.Read(UnboundedCountingSource(3)) + + proto = p.to_runner_api(use_fake_coders=True) + transforms = proto.components.transforms.values() + deprecated_reads = [ + transform.unique_name for transform in transforms + if transform.spec.urn == common_urns.deprecated_primitives.READ.urn + ] + read_transforms = [ + transform for transform in proto.components.transforms.values() + if transform.unique_name == 'ReadIt' + ] + + self.assertEqual([], deprecated_reads) + self.assertEqual(1, len(read_transforms)) + self.assertEqual( + python_urns.GENERIC_COMPOSITE_TRANSFORM, read_transforms[0].spec.urn) + self.assertTrue(read_transforms[0].subtransforms) + + if __name__ == '__main__': unittest.main() diff --git a/sdks/python/apache_beam/io/unbounded_source.py b/sdks/python/apache_beam/io/unbounded_source.py new file mode 100644 index 000000000000..3246602252e3 --- /dev/null +++ b/sdks/python/apache_beam/io/unbounded_source.py @@ -0,0 +1,996 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Experimental ``UnboundedSource`` support for the Python SDK. + +``UnboundedSource`` support is currently experimental in the Python SDK; the +API may change in backwards-incompatible ways. + +An unbounded source reads an effectively infinite stream of records (message +queues, change-data-capture feeds, and similar) with checkpoint-based +pause/resume, watermark reporting, and bundle finalization. + +To define a source, implement :class:`UnboundedSource`, an +:class:`UnboundedReader`, and (when the reader has a resumable position) a +:class:`CheckpointMark`:: + + import apache_beam as beam + from apache_beam.io.unbounded_source import ( + CheckpointMark, UnboundedReader, UnboundedSource) + from apache_beam.utils.timestamp import MAX_TIMESTAMP + + class MyCheckpointMark(CheckpointMark): + def __init__(self, position): + self.position = position + + def finalize_checkpoint(self): + # Commit/acknowledge records up to ``position`` upstream, e.g. ack the + # consumed messages on a queue. + ... + + class MyReader(UnboundedReader): + def start(self): + # Position at the first record; return whether one is available. + ... + + def advance(self): + # Move to the next record; ``False`` means no data is available now. + ... + + def get_current(self): + ... + + def get_current_timestamp(self): + ... # event time of the current record + + def get_watermark(self): + # Lower bound on the timestamps of future records. Return + # ``MAX_TIMESTAMP`` to signal the reader has permanently finished. + ... + + def get_checkpoint_mark(self): + return MyCheckpointMark(...) + + class MySource(UnboundedSource): + def split(self, desired_num_splits, options=None): + # Return independent sub-sources, or ``[self]`` when not splittable. + return [self] + + def create_reader(self, options, checkpoint_mark): + # Build a reader; resume after ``checkpoint_mark`` when it is not None. + return MyReader(...) + + def get_checkpoint_mark_coder(self): + return ... # a Coder for MyCheckpointMark + +Read the source in a pipeline with :class:`apache_beam.io.Read`:: + + with beam.Pipeline() as p: + p | beam.io.Read(MySource()) | beam.Map(print) +""" + +import collections +import dataclasses +import logging +import threading +import time +from typing import Any +from typing import Callable +from typing import Iterable +from typing import Optional + +from apache_beam import coders +from apache_beam.coders.coders import BooleanCoder +from apache_beam.coders.coders import Coder +from apache_beam.coders.coders import NullableCoder +from apache_beam.coders.coders import TimestampCoder +from apache_beam.coders.coders import TupleCoder +from apache_beam.coders.coders import _MemoizingPickleCoder +from apache_beam.io import iobase +from apache_beam.io.watermark_estimators import ManualWatermarkEstimator +from apache_beam.runners import sdf_utils +from apache_beam.transforms import PTransform +from apache_beam.transforms import core +from apache_beam.transforms.window import TimestampedValue +from apache_beam.utils.timestamp import MAX_TIMESTAMP +from apache_beam.utils.timestamp import MIN_TIMESTAMP +from apache_beam.utils.timestamp import Duration +from apache_beam.utils.timestamp import Timestamp + +__all__ = [ + 'CheckpointMark', + 'UnboundedReader', + 'UnboundedSource', + 'ReadFromUnboundedSource', +] + +_LOGGER = logging.getLogger(__name__) + +# Sentinel used when a reader has no data available right now. +# This is distinct from end-of-stream. +_NO_DATA = object() + +_DEFAULT_POLL_INTERVAL_SECONDS = 1.0 +_DEFAULT_DESIRED_NUM_SPLITS = 20 +_DEFAULT_MAX_RECORDS_PER_BUNDLE = 10000 +_DEFAULT_MAX_READ_TIME_SECONDS = 10.0 +# A reader parked by a residual that never resumes is closed once idle this +# long; the cache also caps its entry count as a memory backstop. +_DEFAULT_READER_CACHE_IDLE_SECONDS = 60.0 +_DEFAULT_READER_CACHE_MAX_SIZE = 100 + +# Encodes a source to a structural cache key. Internally consistent across park +# and acquire; need not match the restriction wire coder. +_SOURCE_KEY_CODER = _MemoizingPickleCoder() + +# ------------------------------------------------------------------------------ +# Public abstract base classes. +# ------------------------------------------------------------------------------ + + +class CheckpointMark(object): + """A durable, serializable position in an :class:`UnboundedSource`. + + Produced by :meth:`UnboundedReader.get_checkpoint_mark`, encoded with + :meth:`UnboundedSource.get_checkpoint_mark_coder`, and used to resume a reader + (see :meth:`UnboundedSource.create_reader`). + """ + def finalize_checkpoint(self) -> None: + """Called once the runner has durably committed work up to this mark. + + Override to acknowledge/commit upstream (for example, ack the consumed + messages on a queue). The default is a no-op. + + The runner calls this at most once for a committed checkpoint mark. + Finalization is best effort; a mark may never be finalized. An exception + raised here is logged. On bundle retry an uncommitted mark may be re-cut + over an overlapping span, so this method must be idempotent (acknowledge by + absolute position). + """ + pass + + +class UnboundedReader(object): + """Reads records from an :class:`UnboundedSource`. + + Lifecycle: exactly one :meth:`start`, then any number of :meth:`advance` + calls; whenever one returns ``True`` the current record is available via + :meth:`get_current` / :meth:`get_current_timestamp`. A ``False`` return means + "no data available right now", which is distinct from end-of-stream: a reader + signals a permanent end by reporting a watermark of ``MAX_TIMESTAMP``. + """ + def start(self) -> bool: + """Positions at the first record; returns whether one is available.""" + raise NotImplementedError + + def advance(self) -> bool: + """Advances to the next record. ``False`` means no data is available now. + + Should not block. The wrapper enforces the per-bundle record and time caps + only between records, so a blocking ``start``/``advance`` can overrun the + time cap and stall the bundle. Return ``False`` when no data is currently + available instead of waiting. + """ + raise NotImplementedError + + def get_current(self) -> Any: + """Returns the record claimed by the last successful start/advance.""" + raise NotImplementedError + + def get_current_timestamp(self) -> Timestamp: + """Returns the event-time timestamp of the current record.""" + raise NotImplementedError + + def get_watermark(self) -> Timestamp: + """An approximate lower bound on timestamps of future records. + + Treated as monotonic by the wrapper. Return ``MAX_TIMESTAMP`` to signal that + this reader has permanently finished. + """ + raise NotImplementedError + + def get_checkpoint_mark(self) -> CheckpointMark: + """Returns a durable mark to resume from. Call only at a bundle boundary.""" + raise NotImplementedError + + def close(self) -> None: + """Releases reader resources. Default no-op.""" + pass + + +class UnboundedSource(iobase.SourceBase): + """A source producing an unbounded stream of records with checkpointing. + + Read it in a pipeline with :class:`apache_beam.io.Read`:: + + p | beam.io.Read(MyUnboundedSource()) + """ + def split(self, + desired_num_splits: int, + options: Optional[Any] = None) -> Iterable['UnboundedSource']: + """Splits into at most ``desired_num_splits`` independent sub-sources. + + Each returned sub-source must be independent and must not share mutable + state with siblings (the runner may execute them concurrently across + workers). Return ``[self]`` if the source cannot be split. Splitting is + performed once, before any checkpoint exists; once a reader has + checkpointed, the restriction is kept intact. + """ + raise NotImplementedError + + def create_reader( + self, options: Optional[Any], + checkpoint_mark: Optional[CheckpointMark]) -> UnboundedReader: + """Creates a reader, optionally resuming from ``checkpoint_mark``. + + Contract: + * When ``checkpoint_mark`` is ``None``, the returned reader's ``start()`` + produces the very first record of the source (or returns ``False`` if + none yet). + * When ``checkpoint_mark`` is not ``None``, the returned reader's + ``start()`` produces the first record strictly after the position + encoded by ``checkpoint_mark``. The reader must not re-deliver records + already covered by the prior bundle. + """ + raise NotImplementedError + + def get_checkpoint_mark_coder(self) -> Coder: + """Returns the coder for this source's :class:`CheckpointMark` instances. + + The SDK may call this while encoding or decoding source restrictions. + Implementations should be deterministic, side-effect free, and should not + perform I/O. + """ + raise NotImplementedError( + '%s must override get_checkpoint_mark_coder() to return a Coder for ' + 'its CheckpointMark subclass.' % type(self).__name__) + + def is_bounded(self) -> bool: + # SourceBase.is_bounded raises; an unbounded source is, by definition, not. + return False + + def default_output_coder(self) -> Coder: + # Permissive default; override for a tighter coder. + return coders.registry.get_coder(object) + + +# ------------------------------------------------------------------------------ +# SDF wrapper internals: a private implementation detail of +# ReadFromUnboundedSource. +# ------------------------------------------------------------------------------ + + +@dataclasses.dataclass(frozen=True) +class _UnboundedSourceRestriction(object): + """Durable SDF restriction describing where a reader should (re)start. + + Holds only serializable state -- never a live reader. ``is_done`` marks the + terminal (MAX-watermark) transition. ``finalization_checkpoint_mark`` is kept + separate from ``checkpoint_mark`` so a done primary can carry a commit hook + without polluting the resume state. + + Field roles: + * ``checkpoint_mark`` -- RESUME state. A reader rebuilt from this mark + must produce the first record strictly after it. + * ``finalization_checkpoint_mark`` -- COMMIT hook. Only set on a done + primary that was just cut this bundle. Registered with the runner's + bundle finalizer to acknowledge upstream. Independent of + ``checkpoint_mark`` so a residual's resume state can be ``None`` while + still recording what should be committed. + """ + source: UnboundedSource + checkpoint_mark: Optional[CheckpointMark] = None + watermark: Timestamp = MIN_TIMESTAMP + is_done: bool = False + finalization_checkpoint_mark: Optional[CheckpointMark] = None + + +class _UnboundedSourceRestrictionCoder(Coder): + """Encodes :class:`_UnboundedSourceRestriction` as a fixed 5-tuple. + + Stateless: at encode time the source's own + :meth:`UnboundedSource.get_checkpoint_mark_coder` is looked up from the + restriction; at decode time the source is decoded first and its coder + drives the checkpoint-mark decoding. This avoids passing source-specific + coder state into the coder's constructor, which in turn lets + :class:`_UnboundedSourceRestrictionProvider` and + :class:`_ReadFromUnboundedSourceDoFn` be module-level classes. + + Wire shape: source_bytes / checkpoint_bytes / watermark / done / + finalization_checkpoint_bytes -- the checkpoint and finalization bytes + are independently encoded with the (source-declared) checkpoint coder + wrapped in :class:`NullableCoder`. + """ + def __init__(self): + self._source_coder = _MemoizingPickleCoder() + self._bytes_coder = coders.BytesCoder() + self._tuple_coder = TupleCoder(( + self._bytes_coder, # source (pickled bytes) + self._bytes_coder, # checkpoint_mark (nullable-encoded bytes) + TimestampCoder(), # watermark + BooleanCoder(), # is_done + self._bytes_coder)) # finalization_checkpoint_mark (nullable-encoded) + + def _checkpoint_coder(self, source: UnboundedSource) -> Coder: + return NullableCoder(source.get_checkpoint_mark_coder()) + + def encode(self, restriction: '_UnboundedSourceRestriction') -> bytes: + source_bytes = self._source_coder.encode(restriction.source) + cp_coder = self._checkpoint_coder(restriction.source) + return self._tuple_coder.encode(( + source_bytes, + cp_coder.encode(restriction.checkpoint_mark), + restriction.watermark, + restriction.is_done, + cp_coder.encode(restriction.finalization_checkpoint_mark))) + + def decode(self, encoded: bytes) -> '_UnboundedSourceRestriction': + (source_bytes, checkpoint_bytes, watermark, is_done, + finalization_bytes) = self._tuple_coder.decode(encoded) + source = self._source_coder.decode(source_bytes) + cp_coder = self._checkpoint_coder(source) + return _UnboundedSourceRestriction( + source=source, + checkpoint_mark=cp_coder.decode(checkpoint_bytes), + watermark=watermark, + is_done=is_done, + finalization_checkpoint_mark=cp_coder.decode(finalization_bytes)) + + def is_deterministic(self) -> bool: + # Pickled source and checkpoint are not guaranteed deterministic. + return False + + +class _ReaderCache(object): + """Holds live readers between an SDF self-checkpoint and its resume. + + A fresh tracker is built for every bundle, so a reader parked at a + self-checkpoint would otherwise be closed and rebuilt from its checkpoint + mark on the next bundle. Parking it here lets the resuming bundle reclaim the + same started reader, keyed by the residual's structural ``(source, checkpoint + mark)``. ``acquire`` removes the entry, so two trackers never drive one + reader. + + A residual may be reassigned to another worker and never resume here; such a + reader is released once it falls idle past ``idle_seconds`` or when the entry + count exceeds ``max_size``, and the owning DoFn's teardown releases the rest. + One DoFn instance drives several trackers across threads, so access is locked. + """ + def __init__( + self, + idle_seconds: float = _DEFAULT_READER_CACHE_IDLE_SECONDS, + max_size: int = _DEFAULT_READER_CACHE_MAX_SIZE, + now: Optional[Callable[[], float]] = None): + self._idle_seconds = idle_seconds + self._max_size = max_size + self._now = now or time.monotonic + self._lock = threading.Lock() + # key -> (reader, started, parked_at); ordered oldest-first for eviction. + self._entries = collections.OrderedDict( + ) # type: collections.OrderedDict[Any, tuple[UnboundedReader, bool, float]] + + def acquire(self, key: Any) -> Optional[tuple['UnboundedReader', bool]]: + """Removes and returns ``(reader, started)`` for ``key``, or None.""" + with self._lock: + entry = self._entries.pop(key, None) + stale = self._evict_idle() + self._close_readers(stale) + if entry is None: + return None + return entry[0], entry[1] + + def park(self, key: Any, reader: 'UnboundedReader', started: bool) -> None: + """Stores ``reader`` under ``key`` for a later bundle to reclaim. A reader + already parked under ``key`` is closed.""" + with self._lock: + replaced = self._entries.pop(key, None) + self._entries[key] = (reader, started, self._now()) + stale = self._evict_idle() + while len(self._entries) > self._max_size: + _, oldest = self._entries.popitem(last=False) + stale.append(oldest[0]) + if replaced is not None and replaced[0] is not reader: + stale.append(replaced[0]) + self._close_readers(stale) + + def close_all(self) -> None: + """Closes every parked reader. Called from the owning DoFn's teardown.""" + with self._lock: + entries = list(self._entries.values()) + self._entries.clear() + self._close_readers(entry[0] for entry in entries) + + def _evict_idle(self) -> list: + # Caller holds the lock. Pops entries idle past the window (oldest first) + # and returns their readers for the caller to close after unlocking. + deadline = self._now() - self._idle_seconds + stale = [] + while self._entries: + key, entry = next(iter(self._entries.items())) + if entry[2] > deadline: + break + del self._entries[key] + stale.append(entry[0]) + return stale + + def _close_readers(self, readers) -> None: + for reader in readers: + try: + reader.close() + except Exception: # pylint: disable=broad-except + _LOGGER.warning('Error closing UnboundedReader', exc_info=True) + + +class _UnboundedSourceRestrictionTracker(iobase.RestrictionTracker): + """Drives an :class:`UnboundedReader` for one SDF restriction. + + Owns the live reader (lazily created, never serialized): both runner-initiated + ``defer_remainder`` self-checkpoints with ``try_split(0)``, which must + checkpoint the live reader. + + A self-checkpoint parks the reader in the DoFn's :class:`_ReaderCache` for the + next bundle to reclaim, keeping one started reader alive across bundles. The + DoFn injects ``_reader_cache`` at the start of ``process()``; with no cache + the tracker builds a fresh reader each bundle. + + ``process()`` only sees a ``RestrictionTrackerView``, which hides custom + methods and whose ``try_claim`` returns just a bool, so the freshly-read + record is handed back through a one-element holder list passed as the + ``try_claim`` *position* argument. + """ + def __init__( + self, + restriction: _UnboundedSourceRestriction, + options: Optional[Any] = None): + self._restriction = restriction + self._options = options + self._reader = None # type: Optional[UnboundedReader] + self._started = False + # True once a checkpoint has been cut this bundle (EOF or self-checkpoint). + self._checkpoint_taken = False + # Cross-bundle reader cache, injected by the DoFn; None disables caching. + self._reader_cache = None # type: Optional[_ReaderCache] + + def _ensure_reader(self) -> None: + if self._reader is not None: + return + cached = self._acquire_cached_reader() + if cached is not None: + # A parked reader is already started and positioned past its checkpoint. + self._reader, self._started = cached + return + self._reader = self._restriction.source.create_reader( + self._options, self._restriction.checkpoint_mark) + + def _cache_key(self, + restriction: _UnboundedSourceRestriction) -> Optional[Any]: + """Structural ``(source, checkpoint)`` key, or None when uncacheable. + + Built from the source pickle and the source's own checkpoint coder so a + parked reader and its resuming restriction map to the same entry. A None + key disables caching for that restriction; the resume then rebuilds from + the checkpoint mark under the source's ``create_reader`` contract. + """ + try: + source_bytes = _SOURCE_KEY_CODER.encode(restriction.source) + cp_coder = NullableCoder(restriction.source.get_checkpoint_mark_coder()) + return source_bytes, cp_coder.encode(restriction.checkpoint_mark) + except Exception: # pylint: disable=broad-except + return None + + def _acquire_cached_reader(self) -> Optional[tuple['UnboundedReader', bool]]: + if self._reader_cache is None: + return None + key = self._cache_key(self._restriction) + if key is None: + return None + return self._reader_cache.acquire(key) + + def _park_or_close_reader( + self, residual: _UnboundedSourceRestriction) -> None: + """Hands the live reader to the cache for ``residual`` to reclaim, or + closes it when no cache is available or the restriction is uncacheable.""" + if self._reader is None: + return + key = ( + self._cache_key(residual) if self._reader_cache is not None else None) + if key is None: + self._close_reader_if_open() + return + reader, self._reader = self._reader, None + self._reader_cache.park(key, reader, self._started) + + def _clone_checkpoint( + self, checkpoint: Optional[CheckpointMark]) -> Optional[CheckpointMark]: + """Returns an independent copy of a mark via the source's checkpoint coder. + + Used to keep the primary's finalize hook and the residual's resume state + from sharing one object, since a user ``finalize_checkpoint()`` may mutate + the mark. + """ + if checkpoint is None: + return None + coder = self._restriction.source.get_checkpoint_mark_coder() + return coder.decode(coder.encode(checkpoint)) + + def _close_reader_if_open(self) -> None: + """Idempotent reader release. Called by the EOF and split paths, and by + the DoFn's ``finally`` so an exception inside ``process()`` does not leak + sockets / file descriptors held by the live :class:`UnboundedReader`. + """ + if self._reader is None: + return + try: + self._reader.close() + except Exception: # pylint: disable=broad-except + _LOGGER.warning('Error closing UnboundedReader', exc_info=True) + finally: + self._reader = None + + def current_restriction(self) -> _UnboundedSourceRestriction: + return self._restriction + + def try_claim(self, out: list[Any]) -> bool: + """Advances the reader by one record. + + ``out[0]`` receives ``(value, record_timestamp, source_watermark)`` on the + has-data path, or the :data:`_NO_DATA` sentinel otherwise. The watermark is + the source's reported watermark, not the record's event time: the DoFn + advances the output watermark with the former and timestamps the record + with the latter. The argument doubles as the output holder (the + ``RestrictionTracker`` ABC treats it as a claim position), which the + threadsafe-tracker chain forwards opaquely. + """ + try: + return self._try_claim_inner(out) + except Exception: + # Reader state is now indeterminate; release it before re-raising. + self._close_reader_if_open() + raise + + def _try_claim_inner(self, out: list[Any]) -> bool: + if self._restriction.is_done: + out[0] = _NO_DATA + return False + self._ensure_reader() + if not self._started: + has_data = self._reader.start() + else: + has_data = self._reader.advance() + self._started = True + if has_data: + # Emit an available record before checking the watermark: a reader may + # report its last record and a MAX_TIMESTAMP watermark on the same call, + # and EOF is realized on the next data-less claim. + out[0] = ( + self._reader.get_current(), + self._reader.get_current_timestamp(), + self._reader.get_watermark()) + return True + watermark = self._reader.get_watermark() + if watermark >= MAX_TIMESTAMP: + # No data and watermark at MAX: cut a final checkpoint, close, mark done. + checkpoint = self._reader.get_checkpoint_mark() + self._close_reader_if_open() + self._restriction = dataclasses.replace( + self._restriction, + checkpoint_mark=None, # nothing left to resume from + watermark=MAX_TIMESTAMP, + is_done=True, + finalization_checkpoint_mark=checkpoint) + self._checkpoint_taken = True + out[0] = _NO_DATA + return False + # No data is available now. Refresh the watermark before deferring. + self._restriction = dataclasses.replace( + self._restriction, watermark=watermark) + out[0] = _NO_DATA + return True + + def try_split( + self, fraction_of_remainder + ) -> Optional[tuple[_UnboundedSourceRestriction, + _UnboundedSourceRestriction]]: + """Cuts a checkpoint, returning (primary, residual) or None. + + The cut checkpoint goes into ``primary.finalization_checkpoint_mark`` so + the DoFn can register a bundle-finalize callback for it. The same + checkpoint object also goes into ``residual.checkpoint_mark`` so the + resumed reader rebuilds at the correct position. The two fields are + independent on purpose (see :class:`_UnboundedSourceRestriction` + docstring): a runner that re-processes the primary alone must not see + a stale resume state, and a residual must not register finalize again + until ITS checkpoint is cut in a future bundle. + """ + try: + return self._try_split_inner(fraction_of_remainder) + except Exception: + self._close_reader_if_open() + raise + + def _try_split_inner(self, fraction_of_remainder): + # Only self-checkpoint (fraction 0) is supported; decline runner-initiated + # dynamic splits. + if fraction_of_remainder != 0: + return None + if self._reader is None or not self._started or self._restriction.is_done: + return None + checkpoint = self._reader.get_checkpoint_mark() + # The residual watermark is advisory; the SDF watermark estimator state is + # the authoritative cross-bundle watermark. + watermark = self._reader.get_watermark() + # Keep the two channels independent: the primary carries only the finalize + # hook, the residual only the resume state. The residual gets its own clone + # so a finalize_checkpoint() that mutates the primary's mark cannot corrupt + # the residual's resume position before the runner encodes it. + primary = dataclasses.replace( + self._restriction, + checkpoint_mark=None, + is_done=True, + finalization_checkpoint_mark=checkpoint) + residual = _UnboundedSourceRestriction( + source=self._restriction.source, + checkpoint_mark=self._clone_checkpoint(checkpoint), + watermark=watermark, + is_done=False, + finalization_checkpoint_mark=None) + self._restriction = primary + self._checkpoint_taken = True + # Park the reader so the resuming bundle reclaims it; on a cache miss the + # residual rebuilds one from its checkpoint mark. + self._park_or_close_reader(residual) + return primary, residual + + def check_done(self) -> bool: + # Called after every process(); must raise if work is left unaccounted for. + if self._restriction.is_done or self._checkpoint_taken: + return True + raise ValueError( + 'UnboundedSource restriction was neither finished nor checkpointed; ' + 'process() must self-checkpoint via defer_remainder() or run to EOF: ' + '%r' % (self._restriction, )) + + def current_progress(self) -> 'iobase.RestrictionProgress': + # Backlog-based progress is not implemented; report a coarse done/not-done + # signal via ``completed`` / ``remaining``. + if self._restriction.is_done: + return iobase.RestrictionProgress(completed=1.0, remaining=0.0) + return iobase.RestrictionProgress(completed=0.0, remaining=1.0) + + def is_bounded(self) -> bool: + return False + + +class _UnboundedSourceRestrictionProvider(core.RestrictionProvider): + """Wraps an :class:`UnboundedSource` element as an SDF restriction. + + Stateless module-level singleton (see :data:`_PROVIDER`): all + source-specific state (e.g. the source's checkpoint coder) is derived + per-call from the restriction's ``source`` field, which lets + :class:`_ReadFromUnboundedSourceDoFn` live at module level too. The provider + currently passes ``None`` for the ``options`` forwarded to + :meth:`UnboundedSource.split`. + """ + def __init__(self): + self._restriction_coder = _UnboundedSourceRestrictionCoder() + + def initial_restriction( + self, element: UnboundedSource) -> _UnboundedSourceRestriction: + if not isinstance(element, UnboundedSource): + raise TypeError( + 'ReadFromUnboundedSource expected an UnboundedSource element, got %r' + % (element, )) + return _UnboundedSourceRestriction(source=element) + + def create_tracker( + self, restriction: _UnboundedSourceRestriction + ) -> _UnboundedSourceRestrictionTracker: + return _UnboundedSourceRestrictionTracker(restriction) + + def split(self, element, + restriction) -> Iterable[_UnboundedSourceRestriction]: + if restriction.is_done or restriction.checkpoint_mark is not None: + yield restriction + return + + # ``source.split`` is user code and may refuse to split; fall back to a + # single restriction on error. + try: + split_sources = list( + restriction.source.split(_DEFAULT_DESIRED_NUM_SPLITS, None)) + except Exception: # pylint: disable=broad-except + _LOGGER.warning( + 'Exception while splitting UnboundedSource. Source not split.', + exc_info=True) + yield restriction + return + + if not split_sources: + yield restriction + return + + # A non-UnboundedSource split result is a contract violation, not a + # refusal, so fail loudly (outside the try/except above). + for split_source in split_sources: + if not isinstance(split_source, UnboundedSource): + raise TypeError( + 'UnboundedSource.split() produced %r, expected UnboundedSource' % + (split_source, )) + + for split_source in split_sources: + yield dataclasses.replace( + restriction, + source=split_source, + checkpoint_mark=None, + is_done=False, + finalization_checkpoint_mark=None) + + def restriction_size(self, element, restriction) -> int: + # TODO(https://github.com/apache/beam/issues/19137): implement backlog + # estimation. + return 1 + + def restriction_coder(self) -> Coder: + return self._restriction_coder + + def truncate(self, element, restriction): + # On drain, stop emitting new records. + return None + + +# Module-level stateless singleton, captured via ``RestrictionParam`` at the +# DoFn's class-definition time. +_PROVIDER = _UnboundedSourceRestrictionProvider() + + +class _FinalizeCheckpointOnce(object): + def __init__(self, checkpoint_mark: CheckpointMark): + self._checkpoint_mark = checkpoint_mark + # The lock keeps finalization idempotent if a runner ever invokes the + # callback more than once. + self._lock = threading.Lock() + self._finalized = False + + def __call__(self) -> None: + with self._lock: + if self._finalized: + return + self._finalized = True + # Finalization is best effort: log and swallow so a failing user override + # does not fail the bundle (matches CheckpointMark.finalize_checkpoint). + try: + self._checkpoint_mark.finalize_checkpoint() + except Exception: # pylint: disable=broad-except + _LOGGER.warning( + 'Error finalizing UnboundedSource checkpoint mark.', exc_info=True) + + +class _ReadFromUnboundedSourceDoFn(core.DoFn): + """SDF wrapper driving an :class:`UnboundedReader` for one restriction. + + Module-level so stdlib pickle and cloudpickle can serialise the DoFn. The + restriction provider is the module-level :data:`_PROVIDER` singleton. + """ + def __init__( + self, + poll_interval: float = _DEFAULT_POLL_INTERVAL_SECONDS, + max_records_per_bundle: int = _DEFAULT_MAX_RECORDS_PER_BUNDLE, + max_read_time_seconds: float = _DEFAULT_MAX_READ_TIME_SECONDS, + _now: Optional[Callable[[], float]] = None): + self._poll_interval = poll_interval + self._max_records_per_bundle = max_records_per_bundle + self._max_read_time_seconds = max_read_time_seconds + # Monotonic clock seam; tests inject a deterministic clock. + self._now = _now + # Per-worker reader cache; created in setup(), never serialized. + self._reader_cache = None # type: Optional[_ReaderCache] + + def setup(self): + self._reader_cache = _ReaderCache() + + def teardown(self): + if self._reader_cache is not None: + self._reader_cache.close_all() + self._reader_cache = None + + @core.DoFn.unbounded_per_element() + def process( + self, + unused_element, + bundle_finalizer=core.DoFn.BundleFinalizerParam, + tracker=core.DoFn.RestrictionParam(_PROVIDER), + watermark_estimator=core.DoFn.WatermarkEstimatorParam( + ManualWatermarkEstimator.default_provider())): + # Positional params (element, bundle finalizer) must precede the + # kwarg-injected ones (tracker, watermark estimator). + assert isinstance(tracker, sdf_utils.RestrictionTrackerView) + inner_tracker = _unwrap_tracker(tracker) + if inner_tracker is not None and self._reader_cache is not None: + # Let this bundle reclaim a reader parked by the prior bundle and re-park + # it on self-checkpoint. No cache means setup() was skipped. + inner_tracker._reader_cache = self._reader_cache + initial = tracker.current_restriction() + now = self._now or time.monotonic + records_emitted = 0 + # Armed on the first emitted record so reader startup is excluded. + read_deadline = None # type: Optional[float] + try: + while True: + holder = [None] + if not tracker.try_claim(holder): + # EOF: advance the estimator to the tracker's MAX watermark so + # downstream event-time windows can close. + _set_watermark_if_greater( + watermark_estimator, tracker.current_restriction().watermark) + break + record = holder[0] + if record is _NO_DATA: + # No data now: advance the watermark and self-checkpoint with the + # poll delay so an idle source backs off before resuming. + _set_watermark_if_greater( + watermark_estimator, tracker.current_restriction().watermark) + tracker.defer_remainder(Duration(seconds=self._poll_interval)) + break + # The third tuple field is the source watermark. The record timestamp + # remains the output event time. Emit the element before advancing the + # estimator so a reader that reports MAX on the same claim as its final + # record cannot push the output watermark past that record first. + value, record_timestamp, source_watermark = record + yield TimestampedValue(value, record_timestamp) + _set_watermark_if_greater(watermark_estimator, source_watermark) + records_emitted += 1 + if read_deadline is None: + read_deadline = now() + self._max_read_time_seconds + # A busy reader never hits the EOF or no-data branch. Bound the bundle + # by record count and elapsed time so the runner commits the checkpoint + # and runs finalization, then resume with no delay. The deadline is + # checked between records; a reader that blocks inside advance() can + # overrun it, so the record cap is the hard backstop. + reached_record_cap = records_emitted >= self._max_records_per_bundle + if reached_record_cap or now() >= read_deadline: + tracker.defer_remainder() + break + finally: + current = tracker.current_restriction() + try: + # Register finalization only when a checkpoint was cut this bundle. + # The SDK bundle finalizer applies no deadline, so finalization is + # unbounded best effort. + finalize_mark = current.finalization_checkpoint_mark + if current is not initial and finalize_mark is not None: + bundle_finalizer.register(_FinalizeCheckpointOnce(finalize_mark)) + finally: + # The EOF and self-checkpoint paths already closed or parked the + # reader, so this is a no-op there. It closes a reader still held when + # process() exits early, e.g. a downstream yield raised before any + # checkpoint. + if inner_tracker is not None: + inner_tracker._close_reader_if_open() + else: + _LOGGER.warning( + 'UnboundedSource DoFn could not close a reader because the SDF ' + 'tracker wrapper did not expose ' + '_UnboundedSourceRestrictionTracker (got %s). Reader resources ' + 'may remain open until garbage collection.', + type(tracker).__name__) + + +def _unwrap_tracker( + tracker: Any) -> Optional['_UnboundedSourceRestrictionTracker']: + """Returns the :class:`_UnboundedSourceRestrictionTracker` behind the SDF + view and threadsafe wrappers, or None when the chain is unexpected.""" + inner = tracker + if hasattr(inner, '_threadsafe_restriction_tracker'): + inner = inner._threadsafe_restriction_tracker + if hasattr(inner, '_restriction_tracker'): + inner = inner._restriction_tracker + if isinstance(inner, _UnboundedSourceRestrictionTracker): + return inner + return None + + +def _set_watermark_if_greater( + watermark_estimator, new_watermark: Timestamp) -> None: + # ManualWatermarkEstimator.set_watermark raises on regression, so only ever + # advance it (a regressing reader watermark is absorbed here). + current = watermark_estimator.current_watermark() + if current is None or new_watermark > current: + watermark_estimator.set_watermark(new_watermark) + + +class ReadFromUnboundedSource(PTransform): + """Reads an :class:`UnboundedSource`. + + Most users should prefer :class:`apache_beam.io.Read`, which dispatches an + ``UnboundedSource`` here automatically:: + + p | beam.io.Read(MyUnboundedSource()) + + Args: + source: the :class:`UnboundedSource` to read. + poll_interval: resume delay in seconds applied when the reader has no data, + which bounds how often an idle source is polled. Must be >= 0. + max_records_per_bundle: a busy reader self-checkpoints after emitting this + many records in one bundle. Must be >= 1. Defaults to 10000. + max_read_time_seconds: a busy reader self-checkpoints after this many + seconds in one bundle. Must be > 0. Defaults to 10.0. The deadline is + checked between records, so a reader that blocks inside ``advance()`` may + overrun it; ``max_records_per_bundle`` is the hard backstop. + + The bundle self-checkpoints as soon as either cap is reached. + """ + def __init__( + self, + source: UnboundedSource, + poll_interval: float = _DEFAULT_POLL_INTERVAL_SECONDS, + max_records_per_bundle: int = _DEFAULT_MAX_RECORDS_PER_BUNDLE, + max_read_time_seconds: float = _DEFAULT_MAX_READ_TIME_SECONDS): + if not isinstance(source, UnboundedSource): + raise TypeError('source must be an UnboundedSource, got %r' % (source, )) + if max_records_per_bundle < 1: + raise ValueError( + 'max_records_per_bundle must be >= 1, got %r' % + (max_records_per_bundle, )) + if max_read_time_seconds <= 0: + raise ValueError( + 'max_read_time_seconds must be > 0, got %r' % + (max_read_time_seconds, )) + if poll_interval < 0: + raise ValueError( + 'poll_interval must be >= 0, got %r' % (poll_interval, )) + super().__init__() + self._source = source + self._poll_interval = poll_interval + self._max_records_per_bundle = max_records_per_bundle + self._max_read_time_seconds = max_read_time_seconds + + def expand(self, pbegin): + source = self._source + output_coder = source.default_output_coder() + # The source is the SDF element used to derive the initial restriction. + # process() reads from the restriction, so it does not use the element + # directly. + output = ( + pbegin + | 'Create' >> core.Create([source]) + | 'ReadUnbounded' >> core.ParDo( + _ReadFromUnboundedSourceDoFn( + self._poll_interval, + self._max_records_per_bundle, + self._max_read_time_seconds))) + # Surface an element type only when the global registry already maps it to + # an equivalent coder. Avoid mutating ``coders.registry`` for a + # parameterized coder whose instance state would be lost. + try: + type_hint = output_coder.to_type_hint() + except NotImplementedError: + type_hint = None + if type_hint is not None: + try: + registered_coder = coders.registry.get_coder(type_hint) + except Exception: # pylint: disable=broad-except + _LOGGER.warning( + 'Could not look up the registered coder for element type %s.', + type_hint, + exc_info=True) + else: + if registered_coder == output_coder: + output.element_type = type_hint + return output + + def _infer_output_coder(self, input_type=None, input_coder=None): + return self._source.default_output_coder() diff --git a/sdks/python/apache_beam/io/unbounded_source_test.py b/sdks/python/apache_beam/io/unbounded_source_test.py new file mode 100644 index 000000000000..3b63d0d2de98 --- /dev/null +++ b/sdks/python/apache_beam/io/unbounded_source_test.py @@ -0,0 +1,1380 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Tests for apache_beam.io.unbounded_source. + +Semantics are covered by deterministic unit tests; the end-to-end DirectRunner +tests assert ordering and termination only (no flaky defer-timing assertions). +""" + +import logging +import unittest + +from typing_extensions import override + +import apache_beam as beam +from apache_beam import coders +from apache_beam.io import unbounded_source as _unbounded_source_module +from apache_beam.io.unbounded_source import _NO_DATA +from apache_beam.io.unbounded_source import CheckpointMark +from apache_beam.io.unbounded_source import ReadFromUnboundedSource +from apache_beam.io.unbounded_source import UnboundedReader +from apache_beam.io.unbounded_source import UnboundedSource +from apache_beam.io.unbounded_source import _FinalizeCheckpointOnce +from apache_beam.io.unbounded_source import _ReaderCache +from apache_beam.io.unbounded_source import _ReadFromUnboundedSourceDoFn +from apache_beam.io.unbounded_source import _set_watermark_if_greater +from apache_beam.io.unbounded_source import _UnboundedSourceRestriction +from apache_beam.io.unbounded_source import _UnboundedSourceRestrictionCoder +from apache_beam.io.unbounded_source import _UnboundedSourceRestrictionProvider +from apache_beam.io.unbounded_source import _UnboundedSourceRestrictionTracker +from apache_beam.io.watermark_estimators import ManualWatermarkEstimator +from apache_beam.runners import sdf_utils +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.util import assert_that +from apache_beam.testing.util import equal_to +from apache_beam.transforms import core +from apache_beam.transforms.window import FixedWindows +from apache_beam.utils.timestamp import MAX_TIMESTAMP +from apache_beam.utils.timestamp import MIN_TIMESTAMP +from apache_beam.utils.timestamp import Timestamp + +# pylint: disable=expression-not-assigned + +# Realistic event-time base away from the Unix epoch. +_EVENT_TIME_BASE = Timestamp(1729987200) # 2024-10-27T00:00:00Z + +# ------------------------------------------------------------------------------ +# In-memory demo source emitting integers 0..count-1 with event time +# ``_EVENT_TIME_BASE + index``. It self-terminates at EOF, resumes from +# ``last_index + 1``, and splits into even/odd sub-sources when configured. +# ------------------------------------------------------------------------------ + + +class _CountingCheckpointMark(CheckpointMark): + def __init__(self, last_index, finalize_log=None): + self.last_index = last_index + self._finalize_log = finalize_log + + @override + def finalize_checkpoint(self): + if self._finalize_log is not None: + self._finalize_log.append(self.last_index) + + def __eq__(self, other): + return ( + isinstance(other, _CountingCheckpointMark) and + other.last_index == self.last_index) + + def __hash__(self): + return hash(self.last_index) + + def __repr__(self): + return '_CountingCheckpointMark(last_index=%r)' % (self.last_index, ) + + +class _CountingReader(UnboundedReader): + def __init__( + self, count, start_index, finalize_log=None, modulus=1, residue=0): + self._count = count + self._next = start_index + self._modulus = modulus + self._residue = residue + self._current = None + self._exhausted = False + self._finalize_log = finalize_log + self.closed = False + + def _read_next(self): + while self._next < self._count: + index = self._next + self._next += 1 + if index % self._modulus == self._residue: + self._current = index + return True + self._exhausted = True + return False + + @override + def start(self): + return self._read_next() + + @override + def advance(self): + return self._read_next() + + @override + def get_current(self): + return self._current + + @override + def get_current_timestamp(self): + return _EVENT_TIME_BASE + self._current + + @override + def get_watermark(self): + if self._exhausted: + return MAX_TIMESTAMP + if self._current is None: + return MIN_TIMESTAMP + return _EVENT_TIME_BASE + self._current + + @override + def get_checkpoint_mark(self): + last = self._current if self._current is not None else self._next - 1 + return _CountingCheckpointMark(last, finalize_log=self._finalize_log) + + @override + def close(self): + self.closed = True + + +class UnboundedCountingSource(UnboundedSource): + def __init__( + self, + count, + finalize_log=None, + is_splittable=False, + modulus=1, + residue=0): + self._count = count + self._finalize_log = finalize_log + self._is_splittable = is_splittable + self._modulus = modulus + self._residue = residue + self.last_reader = None + + @override + def split(self, desired_num_splits, options=None): + if not self._is_splittable or desired_num_splits < 2: + return [self] + # Split into independent even/odd sub-sources (each non-splittable). + return [ + UnboundedCountingSource( + self._count, + finalize_log=self._finalize_log, + modulus=2, + residue=residue) for residue in (0, 1) + ] + + @override + def create_reader(self, options, checkpoint_mark): + start_index = ( + 0 if checkpoint_mark is None else checkpoint_mark.last_index + 1) + self.last_reader = _CountingReader( + self._count, + start_index, + finalize_log=self._finalize_log, + modulus=self._modulus, + residue=self._residue) + return self.last_reader + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + +class _StringCountingReader(_CountingReader): + @override + def get_current(self): + return 'v%s' % self._current + + +class _StringCountingSource(UnboundedCountingSource): + @override + def create_reader(self, options, checkpoint_mark): + start_index = ( + 0 if checkpoint_mark is None else checkpoint_mark.last_index + 1) + self.last_reader = _StringCountingReader( + self._count, start_index, finalize_log=self._finalize_log) + return self.last_reader + + @override + def default_output_coder(self): + return coders.StrUtf8Coder() + + +class _PrefixStrCoder(coders.Coder): + def __init__(self, prefix): + self._prefix = prefix + + @override + def encode(self, value): + if not value.startswith(self._prefix): + raise ValueError('expected %r prefix' % self._prefix) + return value[len(self._prefix):].encode('utf-8') + + @override + def decode(self, value): + return self._prefix + value.decode('utf-8') + + @override + def is_deterministic(self): + return True + + @override + def to_type_hint(self): + return str + + +class _PrefixStringReader(_StringCountingReader): + @override + def get_current(self): + return 'prefix:%s' % super().get_current() + + +class _PrefixStringSource(_StringCountingSource): + @override + def create_reader(self, options, checkpoint_mark): + start_index = ( + 0 if checkpoint_mark is None else checkpoint_mark.last_index + 1) + self.last_reader = _PrefixStringReader( + self._count, start_index, finalize_log=self._finalize_log) + return self.last_reader + + @override + def default_output_coder(self): + return _PrefixStrCoder('prefix:') + + +class _NoDataReader(UnboundedReader): + """Always reports temporary absence of data with watermark below MAX.""" + @override + def start(self): + return False + + @override + def advance(self): + return False + + @override + def get_current(self): + raise AssertionError('no data available') + + @override + def get_current_timestamp(self): + raise AssertionError('no data available') + + @override + def get_watermark(self): + return _EVENT_TIME_BASE + + @override + def get_checkpoint_mark(self): + return _CountingCheckpointMark(-1) + + +class _NoDataSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _NoDataReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + +class _MutatingCheckpointMark(CheckpointMark): + """A mark that mutates itself on finalize, to test primary/residual mark + isolation across a checkpoint cut.""" + def __init__(self, last_index): + self.last_index = last_index + + @override + def finalize_checkpoint(self): + self.last_index = -999 + + +class _MutatingReader(UnboundedReader): + def __init__(self): + self._index = -1 + + @override + def start(self): + self._index = 0 + return True + + @override + def advance(self): + self._index += 1 + return True + + @override + def get_current(self): + return self._index + + @override + def get_current_timestamp(self): + return _EVENT_TIME_BASE + self._index + + @override + def get_watermark(self): + return _EVENT_TIME_BASE + self._index + + @override + def get_checkpoint_mark(self): + return _MutatingCheckpointMark(self._index) + + +class _MutatingSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _MutatingReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + +class _MaxOnLastReader(UnboundedReader): + """Returns its only record with a MAX_TIMESTAMP watermark on the same claim, + then EOF on the next claim.""" + @override + def start(self): + return True + + @override + def advance(self): + return False + + @override + def get_current(self): + return 7 + + @override + def get_current_timestamp(self): + return _EVENT_TIME_BASE + + @override + def get_watermark(self): + return MAX_TIMESTAMP + + @override + def get_checkpoint_mark(self): + return _CountingCheckpointMark(0) + + +class _MaxOnLastSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _MaxOnLastReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + +def _new_tracker(source, checkpoint=None): + restriction = _UnboundedSourceRestriction( + source=source, checkpoint_mark=checkpoint) + return _UnboundedSourceRestrictionTracker(restriction) + + +def _claim(tracker): + """Claims once; returns (claimed_bool, holder_value).""" + holder = [None] + claimed = tracker.try_claim(holder) + return claimed, holder[0] + + +# ------------------------------------------------------------------------------ +# Tests +# ------------------------------------------------------------------------------ + + +class AbcContractTest(unittest.TestCase): + def test_checkpointmark_default_finalize_is_noop(self): + self.assertIsNone(CheckpointMark().finalize_checkpoint()) + + def test_unboundedsource_is_bounded_false(self): + self.assertFalse(UnboundedCountingSource(3).is_bounded()) + + def test_reader_lifecycle_start_advance_eof(self): + reader = UnboundedCountingSource(3).create_reader(None, None) + self.assertTrue(reader.start()) + self.assertEqual(reader.get_current(), 0) + self.assertEqual(reader.get_current_timestamp(), _EVENT_TIME_BASE) + self.assertTrue(reader.advance()) + self.assertEqual(reader.get_current(), 1) + self.assertTrue(reader.advance()) + self.assertEqual(reader.get_current(), 2) + self.assertFalse(reader.advance()) + self.assertEqual(reader.get_watermark(), MAX_TIMESTAMP) + + +class RestrictionCoderTest(unittest.TestCase): + def test_roundtrip_no_checkpoint(self): + source = UnboundedCountingSource(3) + coder = _UnboundedSourceRestrictionCoder() + decoded = coder.decode( + coder.encode(_UnboundedSourceRestriction(source=source))) + self.assertIsNone(decoded.checkpoint_mark) + self.assertEqual(decoded.watermark, MIN_TIMESTAMP) + self.assertFalse(decoded.is_done) + reader = decoded.source.create_reader(None, None) + self.assertTrue(reader.start()) + self.assertEqual(reader.get_current(), 0) + + def test_roundtrip_with_checkpoint_resumes(self): + source = UnboundedCountingSource(5) + coder = _UnboundedSourceRestrictionCoder() + restriction = _UnboundedSourceRestriction( + source=source, + checkpoint_mark=_CountingCheckpointMark(1), + watermark=Timestamp(1), + is_done=False) + decoded = coder.decode(coder.encode(restriction)) + self.assertEqual(decoded.checkpoint_mark.last_index, 1) + self.assertEqual(decoded.watermark, Timestamp(1)) + self.assertFalse(decoded.is_done) + # A reader built from the decoded checkpoint resumes at the next index. + reader = decoded.source.create_reader(None, decoded.checkpoint_mark) + self.assertTrue(reader.start()) + self.assertEqual(reader.get_current(), 2) + + +class RestrictionProviderTest(unittest.TestCase): + def test_initial_split_calls_source_split(self): + split_log = [] + + class _NamedSource(UnboundedCountingSource): + def __init__(self, name): + super().__init__(0) + self.name = name + + @override + def split(self, desired_num_splits, options=None): + split_log.append((desired_num_splits, options)) + return [_NamedSource('a'), _NamedSource('b')] + + source = _NamedSource('root') + provider = _UnboundedSourceRestrictionProvider() + restriction = _UnboundedSourceRestriction( + source=source, watermark=Timestamp(7)) + + splits = list(provider.split(source, restriction)) + + # The provider is a stateless module-level singleton, so it always + # passes ``None`` as the ``options`` argument to ``UnboundedSource.split``. + self.assertEqual(split_log, [(20, None)]) + self.assertEqual([split.source.name for split in splits], ['a', 'b']) + self.assertEqual([split.watermark for split in splits], [Timestamp(7)] * 2) + self.assertTrue(all(split.checkpoint_mark is None for split in splits)) + self.assertTrue( + all(split.finalization_checkpoint_mark is None for split in splits)) + + def test_initial_split_does_not_split_checkpointed_restriction(self): + split_log = [] + + class _SplitSource(UnboundedCountingSource): + @override + def split(self, desired_num_splits, options=None): + split_log.append((desired_num_splits, options)) + return [self] + + source = _SplitSource(5) + provider = _UnboundedSourceRestrictionProvider() + restriction = _UnboundedSourceRestriction( + source=source, checkpoint_mark=_CountingCheckpointMark(2)) + + self.assertEqual(list(provider.split(source, restriction)), [restriction]) + self.assertEqual(split_log, []) + + def test_initial_split_falls_back_to_original_on_split_error(self): + class _BoomSource(UnboundedCountingSource): + @override + def split(self, desired_num_splits, options=None): + raise RuntimeError('split boom') + + source = _BoomSource(5) + provider = _UnboundedSourceRestrictionProvider() + restriction = _UnboundedSourceRestriction(source=source) + + self.assertEqual(list(provider.split(source, restriction)), [restriction]) + + def test_truncate_returns_none_for_drain(self): + # On drain the SDF stops emitting; truncate yields no residual. + provider = _UnboundedSourceRestrictionProvider() + source = UnboundedCountingSource(5) + restriction = _UnboundedSourceRestriction(source=source) + self.assertIsNone(provider.truncate(source, restriction)) + + def test_splittable_source_partitions_into_independent_subsources(self): + # A splittable source fans out into two sub-sources; reading each in + # isolation yields the even and the odd integers, and their union is the + # full sequence with no overlap. + source = UnboundedCountingSource(6, is_splittable=True) + provider = _UnboundedSourceRestrictionProvider() + restriction = _UnboundedSourceRestriction(source=source) + + splits = list(provider.split(source, restriction)) + self.assertEqual(len(splits), 2) + + shards = [] + for split in splits: + tracker = _UnboundedSourceRestrictionTracker(split) + shard = [] + while True: + claimed, record = _claim(tracker) + if not claimed: + break + if record is not _NO_DATA: + shard.append(record[0]) + shards.append(shard) + self.assertEqual(sorted(shards), [[0, 2, 4], [1, 3, 5]]) + + +class RestrictionTrackerTest(unittest.TestCase): + def test_claim_emits_in_order(self): + tracker = _new_tracker(UnboundedCountingSource(3)) + values = [] + while True: + claimed, record = _claim(tracker) + if not claimed: + break + self.assertIsNot(record, _NO_DATA) + values.append(record[0]) + self.assertEqual(values, [0, 1, 2]) + self.assertTrue(tracker.check_done()) + + def test_claim_emits_final_record_when_watermark_is_max(self): + # A reader may return its last record with a MAX_TIMESTAMP watermark on the + # same call; the record must still be emitted (EOF comes on the next claim). + class _FinalRecordReader(UnboundedReader): + @override + def start(self): + return True + + @override + def advance(self): + return False + + @override + def get_current(self): + return 'last' + + @override + def get_current_timestamp(self): + return _EVENT_TIME_BASE + + @override + def get_watermark(self): + return MAX_TIMESTAMP + + @override + def get_checkpoint_mark(self): + return _CountingCheckpointMark(0) + + class _FinalSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _FinalRecordReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + tracker = _new_tracker(_FinalSource()) + claimed, record = _claim(tracker) + self.assertTrue(claimed) + self.assertIsNot(record, _NO_DATA) + self.assertEqual(record[0], 'last') + # The next claim observes EOF and finishes (no second, phantom record). + claimed_again, _ = _claim(tracker) + self.assertFalse(claimed_again) + self.assertTrue(tracker.check_done()) + + def test_try_split_zero_produces_resumable_residual(self): + source = UnboundedCountingSource(5) + tracker = _new_tracker(source) + # Claim 0 and 1. + self.assertEqual(_claim(tracker)[1][0], 0) + self.assertEqual(_claim(tracker)[1][0], 1) + + split = tracker.try_split(0) + self.assertIsNotNone(split) + primary, residual = split + self.assertTrue(primary.is_done) + self.assertFalse(residual.is_done) + # Resume / finalize channel separation: primary carries only the + # finalize hook, residual carries only the resume state. + self.assertIsNone(primary.checkpoint_mark) + self.assertIsNotNone(primary.finalization_checkpoint_mark) + self.assertEqual(primary.finalization_checkpoint_mark.last_index, 1) + self.assertEqual(residual.checkpoint_mark.last_index, 1) + self.assertIsNone(residual.finalization_checkpoint_mark) + # check_done passes on the (now done) primary. + self.assertTrue(tracker.check_done()) + + # Resuming from the residual continues at index 2. + resumed = _new_tracker(source, checkpoint=residual.checkpoint_mark) + self.assertEqual(_claim(resumed)[1][0], 2) + + def test_try_split_isolates_residual_from_finalize_mutation(self): + # The primary's finalize hook and the residual's resume state must not + # share one object, so a mutating finalize_checkpoint() cannot corrupt the + # residual's resume position. + tracker = _new_tracker(_MutatingSource()) + _claim(tracker) # start -> index 0 + split = tracker.try_split(0) + self.assertIsNotNone(split) + primary, residual = split + self.assertIsNot( + primary.finalization_checkpoint_mark, residual.checkpoint_mark) + self.assertEqual(residual.checkpoint_mark.last_index, 0) + primary.finalization_checkpoint_mark.finalize_checkpoint() + self.assertEqual(residual.checkpoint_mark.last_index, 0) + + def test_try_split_nonzero_declined(self): + source = UnboundedCountingSource(5) + tracker = _new_tracker(source) + self.assertEqual(_claim(tracker)[1][0], 0) + + self.assertIsNone(tracker.try_split(0.5)) + self.assertFalse(tracker.current_restriction().is_done) + self.assertIsNotNone(tracker._reader) + self.assertEqual(_claim(tracker)[1][0], 1) + + def test_no_data_returns_sentinel_without_finishing(self): + tracker = _new_tracker(_NoDataSource()) + claimed, record = _claim(tracker) + self.assertTrue(claimed) + self.assertIs(record, _NO_DATA) + # A self-checkpoint is still possible (poll/resume path). + self.assertIsNotNone(tracker.try_split(0)) + + def test_check_done_raises_when_not_done(self): + tracker = _new_tracker(UnboundedCountingSource(3)) + with self.assertRaises(ValueError): + tracker.check_done() + + def test_is_bounded_false(self): + self.assertFalse(_new_tracker(UnboundedCountingSource(3)).is_bounded()) + + +class _RecordingBundleFinalizer: + def __init__(self): + self.registered = [] + + def register(self, callback): + self.registered.append(callback) + + +class _ManualClock: + """A deterministic monotonic clock for the time-cap tests.""" + def __init__(self, now=0.0): + self.now = now + + def __call__(self): + return self.now + + +class BundleCapTest(unittest.TestCase): + """A busy reader self-checkpoints once the per-bundle record or time cap is + reached, so the runner can commit progress and run finalization.""" + def _bundle(self, dofn, source, checkpoint=None, estimator=None): + """Builds the SDF tracker chain and returns the process() generator plus the + tracker, threadsafe tracker, finalizer, and watermark estimator.""" + tracker = _UnboundedSourceRestrictionTracker( + _UnboundedSourceRestriction(source=source, checkpoint_mark=checkpoint)) + threadsafe = sdf_utils.ThreadsafeRestrictionTracker(tracker) + view = sdf_utils.RestrictionTrackerView(threadsafe) + finalizer = _RecordingBundleFinalizer() + estimator = estimator or ManualWatermarkEstimator(None) + gen = dofn.process( + None, + bundle_finalizer=finalizer, + tracker=view, + watermark_estimator=estimator) + return gen, tracker, threadsafe, finalizer, estimator + + def test_record_cap_checkpoints_busy_source(self): + finalize_log = [] + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, max_records_per_bundle=5, max_read_time_seconds=1e9) + # 1000 records is effectively unbounded against a cap of 5. + gen, tracker, threadsafe, finalizer, estimator = self._bundle( + dofn, UnboundedCountingSource(1000, finalize_log=finalize_log)) + outputs = list(gen) + + self.assertEqual([tv.value for tv in outputs], [0, 1, 2, 3, 4]) + self.assertTrue(tracker.current_restriction().is_done) + self.assertTrue(tracker.check_done()) + # The estimator holds the last emitted record's source watermark. + self.assertEqual(estimator.current_watermark(), _EVENT_TIME_BASE + 4) + # Residual resumes after the cut and carries no finalize hook. + residual, _ = threadsafe.deferred_status() + self.assertEqual(residual.checkpoint_mark.last_index, 4) + self.assertIsNone(residual.finalization_checkpoint_mark) + # Exactly one finalizer is registered; firing it commits the cut index once. + self.assertEqual(len(finalizer.registered), 1) + finalizer.registered[0]() + finalizer.registered[0]() + self.assertEqual(finalize_log, [4]) + + def test_time_cap_checkpoints_busy_source(self): + clock = _ManualClock(1000.0) + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, + max_records_per_bundle=10**9, + max_read_time_seconds=5.0, + _now=clock) + gen, tracker, threadsafe, _, _ = self._bundle( + dofn, UnboundedCountingSource(1000)) + + # The deadline arms at 1000 + 5 after the first record and is checked + # between records, so records keep flowing until the clock passes it. + self.assertEqual(next(gen).value, 0) + self.assertEqual(next(gen).value, 1) + self.assertEqual(next(gen).value, 2) + clock.now = 1006.0 + with self.assertRaises(StopIteration): + next(gen) + + self.assertTrue(tracker.current_restriction().is_done) + residual, _ = threadsafe.deferred_status() + self.assertEqual(residual.checkpoint_mark.last_index, 2) + + def test_cap_residual_resumes_in_next_bundle(self): + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, max_records_per_bundle=5, max_read_time_seconds=1e9) + source = UnboundedCountingSource(1000) + # Bundle 1 emits 0-4 and cuts a residual at index 4. + gen1, _, threadsafe1, _, _ = self._bundle(dofn, source) + self.assertEqual([tv.value for tv in gen1], [0, 1, 2, 3, 4]) + residual1, _ = threadsafe1.deferred_status() + + # Bundle 2 rebuilds the reader from the residual and emits 5-9. + gen2, _, threadsafe2, _, _ = self._bundle( + dofn, source, checkpoint=residual1.checkpoint_mark) + self.assertEqual([tv.value for tv in gen2], [5, 6, 7, 8, 9]) + residual2, _ = threadsafe2.deferred_status() + self.assertEqual(residual2.checkpoint_mark.last_index, 9) + + def test_busy_reader_is_reused_across_bundles(self): + # The self-checkpoint parks the reader; the resuming bundle reclaims the + # same started reader. + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, max_records_per_bundle=5, max_read_time_seconds=1e9) + dofn.setup() # creates the cross-bundle reader cache + source = UnboundedCountingSource(1000) + gen1, _, threadsafe1, _, _ = self._bundle(dofn, source) + self.assertEqual([tv.value for tv in gen1], [0, 1, 2, 3, 4]) + reader1 = source.last_reader + self.assertFalse(reader1.closed) # parked, not closed + residual1, _ = threadsafe1.deferred_status() + + gen2, _, _, _, _ = self._bundle( + dofn, source, checkpoint=residual1.checkpoint_mark) + self.assertEqual([tv.value for tv in gen2], [5, 6, 7, 8, 9]) + # No new reader was created; source.last_reader is unchanged. + self.assertIs(source.last_reader, reader1) + + def test_teardown_closes_parked_readers(self): + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, max_records_per_bundle=5, max_read_time_seconds=1e9) + dofn.setup() + source = UnboundedCountingSource(1000) + gen, _, _, _, _ = self._bundle(dofn, source) + list(gen) # the bundle parks its reader + reader = source.last_reader + self.assertFalse(reader.closed) + dofn.teardown() + self.assertTrue(reader.closed) + + def test_eof_exactly_at_cap_resumes_then_finishes(self): + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, max_records_per_bundle=5, max_read_time_seconds=1e9) + source = UnboundedCountingSource(5) # exactly cap records + # Bundle 1 hits the cap on the last record before observing EOF. + gen1, t1, threadsafe1, _, _ = self._bundle(dofn, source) + self.assertEqual([tv.value for tv in gen1], [0, 1, 2, 3, 4]) + self.assertTrue(t1.current_restriction().is_done) + residual1, _ = threadsafe1.deferred_status() + self.assertEqual(residual1.checkpoint_mark.last_index, 4) + + # Bundle 2 resumes at index 5, finds EOF, and finishes with no output. + gen2, t2, threadsafe2, _, _ = self._bundle( + dofn, source, checkpoint=residual1.checkpoint_mark) + self.assertEqual(list(gen2), []) + self.assertTrue(t2.current_restriction().is_done) + self.assertIsNone(threadsafe2.deferred_status()) + + def test_eof_before_cap_finishes_without_residual(self): + dofn = _ReadFromUnboundedSourceDoFn( + poll_interval=0, max_records_per_bundle=100, max_read_time_seconds=1e9) + gen, tracker, threadsafe, _, _ = self._bundle( + dofn, UnboundedCountingSource(3)) + + self.assertEqual([tv.value for tv in gen], [0, 1, 2]) + self.assertTrue(tracker.current_restriction().is_done) + self.assertIsNone(threadsafe.deferred_status()) + + def test_max_watermark_on_final_record_emitted_before_estimator_advances( + self): + # A reader that returns its final record with a MAX_TIMESTAMP watermark on + # the same claim must have that record emitted before the estimator reaches + # MAX, so the element is not stranded behind the output watermark. + dofn = _ReadFromUnboundedSourceDoFn(poll_interval=0) + gen, _, _, _, estimator = self._bundle(dofn, _MaxOnLastSource()) + + first = next(gen) + self.assertEqual(first.value, 7) + # The estimator has not yet been advanced to MAX when the record is yielded. + self.assertNotEqual(estimator.current_watermark(), MAX_TIMESTAMP) + # Draining the bundle then advances the estimator to MAX on EOF. + self.assertEqual(list(gen), []) + self.assertEqual(estimator.current_watermark(), MAX_TIMESTAMP) + + +class WatermarkTest(unittest.TestCase): + def test_set_watermark_is_monotonic(self): + estimator = ManualWatermarkEstimator(None) + _set_watermark_if_greater(estimator, Timestamp(5)) + self.assertEqual(estimator.current_watermark(), Timestamp(5)) + # A regression is ignored (would otherwise raise inside set_watermark). + _set_watermark_if_greater(estimator, Timestamp(3)) + self.assertEqual(estimator.current_watermark(), Timestamp(5)) + _set_watermark_if_greater(estimator, Timestamp(7)) + self.assertEqual(estimator.current_watermark(), Timestamp(7)) + + +class FinalizationTest(unittest.TestCase): + def test_finalize_checkpoint_callback_is_at_most_once(self): + finalize_log = [] + finalize_once = _FinalizeCheckpointOnce( + _CountingCheckpointMark(1, finalize_log=finalize_log)) + + finalize_once() + finalize_once() + + self.assertEqual(finalize_log, [1]) + + def test_finalize_checkpoint_invoked(self): + # Unit-level finalize test (the e2e finalize may run in a worker process); + # the hook lives on the primary, independent of the residual's resume state. + finalize_log = [] + source = UnboundedCountingSource(5, finalize_log=finalize_log) + tracker = _new_tracker(source) + _claim(tracker) # 0 + _claim(tracker) # 1 + primary, _ = tracker.try_split(0) + primary.finalization_checkpoint_mark.finalize_checkpoint() + self.assertEqual(finalize_log, [1]) + + +class EndToEndTest(unittest.TestCase): + def test_direct_runner_emits_all_in_order(self): + with TestPipeline() as p: + out = p | ReadFromUnboundedSource(UnboundedCountingSource(5)) + self.assertFalse(out.is_bounded) + assert_that(out, equal_to([0, 1, 2, 3, 4])) + + def test_eof_lets_event_time_window_fire(self): + # On EOF the DoFn advances the watermark estimator to MAX_TIMESTAMP so the + # downstream FixedWindow closes and the GroupByKey fires; otherwise the + # output would be empty. + with TestPipeline() as p: + out = ( + p + | ReadFromUnboundedSource(UnboundedCountingSource(5)) + | beam.WindowInto(FixedWindows(100)) + | beam.Map(lambda v: ('all', v)) + | beam.GroupByKey() + | beam.MapTuple(lambda _key, values: sorted(values))) + assert_that(out, equal_to([[0, 1, 2, 3, 4]])) + + def test_read_dispatches_through_iobase_read(self): + # ``beam.io.Read(source)`` must produce the same records as + # ``ReadFromUnboundedSource(source)``. + with TestPipeline() as p: + out = p | beam.io.Read(UnboundedCountingSource(5)) + self.assertFalse(out.is_bounded) + assert_that(out, equal_to([0, 1, 2, 3, 4])) + + def test_splittable_source_reads_all_records_across_splits(self): + # A splittable source fans out into even/odd sub-sources during initial + # SDF splitting; the union of all sub-source reads is the full sequence. + with TestPipeline() as p: + out = p | beam.io.Read(UnboundedCountingSource(6, is_splittable=True)) + assert_that(out, equal_to([0, 1, 2, 3, 4, 5])) + + def test_source_default_output_coder_sets_output_type(self): + with TestPipeline() as p: + out = p | ReadFromUnboundedSource(_StringCountingSource(2)) + self.assertEqual(out.element_type, str) + assert_that(out, equal_to(['v0', 'v1'])) + + def test_small_cap_self_checkpoints_and_resumes_to_eof(self): + # A small per-bundle cap forces several self-checkpoint/resume cycles + # through the runner before EOF, exercising the cross-bundle reader cache + # over real residual encode/decode. All records still arrive in order. + with TestPipeline() as p: + out = p | ReadFromUnboundedSource( + UnboundedCountingSource(20), max_records_per_bundle=5) + assert_that(out, equal_to(list(range(20)))) + + +class ReadFromUnboundedSourceCoderTest(unittest.TestCase): + def test_parameterized_output_coder_does_not_mutate_global_registry(self): + try: + p = beam.Pipeline() + out = p | ReadFromUnboundedSource(_PrefixStringSource(1)) + + self.assertNotEqual(out.element_type, str) + self.assertEqual(coders.registry.get_coder(str), coders.StrUtf8Coder()) + self.assertEqual( + ReadFromUnboundedSource(_PrefixStringSource(1))._infer_output_coder(), + _PrefixStrCoder('prefix:')) + finally: + coders.registry.register_coder(str, coders.StrUtf8Coder) + + +# ------------------------------------------------------------------------------ +# Reader lifecycle, watermark, and contract regression tests (reader close on +# every exit path, the NotImplementedError message, finalize idempotency). +# ------------------------------------------------------------------------------ + + +class ReaderCloseTest(unittest.TestCase): + """Reader lifecycle: close() must run on every tracker-driven exit path.""" + def test_tracker_closes_reader_on_eof(self): + source = UnboundedCountingSource(0) # immediately exhausted + tracker = _new_tracker(source) + holder = [None] + self.assertFalse(tracker.try_claim(holder)) + self.assertIsNone(tracker._reader) + self.assertTrue(source.last_reader.closed) + + def test_tracker_closes_reader_on_split_without_cache(self): + # With no cache injected, a self-checkpoint closes the reader. + source = UnboundedCountingSource(5) + tracker = _new_tracker(source) + _claim(tracker) # creates reader, claims 0 + reader = source.last_reader + self.assertFalse(reader.closed) + split = tracker.try_split(0) + self.assertIsNotNone(split) + self.assertIsNone(tracker._reader) + self.assertTrue(reader.closed) + + def test_tracker_parks_reader_on_split_with_cache(self): + # With a cache, a self-checkpoint parks the reader for the residual to + # reclaim. + source = UnboundedCountingSource(5) + cache = _ReaderCache() + tracker = _new_tracker(source) + tracker._reader_cache = cache + _claim(tracker) + reader = source.last_reader + _, residual = tracker.try_split(0) + self.assertIsNone(tracker._reader) + self.assertFalse(reader.closed) + # The residual's key reclaims the same started reader. + self.assertEqual( + cache.acquire(tracker._cache_key(residual)), (reader, True)) + + def test_close_helper_is_idempotent_and_safe_on_empty_tracker(self): + tracker = _new_tracker(UnboundedCountingSource(3)) + # No reader yet -- helper must be a no-op. + tracker._close_reader_if_open() + _claim(tracker) + reader = tracker._reader + tracker._close_reader_if_open() + self.assertTrue(reader.closed) + self.assertIsNone(tracker._reader) + # Second call is a no-op (no reader to close). + tracker._close_reader_if_open() + + def test_close_helper_swallows_reader_close_errors(self): + class _BoomReader(UnboundedReader): + @override + def start(self): + return True + + @override + def advance(self): + return False + + @override + def get_current(self): + return 'x' + + @override + def get_current_timestamp(self): + return _EVENT_TIME_BASE + + @override + def get_watermark(self): + return _EVENT_TIME_BASE + + @override + def get_checkpoint_mark(self): + return CheckpointMark() + + @override + def close(self): + raise RuntimeError('close blew up') + + class _BoomSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _BoomReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + tracker = _new_tracker(_BoomSource()) + _claim(tracker) + with self.assertLogs(_unbounded_source_module._LOGGER, 'WARNING') as logs: + tracker._close_reader_if_open() + self.assertTrue( + any('Error closing UnboundedReader' in line for line in logs.output)) + self.assertIsNone(tracker._reader) + + +class ReaderCacheTest(unittest.TestCase): + """The cache parks a reader under one key, hands it to the next acquirer, + bounds itself by idle time and entry count, and closes on teardown.""" + def _reader(self): + class _FakeReader(UnboundedReader): + def __init__(self): + self.closed = False + + @override + def close(self): + self.closed = True + + return _FakeReader() + + def test_park_then_acquire_returns_same_reader_and_started_flag(self): + cache = _ReaderCache() + reader = self._reader() + cache.park('k', reader, True) + self.assertEqual(cache.acquire('k'), (reader, True)) + # Acquire removes the entry so two trackers cannot share one reader. + self.assertIsNone(cache.acquire('k')) + self.assertFalse(reader.closed) + + def test_acquire_miss_returns_none(self): + self.assertIsNone(_ReaderCache().acquire('absent')) + + def test_park_replacing_same_key_closes_displaced_reader(self): + # Two parks under one key without an intervening acquire (e.g. a bundle + # retry) must not leak the first reader. + cache = _ReaderCache() + old, new = self._reader(), self._reader() + cache.park('k', old, True) + cache.park('k', new, True) + self.assertTrue(old.closed) + self.assertFalse(new.closed) + self.assertEqual(cache.acquire('k'), (new, True)) + + def test_idle_reader_is_closed_on_next_touch(self): + clock = _ManualClock(1000.0) + cache = _ReaderCache(idle_seconds=30.0, now=clock) + reader = self._reader() + cache.park('k', reader, True) + clock.now = 1031.0 # past the idle window + cache.park('other', self._reader(), False) # triggers idle eviction + self.assertTrue(reader.closed) + self.assertIsNone(cache.acquire('k')) + + def test_max_size_evicts_and_closes_oldest(self): + cache = _ReaderCache(max_size=2) + readers = [self._reader() for _ in range(3)] + cache.park('a', readers[0], True) + cache.park('b', readers[1], True) + cache.park('c', readers[2], True) # exceeds the cap, evicts 'a' + self.assertTrue(readers[0].closed) + self.assertIsNone(cache.acquire('a')) + self.assertIsNotNone(cache.acquire('b')) + self.assertIsNotNone(cache.acquire('c')) + + def test_close_all_closes_every_parked_reader(self): + cache = _ReaderCache() + readers = [self._reader() for _ in range(3)] + for i, reader in enumerate(readers): + cache.park(str(i), reader, True) + cache.close_all() + self.assertTrue(all(reader.closed for reader in readers)) + self.assertIsNone(cache.acquire('0')) + + def test_close_all_swallows_reader_close_errors(self): + class _BoomReader(UnboundedReader): + @override + def close(self): + raise RuntimeError('close blew up') + + cache = _ReaderCache() + cache.park('k', _BoomReader(), True) + with self.assertLogs(_unbounded_source_module._LOGGER, 'WARNING') as logs: + cache.close_all() + self.assertTrue( + any('Error closing UnboundedReader' in line for line in logs.output)) + + +class TrackerContractRegressionTest(unittest.TestCase): + """Tracker contract: source-watermark on the data path, finalize/resume + channel separation, and reader close on a reader-method failure.""" + def test_data_path_holder_carries_source_watermark(self): + class _LaggingReader(UnboundedReader): + @override + def start(self): + return True + + @override + def advance(self): + return False + + @override + def get_current(self): + return 'rec' + + @override + def get_current_timestamp(self): + return Timestamp(1000) # record event time + + @override + def get_watermark(self): + return Timestamp(990) # source watermark lags 10us behind + + @override + def get_checkpoint_mark(self): + return _CountingCheckpointMark(0) + + class _LaggingSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _LaggingReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + tracker = _new_tracker(_LaggingSource()) + claimed, record = _claim(tracker) + self.assertTrue(claimed) + self.assertIsNot(record, _NO_DATA) + value, record_timestamp, source_watermark = record + self.assertEqual(value, 'rec') + self.assertEqual(record_timestamp, Timestamp(1000)) + # Critical: watermark slot is the SOURCE watermark, NOT record timestamp. + self.assertEqual(source_watermark, Timestamp(990)) + self.assertNotEqual(source_watermark, record_timestamp) + + def test_split_separates_finalize_and_resume_channels(self): + source = UnboundedCountingSource(5) + tracker = _new_tracker(source) + _claim(tracker) # claim 0 so reader has progress + primary, residual = tracker.try_split(0) + # Primary carries ONLY the finalize hook -- no resume state. + self.assertIsNone(primary.checkpoint_mark) + self.assertIsNotNone(primary.finalization_checkpoint_mark) + self.assertTrue(primary.is_done) + # Residual carries ONLY the resume state -- no finalize hook (a future + # bundle that splits THIS residual will produce ITS own finalize mark). + self.assertIsNotNone(residual.checkpoint_mark) + self.assertIsNone(residual.finalization_checkpoint_mark) + self.assertFalse(residual.is_done) + # The two marks reference the same underlying checkpoint object. + self.assertEqual( + primary.finalization_checkpoint_mark.last_index, + residual.checkpoint_mark.last_index) + + def test_eof_populates_finalize_and_clears_resume(self): + # EOF transition: restriction.checkpoint_mark goes to None (no more + # records to resume from), finalization_checkpoint_mark carries the + # final commit hook. + source = UnboundedCountingSource(0) # immediately exhausted + tracker = _new_tracker(source) + holder = [None] + self.assertFalse(tracker.try_claim(holder)) + r = tracker.current_restriction() + self.assertTrue(r.is_done) + self.assertEqual(r.watermark, MAX_TIMESTAMP) + self.assertIsNone(r.checkpoint_mark) + self.assertIsNotNone(r.finalization_checkpoint_mark) + + def test_tracker_closes_reader_when_advance_raises(self): + # try_claim closes the reader before re-raising a reader-method failure, so + # the DoFn's finally need not traverse the SDF chain for these. + class _BoomReader(UnboundedReader): + def __init__(self): + self.closed = False + + @override + def start(self): + return True + + @override + def advance(self): + raise RuntimeError('advance boom') + + @override + def get_current(self): + return 'first' + + @override + def get_current_timestamp(self): + return _EVENT_TIME_BASE + + @override + def get_watermark(self): + return _EVENT_TIME_BASE + + @override + def get_checkpoint_mark(self): + return _CountingCheckpointMark(0) + + @override + def close(self): + self.closed = True + + class _BoomSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _BoomReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + src = _BoomSource() + tracker = _new_tracker(src) + # First claim succeeds (start returns True). + self.assertTrue(tracker.try_claim([None])) + reader_after_first = tracker._reader + self.assertIsNotNone(reader_after_first) + # The second claim's advance() raises; the tracker must close the reader + # before propagating. + with self.assertRaises(RuntimeError): + tracker.try_claim([None]) + self.assertTrue(reader_after_first.closed) + self.assertIsNone(tracker._reader) + + def test_tracker_closes_reader_when_get_watermark_raises(self): + # Reader method failures other than advance() also trigger close. + class _WatermarkBoomReader(UnboundedReader): + def __init__(self): + self.closed = False + + @override + def start(self): + return False # no data -> drops into get_watermark path + + @override + def advance(self): + return False + + @override + def get_current(self): + raise AssertionError + + @override + def get_current_timestamp(self): + raise AssertionError + + @override + def get_watermark(self): + raise RuntimeError('watermark boom') + + @override + def get_checkpoint_mark(self): + return _CountingCheckpointMark(0) + + @override + def close(self): + self.closed = True + + class _WatermarkBoomSource(UnboundedSource): + @override + def split(self, desired_num_splits, options=None): + return [self] + + @override + def create_reader(self, options, checkpoint_mark): + return _WatermarkBoomReader() + + @override + def get_checkpoint_mark_coder(self): + return coders.PickleCoder() + + src = _WatermarkBoomSource() + tracker = _new_tracker(src) + with self.assertRaises(RuntimeError): + tracker.try_claim([None]) + self.assertIsNone(tracker._reader) + + +class UnboundedSourceContractTest(unittest.TestCase): + def test_get_checkpoint_mark_coder_default_names_subclass(self): + class MySource(UnboundedSource): + pass + + with self.assertRaises(NotImplementedError) as cm: + MySource().get_checkpoint_mark_coder() + self.assertIn('MySource', str(cm.exception)) + + +class ReadFromUnboundedSourceValidationTest(unittest.TestCase): + def test_non_source_argument_raises(self): + with self.assertRaises(TypeError): + ReadFromUnboundedSource('not-a-source') # type: ignore[arg-type] + + def test_invalid_caps_raise(self): + source = UnboundedCountingSource(1) + with self.assertRaises(ValueError): + ReadFromUnboundedSource(source, max_records_per_bundle=0) + with self.assertRaises(ValueError): + ReadFromUnboundedSource(source, max_read_time_seconds=0) + with self.assertRaises(ValueError): + ReadFromUnboundedSource(source, poll_interval=-1) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + unittest.main() From 4483618b4ec095c1f69be5157918483cfb2ad4fb Mon Sep 17 00:00:00 2001 From: Derrick Williams Date: Thu, 25 Jun 2026 15:08:52 -0400 Subject: [PATCH 478/490] remove infra change (#39104) --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e5975afc56e5..e20194f9ab09 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,7 +61,6 @@ * Python SDK now supports memory profiling with Memray ([#38853](https://github.com/apache/beam/issues/38853)). * (Python) Added [Qdrant](https://qdrant.tech/) VectorDatabaseWriteConfig implementation ([#38141](https://github.com/apache/beam/issues/38141)). -* (CodeQL) Enabled Code scanning alerts in GitHub repo. ([#38893](https://github.com/apache/beam/issues/38893)). ## I/Os From a6bfea11f6433a42b5335af44c60c4bcfb8dae06 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Thu, 25 Jun 2026 16:49:02 -0400 Subject: [PATCH 479/490] Revert "[Python] Optimize BigQuery copy jobs in file loads using multi-source copy" (#39106) * Revert "[Python] Optimize BigQuery copy jobs in file loads using multi-source copy (#38983)" This reverts commit 8e4ea736213d07710c4e64eeed80fe5971689f6b. * Trigger postcommit test. --- .../trigger_files/beam_PostCommit_Python.json | 2 +- .../apache_beam/io/gcp/bigquery_file_loads.py | 162 +++++++------- .../io/gcp/bigquery_file_loads_test.py | 205 ++++-------------- .../apache_beam/io/gcp/bigquery_tools.py | 18 +- 4 files changed, 136 insertions(+), 251 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index c03ecf71f04d..98bf4bf95003 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "37345", - "modification": 50 + "modification": 51 } diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py index 4ef6c392254b..4e45d0324ee2 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py @@ -491,8 +491,6 @@ class TriggerCopyJobs(beam.DoFn): """ TRIGGER_DELETE_TEMP_TABLES = 'TriggerDeleteTempTables' - # https://docs.cloud.google.com/bigquery/quotas#copy_jobs - MAX_SOURCES_PER_COPY_JOB = 1200 def __init__( self, @@ -530,90 +528,96 @@ def process( self, element_list, job_name_prefix=None, unused_schema_mod_jobs=None): if isinstance(element_list, tuple): # Allow this for streaming update compatibility while fixing BEAM-24535. - element_list = [element_list] + self.process_one(element_list, job_name_prefix) + else: + for element in element_list: + self.process_one(element, job_name_prefix) - if not element_list: - return + def process_one(self, element, job_name_prefix): + destination, job_reference = element - first_destination = element_list[0][0] - copy_to_reference = bigquery_tools.parse_table_reference(first_destination) + copy_to_reference = bigquery_tools.parse_table_reference(destination) if copy_to_reference.projectId is None: copy_to_reference.projectId = vp.RuntimeValueProvider.get_value( 'project', str, '') or self.project - copy_from_references = [] - for destination, job_reference in element_list: - copy_from_reference = bigquery_tools.parse_table_reference(destination) - copy_from_reference.tableId = job_reference.jobId - if copy_from_reference.projectId is None: - copy_from_reference.projectId = vp.RuntimeValueProvider.get_value( - 'project', str, '') or self.project - copy_from_references.append(copy_from_reference) + copy_from_reference = bigquery_tools.parse_table_reference(destination) + copy_from_reference.tableId = job_reference.jobId + if copy_from_reference.projectId is None: + copy_from_reference.projectId = vp.RuntimeValueProvider.get_value( + 'project', str, '') or self.project - full_table_ref = bigquery_tools.get_hashable_destination(copy_to_reference) + _LOGGER.info( + "Triggering copy job from %s to %s", + copy_from_reference, + copy_to_reference) - is_first_time = full_table_ref not in self._observed_tables - if is_first_time: - self._observed_tables.add(full_table_ref) - if self.bq_io_metadata: - Lineage.sinks().add( - 'bigquery', - copy_to_reference.projectId, - copy_to_reference.datasetId, - copy_to_reference.tableId) - - # Split into chunks of MAX_SOURCES_PER_COPY_JOB - chunks = [ - copy_from_references[i:i + self.MAX_SOURCES_PER_COPY_JOB] - for i in range( - 0, len(copy_from_references), self.MAX_SOURCES_PER_COPY_JOB) - ] - - copy_job_name_base = '%s_%s' % ( - job_name_prefix, - _bq_uuid(bigquery_tools.get_hashable_destination(copy_to_reference))) + wait_for_job, write_disposition = ( + self._determine_write_disposition(copy_to_reference)) + + if not self.bq_io_metadata: + self.bq_io_metadata = create_bigquery_io_metadata(self._step_name) project_id = ( copy_to_reference.projectId if self.load_job_project_id is None else self.load_job_project_id) + copy_job_name = '%s_%s' % ( + job_name_prefix, + _bq_uuid( + '%s:%s.%s' % ( + copy_from_reference.projectId, + copy_from_reference.datasetId, + copy_from_reference.tableId))) + job_reference = self.bq_wrapper._insert_copy_job( + project_id, + copy_job_name, + copy_from_reference, + copy_to_reference, + create_disposition=self.create_disposition, + write_disposition=write_disposition, + job_labels=self.bq_io_metadata.add_additional_bq_job_labels()) + + if wait_for_job: + self.bq_wrapper.wait_for_bq_job(job_reference, sleep_duration_sec=10) + self.pending_jobs.append( + GlobalWindows.windowed_value((destination, job_reference))) - for i, chunk in enumerate(chunks): - if i == 0 and is_first_time: - write_disposition = self.write_disposition - # Wait inline only if we have multiple chunks and write disposition is WRITE_TRUNCATE or WRITE_EMPTY. - # This ensures the first chunk initializes the table, and subsequent chunks (WRITE_APPEND) append to it. - wait_for_job = ( - self.write_disposition in ('WRITE_TRUNCATE', 'WRITE_EMPTY') and - len(chunks) > 1) - else: - write_disposition = 'WRITE_APPEND' - wait_for_job = False - - chunk_job_name = copy_job_name_base - if len(chunks) > 1: - chunk_job_name = f"{copy_job_name_base}_{i}" - - _LOGGER.info( - "Triggering copy job %s from %s to %s (write_disposition: %s)", - chunk_job_name, [str(r) for r in chunk], - copy_to_reference, - write_disposition) - - job_reference = self.bq_wrapper._insert_copy_job( - project_id, - chunk_job_name, - chunk, - copy_to_reference, - create_disposition=self.create_disposition, - write_disposition=write_disposition, - job_labels=self.bq_io_metadata.add_additional_bq_job_labels() - if self.bq_io_metadata else None) - - if wait_for_job: - self.bq_wrapper.wait_for_bq_job(job_reference, sleep_duration_sec=10) - - self.pending_jobs.append( - GlobalWindows.windowed_value((first_destination, job_reference))) + def _determine_write_disposition(self, copy_to_reference) -> tuple[bool, str]: + """ + Determines the write disposition for a BigQuery copy job, + based on destination. + + When the write_disposition for a job is WRITE_TRUNCATE, multiple copy jobs + to the same destination can interfere with each other, truncate data, and + write to the BigQuery table repeatedly. To prevent this, the first copy job + runs with the user's specified write_disposition, but subsequent jobs must + always use WRITE_APPEND. This ensures that subsequent copy jobs do not + clear out data appended by previous jobs. + + Args: + copy_to_reference: The reference to the destination table. + + Returns: + A tuple containing a boolean indicating whether to wait for the job to + complete and the write disposition to use for the job. + """ + full_table_ref = '%s:%s.%s' % ( + copy_to_reference.projectId, + copy_to_reference.datasetId, + copy_to_reference.tableId) + if full_table_ref not in self._observed_tables: + write_disposition = self.write_disposition + wait_for_job = True + self._observed_tables.add(full_table_ref) + Lineage.sinks().add( + 'bigquery', + copy_to_reference.projectId, + copy_to_reference.datasetId, + copy_to_reference.tableId) + else: + wait_for_job = False + write_disposition = 'WRITE_APPEND' + return wait_for_job, write_disposition def finish_bundle(self): for windowed_value in self.pending_jobs: @@ -740,7 +744,7 @@ def process( else: try: schema = bigquery_tools.table_schema_to_dict( - self.bq_wrapper.get_table( + bigquery_tools.BigQueryWrapper().get_table( project_id=table_reference.projectId, dataset_id=table_reference.datasetId, table_id=table_reference.tableId).schema) @@ -851,8 +855,7 @@ def process(self, element): if latest_partition.can_accept(file_size): latest_partition.add(file_path, file_size) else: - if latest_partition.files: - partitions.append(latest_partition.files) + partitions.append(latest_partition.files) latest_partition = PartitionFiles.Partition( self.max_partition_size, self.max_files_per_partition) latest_partition.add(file_path, file_size) @@ -1178,13 +1181,12 @@ def _load_data( # the truncation happens only once. See # https://github.com/apache/beam/issues/24535. finished_temp_tables_load_job_ids_list_pc = ( - finished_temp_tables_load_job_ids_pc - | beam.MapTuple( + finished_temp_tables_load_job_ids_pc | beam.MapTuple( lambda destination, job_reference: ( - bigquery_tools.get_hashable_destination(destination), + bigquery_tools.parse_table_reference(destination).tableId, (destination, job_reference))) | beam.GroupByKey() - | beam.MapTuple(lambda dest, batch: list(batch))) + | beam.MapTuple(lambda tableId, batch: list(batch))) else: # Loads can happen in parallel. finished_temp_tables_load_job_ids_list_pc = ( diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py index 47c1ce5ea1bb..191719e6a208 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py @@ -924,180 +924,69 @@ def dynamic_destination_resolver(element, *side_inputs): write_disposition=BigQueryDisposition.WRITE_TRUNCATE)) from apache_beam.io.gcp.internal.clients.bigquery import TableReference - mock_insert_copy_job.assert_has_calls([ - call( - 'project1', - mock.ANY, - [ + mock_insert_copy_job.assert_has_calls( + [ + call( + 'project1', + mock.ANY, TableReference( datasetId='dataset1', projectId='project1', tableId='job_name1'), + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='table1'), + create_disposition=None, + write_disposition='WRITE_TRUNCATE', + job_labels={'step_name': 'bigquerybatchfileloads'}), + call( + 'project1', + mock.ANY, TableReference( datasetId='dataset1', projectId='project1', tableId='job_name1'), - ], - TableReference( - datasetId='dataset1', projectId='project1', tableId='table1'), - create_disposition=None, - write_disposition='WRITE_TRUNCATE', - job_labels={'step_name': 'bigquerybatchfileloads'}), - call( - 'project1', - mock.ANY, - [ + TableReference( + datasetId='dataset1', + projectId='project1', + tableId='table1'), + create_disposition=None, + write_disposition='WRITE_APPEND', + job_labels={'step_name': 'bigquerybatchfileloads'}), + call( + 'project1', + mock.ANY, TableReference( datasetId='dataset2', projectId='project1', tableId='job_name1'), - ], - TableReference( - datasetId='dataset2', projectId='project1', tableId='table1'), - create_disposition=None, - write_disposition='WRITE_TRUNCATE', - job_labels={'step_name': 'bigquerybatchfileloads'}), - call( - 'project1', - mock.ANY, - [ + TableReference( + datasetId='dataset2', + projectId='project1', + tableId='table1'), + create_disposition=None, + # Previously this was `WRITE_APPEND`. + write_disposition='WRITE_TRUNCATE', + job_labels={'step_name': 'bigquerybatchfileloads'}), + call( + 'project1', + mock.ANY, TableReference( datasetId='dataset3', projectId='project1', tableId='job_name1'), - ], - TableReference( - datasetId='dataset3', projectId='project1', tableId='table1'), - create_disposition=None, - write_disposition='WRITE_TRUNCATE', - job_labels={'step_name': 'bigquerybatchfileloads'}), - ], - any_order=True) - self.assertEqual(3, mock_insert_copy_job.call_count) - - @mock.patch( - 'apache_beam.io.gcp.bigquery_tools.BigQueryWrapper.wait_for_bq_job') - @mock.patch( - 'apache_beam.io.gcp.bigquery_tools.BigQueryWrapper._insert_copy_job') - def test_copy_jobs_splitting( - self, mock_insert_copy_job, mock_wait_for_bq_job): - destination = 'project1:dataset1.table1' - - from apache_beam.io.gcp.bigquery_file_loads import TriggerCopyJobs - original_max_sources = TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB - TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB = 2 - - try: - job_reference = bigquery_api.JobReference() - job_reference.projectId = 'project1' - job_reference.jobId = 'job_name1' - result_job = mock.Mock() - result_job.jobReference = job_reference - - mock_job = mock.Mock() - mock_job.status.state = 'DONE' - mock_job.status.errorResult = None - mock_job.jobReference = job_reference - - bq_client = mock.Mock() - bq_client.jobs.Get.return_value = mock_job - bq_client.jobs.Insert.return_value = result_job - bq_client.tables.Delete.return_value = None - mock_insert_copy_job.return_value = job_reference - temp_dir = self._new_tempdir() - - with TestPipeline('FnApiRunner') as p: - _ = ( - p - | beam.Create([ - { - 'name': 'a' - }, - { - 'name': 'b' - }, - { - 'name': 'c' - }, - { - 'name': 'd' - }, - { - 'name': 'e' - }, - ], - reshuffle=False) - | bqfl.BigQueryBatchFileLoads( - destination, - custom_gcs_temp_location=temp_dir, - test_client=bq_client, - validate=False, - temp_file_format=bigquery_tools.FileFormat.JSON, - max_file_size=10, - max_partition_size=10, - max_files_per_partition=1, - write_disposition=BigQueryDisposition.WRITE_TRUNCATE)) - - self.assertEqual(3, mock_insert_copy_job.call_count) - - from apache_beam.io.gcp.internal.clients.bigquery import TableReference - expected_calls = [ - call( - 'project1', - mock.ANY, - [ - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='job_name1'), - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='job_name1'), - ], - TableReference( - datasetId='dataset1', projectId='project1', tableId='table1'), - create_disposition=None, - write_disposition='WRITE_TRUNCATE', - job_labels=mock.ANY), - call( - 'project1', - mock.ANY, - [ - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='job_name1'), - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='job_name1'), - ], - TableReference( - datasetId='dataset1', projectId='project1', tableId='table1'), - create_disposition=None, - write_disposition='WRITE_APPEND', - job_labels=mock.ANY), - call( - 'project1', - mock.ANY, - [ - TableReference( - datasetId='dataset1', - projectId='project1', - tableId='job_name1'), - ], - TableReference( - datasetId='dataset1', projectId='project1', tableId='table1'), - create_disposition=None, - write_disposition='WRITE_APPEND', - job_labels=mock.ANY), - ] - mock_insert_copy_job.assert_has_calls(expected_calls, any_order=True) - self.assertEqual(9, mock_wait_for_bq_job.call_count) - - finally: - TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB = original_max_sources + TableReference( + datasetId='dataset3', + projectId='project1', + tableId='table1'), + create_disposition=None, + # Previously this was `WRITE_APPEND`. + write_disposition='WRITE_TRUNCATE', + job_labels={'step_name': 'bigquerybatchfileloads'}), + ], + any_order=True) + self.assertEqual(4, mock_insert_copy_job.call_count) @parameterized.expand([ param(is_streaming=False, with_auto_sharding=False, compat_version=None), diff --git a/sdks/python/apache_beam/io/gcp/bigquery_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_tools.py index 491b7a39b0b7..8dd58cd55a01 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_tools.py @@ -506,22 +506,16 @@ def _insert_copy_job( reference = bigquery.JobReference() reference.jobId = job_id reference.projectId = project_id - - copy_config = bigquery.JobConfigurationTableCopy( - destinationTable=to_table_reference, - createDisposition=create_disposition, - writeDisposition=write_disposition, - ) - if isinstance(from_table_reference, list): - copy_config.sourceTables = from_table_reference - else: - copy_config.sourceTable = from_table_reference - request = bigquery.BigqueryJobsInsertRequest( projectId=project_id, job=bigquery.Job( configuration=bigquery.JobConfiguration( - copy=copy_config, + copy=bigquery.JobConfigurationTableCopy( + destinationTable=to_table_reference, + sourceTable=from_table_reference, + createDisposition=create_disposition, + writeDisposition=write_disposition, + ), labels=_build_job_labels(job_labels), ), jobReference=reference, From 4e417d66f6c4795399ab82ba14c957b032fa2ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Stankiewicz?= Date: Fri, 26 Jun 2026 14:20:42 +0200 Subject: [PATCH 480/490] Write append tables can be asynchronous (#39120) --- .github/trigger_files/beam_PostCommit_Python.json | 4 ++-- sdks/python/apache_beam/io/gcp/bigquery_file_loads.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 98bf4bf95003..2e255c8f3cf6 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", "pr": "37345", - "modification": 51 -} + "modification": 52 +} diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py index 4e45d0324ee2..8857b5fb433c 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py @@ -607,7 +607,7 @@ def _determine_write_disposition(self, copy_to_reference) -> tuple[bool, str]: copy_to_reference.tableId) if full_table_ref not in self._observed_tables: write_disposition = self.write_disposition - wait_for_job = True + wait_for_job = write_disposition != 'WRITE_APPEND' self._observed_tables.add(full_table_ref) Lineage.sinks().add( 'bigquery', From 0f016af5943ba06aae23a8a4eacf0a2905f630f1 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 26 Jun 2026 08:40:54 -0400 Subject: [PATCH 481/490] Fix typo (#39123) --- .../beam/runners/direct/ExecutorServiceParallelExecutor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java index 37cff06a267f..603832843bb6 100644 --- a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java +++ b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/ExecutorServiceParallelExecutor.java @@ -355,10 +355,10 @@ private void shutdownIfNecessary(State newState) { new IllegalStateException( "Error" + (errors.size() == 1 ? "" : "s") - + " occurred during pipeline execution:\\n" + + " occurred during executor shutdown:\n" + errors.stream() .map(e -> e.getMessage() == null ? e.getClass().getName() : e.getMessage()) - .collect(Collectors.joining("\\n- ", "- ", ""))); + .collect(Collectors.joining("\n- ", "- ", ""))); visibleUpdates.failed(exception); } } finally { From a617af527cc63ba223fd1fd5e2173cb6e3a9fdc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:26:09 -0400 Subject: [PATCH 482/490] Bump google-cloud-aiplatform (#39113) Bumps [google-cloud-aiplatform](https://github.com/googleapis/python-aiplatform) from 1.60.0 to 1.133.0. - [Release notes](https://github.com/googleapis/python-aiplatform/releases) - [Changelog](https://github.com/googleapis/python-aiplatform/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/python-aiplatform/compare/v1.60.0...v1.133.0) --- updated-dependencies: - dependency-name: google-cloud-aiplatform dependency-version: 1.133.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../apache_beam/testing/benchmarks/cloudml/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt b/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt index b2f76d200850..03d43524f4b8 100644 --- a/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt +++ b/sdks/python/apache_beam/testing/benchmarks/cloudml/constraints.txt @@ -32,7 +32,7 @@ numpy==1.26.4 pyarrow==10.0.1 # Google Cloud (pin to avoid transitive resolution) -google-cloud-aiplatform==1.60.0 +google-cloud-aiplatform==1.133.0 google-api-core==2.19.1 # Note: google-auth is NOT constrained - let pip resolve it to satisfy From 15b4a7c5467b545961dd14bc1bb86851cc2d1d03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:27:16 -0400 Subject: [PATCH 483/490] Update cython requirement from <4,>=3.2.5 to >=3.2.6,<4 in /sdks/python (#39099) Updates the requirements on [cython](https://github.com/cython/cython) to permit the latest version. - [Release notes](https://github.com/cython/cython/releases) - [Changelog](https://github.com/cython/cython/blob/master/CHANGES.rst) - [Commits](https://github.com/cython/cython/compare/3.2.5...3.2.6) --- updated-dependencies: - dependency-name: cython dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdks/python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 1e12990171df..41c7d6d60d78 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -32,7 +32,7 @@ requires = [ # Numpy headers "numpy>=1.14.3,<2.5.0", # Update setup.py as well. # having cython here will create wheels that are platform dependent. - "cython>=3.2.5,<4", + "cython>=3.2.6,<4", ## deps for generating external transform wrappers: # also update PyYaml bounds in sdks:python:generateExternalTransformsConfig 'pyyaml>=3.12,<7.0.0', From b01c1b7fabb771d6b70391ac09e00569c0485c84 Mon Sep 17 00:00:00 2001 From: Jack McCluskey <34928439+jrmccluskey@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:36:36 -0400 Subject: [PATCH 484/490] [Website] Update Python version compatibility table (#38969) * Update Python version compatibility table * remove ge --- website/www/site/content/en/documentation/sdks/python.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/www/site/content/en/documentation/sdks/python.md b/website/www/site/content/en/documentation/sdks/python.md index 3575437bbff3..0dba9258856e 100644 --- a/website/www/site/content/en/documentation/sdks/python.md +++ b/website/www/site/content/en/documentation/sdks/python.md @@ -71,6 +71,10 @@ Some common errors can occur during worker start-up and prevent jobs from starti Python Version Supported Beam Versions + + 3.14 + ≥ 2.73.0 + 3.13 ≥ 2.69.0 @@ -89,7 +93,7 @@ Some common errors can occur during worker start-up and prevent jobs from starti 3.9 - ≥ 2.37.0 + 2.37.0 - 2.69.0 3.8 From 0da8aacb77f4fc1f1eece2f69262e2fbd1d5dd1f Mon Sep 17 00:00:00 2001 From: jayjayakumar <87327692+jayjayakumar@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:20:05 -0500 Subject: [PATCH 485/490] Docs: update Python version support in Python quickstart (#39122) * Update quickstart-py.md Docs: update Python version support in quickstart * Docs: remove 3.9, add 3.14 per reviewer feedback * Docs: update Python version list based on feedback * Docs: Fix Python 3.9 and 3.8 end of support version. --- website/www/site/content/en/get-started/quickstart-py.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/www/site/content/en/get-started/quickstart-py.md b/website/www/site/content/en/get-started/quickstart-py.md index d7f896153483..0d30bd94568c 100644 --- a/website/www/site/content/en/get-started/quickstart-py.md +++ b/website/www/site/content/en/get-started/quickstart-py.md @@ -23,7 +23,7 @@ If you're interested in contributing to the Apache Beam Python codebase, see the {{< toc >}} -The Python SDK supports Python 3.8, 3.9, 3.10, 3.11 and 3.12. Beam 2.48.0 was the last release with support for Python 3.7. +The Python SDK supports Python 3.10, 3.11, 3.12, 3.13 and 3.14. Beam 2.69.0 was the last release with support for Python 3.9. Beam 2.60.0 was the last release with support for Python 3.8. ## Set up your environment From 54935c3bad1b6633bc7331e394e6cfe99ee581ac Mon Sep 17 00:00:00 2001 From: HansMarcus01 Date: Fri, 26 Jun 2026 12:02:55 -0600 Subject: [PATCH 486/490] =?UTF-8?q?including=20GitHub=20Actions=20to=20sch?= =?UTF-8?q?edule=20daily=20audits=20and=20Add=20documentati=E2=80=A6=20(#3?= =?UTF-8?q?9116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * including GitHub Actions to schedule daily audits and Add documentation for the changes made to manage keys created outside of the Google Cloud Secret Manager key rotation system * correcting a typo in the file name * Fix PreCommit GHA and PreCommit Whitespace, and preparing the test of the action from the pull request * Testing the GitHub action from the PR * Remove the pull request configuration from the github Action --- .github/workflows/README.md | 1 + ...beam_Infrastructure_AuditUnmanagedKeys.yml | 69 +++++++++++++++++++ infra/enforcement/README.md | 12 +++- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 4a32aa271df1..f6b1a440e175 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -545,3 +545,4 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ Infrastructure Policy Enforcer ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml) | N/A | [![.github/workflows/beam_Infrastructure_PolicyEnforcer.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml?query=event%3Aschedule) | | [ Modify the GCP User Roles according to the infra/users.yml file ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml) | N/A | [![.github/workflows/beam_Infrastructure_UsersPermissions.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml?query=event%3Aschedule) | | [ Service Account Keys Management ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml) | N/A | [![.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml?query=event%3Aschedule) | +| [ Unmanaged Service Accounts Keys Audit ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml) | N/A | [![.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml?query=event%3Aschedule) | diff --git a/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml new file mode 100644 index 000000000000..6d34bd1d156f --- /dev/null +++ b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# This workflow works with the GCP security log analyzer to +# generate weekly security reports and initialize log sinks + +name: Unmanaged Service Accounts Keys Audit + +on: + workflow_dispatch: + schedule: + # Every day at 00:00 UTC + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + issues: write + id-token: write + +jobs: + beam_UnmanagedKeysAudit: + name: Audit Unmanaged Service Account Keys + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install Python dependencies + working-directory: ./infra/enforcement + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Unmanaged Service Account Keys Audit + working-directory: ./infra/enforcement + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: python account_keys.py --action announce + + + + diff --git a/infra/enforcement/README.md b/infra/enforcement/README.md index 8136081fed75..6d883f7e6806 100644 --- a/infra/enforcement/README.md +++ b/infra/enforcement/README.md @@ -134,8 +134,9 @@ The enforcement tools are integrated with GitHub Actions to provide automated co ### Workflow Configuration -The GitHub Actions workflow (`.github/workflows/beam_Infrastructure_PolicyEnforcer.yml`) runs: -- **Schedule**: Weekly on Mondays at 9:00 AM UTC +The repository includes workflows for different security domains: +- **IAM Policy Enforcer** (`.github/workflows/beam_Infrastructure_PolicyEnforcer.yml`): Runs weekly on Mondays at 9:00 AM UTC. +- **Unmanaged Keys Audit** (`.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml`): Runs daily at 00:00 UTC. It manages the continuous execution of the `account_keys.py` script to swiftly detect rogue service account keys generated outside the official rotation system. - **Manual trigger**: Can be triggered manually via `workflow_dispatch` - **Actions**: Runs both IAM and Account Keys enforcement with the `announce` action @@ -170,7 +171,9 @@ python account_keys.py --action generate ### Actions - **check**: Validates service account keys and their permissions against defined policies and reports any differences (default behavior) -- **announce**: Creates or updates a GitHub issue and sends an email notification when service account keys policies differ from the defined ones. If no open issue exists, creates a new one; if an open issue exists, updates the issue body with current violations +- **announce**: Creates or updates a GitHub issue and sends an email notification when service account keys policies differ from the defined ones. + - For general configuration errors, it updates the main compliance issue. + - **For unmanaged/rogue keys (Security Alerts)**, it consolidates alerts into a dedicated `[SECURITY]` issue acting as a live dashboard. It updates the issue by placing the newest audit report at the top and moving the previous reports into a collapsed `

      ` history section. If the keys are revoked and the infrastructure becomes healthy, the system automatically resolves and closes the issue. - **print**: Prints announcement details for testing purposes without creating actual GitHub issues or sending emails - **generate**: Updates the compliance file to match the current GCP service account keys and Secret Manager permissions @@ -183,6 +186,9 @@ The Account Keys enforcement tool provides the following capabilities: - **Permission Validation**: Ensures that Secret Manager permissions match the declared authorized users - **Compliance Reporting**: Identifies missing service accounts, undeclared managed secrets, and permission mismatches - **Automatic Remediation**: Can automatically update the compliance file to match current infrastructure state +- **Unmanaged Key Detection**: Identifies physical keys in IAM that were created outside the automated rotation system by comparing them against the legal, managed keys registered in Google Cloud Secret Manager. +- **Stateful Security Alerts**: Consolidates security violations into a single, dynamically updated GitHub issue, keeping a collapsed `
      ` history of past scans to prevent alert fatigue. +- **Self-Healing Resolution**: Automatically closes the security GitHub issue when all unmanaged keys have been successfully revoked. ### Configuration From d7572821cb48beb9ce5c36ce550e829bc0837422 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Fri, 26 Jun 2026 22:04:12 -0400 Subject: [PATCH 487/490] Fix a flaky test in ApproximateQuantilesTest (#39132) --- .../apache_beam/transforms/stats_test.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/sdks/python/apache_beam/transforms/stats_test.py b/sdks/python/apache_beam/transforms/stats_test.py index bf634c003a07..b236c7e3d5ac 100644 --- a/sdks/python/apache_beam/transforms/stats_test.py +++ b/sdks/python/apache_beam/transforms/stats_test.py @@ -264,6 +264,10 @@ def _approx_quantile_generator(size, num_of_quantiles, absoluteError): quantiles.append(size - 1) return quantiles + @staticmethod + def _sum_and_second(x): + return (sum(x), x[1]) + def test_quantiles_globaly(self): with TestPipeline() as p: pc = p | Create(list(range(101))) @@ -490,22 +494,32 @@ def test_batched_quantiles(self): 3, input_batched=True)) with_key = ( pc | 'Globally with key' >> beam.ApproximateQuantiles.Globally( - 3, key=sum, input_batched=True)) + 3, + key=ApproximateQuantilesTest._sum_and_second, + input_batched=True)) key_with_reversed = ( pc | 'Globally with key and reversed' >> beam.ApproximateQuantiles.Globally( - 3, key=sum, reverse=True, input_batched=True)) + 3, + key=ApproximateQuantilesTest._sum_and_second, + reverse=True, + input_batched=True)) assert_that( globally, equal_to([[(0.0, 500), (49.9, 1), (99.9, 499)]]), label='checkGlobally') + # When key is present, both (72.5, 225) and (22.5, 275) produce the exact same + # sum (297.5). If we just use key=sum, tie-breaking is sensitive to bundle merging + # order and shared class-level jitter state, leading to flaky test failures. + # With the secondary key (defined in _sum_and_second), we can break ties + # deterministically. assert_that( with_key, equal_to([[(50.0, 0), (72.5, 225), (99.9, 499)]]), label='checkGloballyWithKey') assert_that( key_with_reversed, - equal_to([[(99.9, 499), (72.5, 225), (50.0, 0)]]), + equal_to([[(99.9, 499), (22.5, 275), (50.0, 0)]]), label='checkGloballyWithKeyAndReversed') def test_batched_weighted_quantiles(self): From 99ddb847f2499897297f4f71902709aa946657c3 Mon Sep 17 00:00:00 2001 From: Shunping Huang Date: Sat, 27 Jun 2026 04:29:20 -0400 Subject: [PATCH 488/490] Fix flaky ML RunInference tests by disabling reshuffle on beam.Create (#39118) --- .../apache_beam/ml/inference/base_test.py | 2 +- .../ml/inference/pytorch_inference_test.py | 5 +- .../ml/inference/sklearn_inference_test.py | 4 +- .../ml/inference/tensorflow_inference_test.py | 4 +- .../ml/inference/tensorrt_inference_test.py | 11 +++-- .../apache_beam/transforms/util_test.py | 49 +++++++++++++++++++ 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/sdks/python/apache_beam/ml/inference/base_test.py b/sdks/python/apache_beam/ml/inference/base_test.py index 3f0bea7fbe1a..7bca1fc63385 100644 --- a/sdks/python/apache_beam/ml/inference/base_test.py +++ b/sdks/python/apache_beam/ml/inference/base_test.py @@ -1047,7 +1047,7 @@ def test_timing_metrics(self): def test_forwards_batch_args(self): examples = list(range(100)) with TestPipeline('FnApiRunner') as pipeline: - pcoll = pipeline | 'start' >> beam.Create(examples) + pcoll = pipeline | 'start' >> beam.Create(examples, reshuffle=False) actual = pcoll | base.RunInference(FakeModelHandlerNeedsBigBatch()) assert_that(actual, equal_to(examples), label='assert:inferences') diff --git a/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py b/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py index 50279820b267..8efec14c865f 100644 --- a/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py +++ b/sdks/python/apache_beam/ml/inference/pytorch_inference_test.py @@ -635,7 +635,8 @@ def batch_validator_keyed_tensor_inference_fn( min_batch_size=2, max_batch_size=2) - pcoll = pipeline | 'start' >> beam.Create(KEYED_TORCH_EXAMPLES) + pcoll = pipeline | 'start' >> beam.Create( + KEYED_TORCH_EXAMPLES, reshuffle=False) inference_args_side_input = ( pipeline | 'create side' >> beam.Create(inference_args)) predictions = pcoll | RunInference( @@ -709,7 +710,7 @@ def batch_validator_tensor_inference_fn( min_batch_size=2, max_batch_size=2) - pcoll = pipeline | 'start' >> beam.Create(examples) + pcoll = pipeline | 'start' >> beam.Create(examples, reshuffle=False) predictions = pcoll | RunInference(model_handler) assert_that( predictions, diff --git a/sdks/python/apache_beam/ml/inference/sklearn_inference_test.py b/sdks/python/apache_beam/ml/inference/sklearn_inference_test.py index 400ac77cf498..76d6bc65729c 100644 --- a/sdks/python/apache_beam/ml/inference/sklearn_inference_test.py +++ b/sdks/python/apache_beam/ml/inference/sklearn_inference_test.py @@ -299,7 +299,7 @@ def batch_validator_numpy_inference_fn( with TestPipeline() as pipeline: examples = [numpy.array([0, 0]), numpy.array([1, 1])] - pcoll = pipeline | 'start' >> beam.Create(examples) + pcoll = pipeline | 'start' >> beam.Create(examples, reshuffle=False) actual = pcoll | RunInference( SklearnModelHandlerNumpy( model_uri=temp_file_name, @@ -457,7 +457,7 @@ def batch_validator_pandas_inference_fn( with TestPipeline() as pipeline: dataframe = pandas_dataframe() splits = [dataframe.loc[[i]] for i in dataframe.index] - pcoll = pipeline | 'start' >> beam.Create(splits) + pcoll = pipeline | 'start' >> beam.Create(splits, reshuffle=False) actual = pcoll | RunInference( SklearnModelHandlerPandas( model_uri=temp_file_name, diff --git a/sdks/python/apache_beam/ml/inference/tensorflow_inference_test.py b/sdks/python/apache_beam/ml/inference/tensorflow_inference_test.py index c884ee58b0a0..3a2e58e378eb 100644 --- a/sdks/python/apache_beam/ml/inference/tensorflow_inference_test.py +++ b/sdks/python/apache_beam/ml/inference/tensorflow_inference_test.py @@ -165,7 +165,7 @@ def fake_batching_inference_fn( examples, [tf.math.multiply(n, 2) for n in examples]) ] - pcoll = pipeline | 'start' >> beam.Create(examples) + pcoll = pipeline | 'start' >> beam.Create(examples, reshuffle=False) predictions = pcoll | RunInference(model_handler) assert_that( predictions, @@ -258,7 +258,7 @@ def fake_batching_inference_fn( examples, [numpy.multiply(n, 2) for n in examples]) ] - pcoll = pipeline | 'start' >> beam.Create(examples) + pcoll = pipeline | 'start' >> beam.Create(examples, reshuffle=False) predictions = pcoll | RunInference(model_handler) assert_that( predictions, diff --git a/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py b/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py index 39e46c7f7c0d..80a01b8f4d4c 100644 --- a/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py +++ b/sdks/python/apache_beam/ml/inference/tensorrt_inference_test.py @@ -362,7 +362,8 @@ def test_pipeline_single_tensor_feature_built_engine(self): max_batch_size=4, engine_path= 'gs://apache-beam-ml/models/single_tensor_features_engine.trt') - pcoll = pipeline | 'start' >> beam.Create(SINGLE_FEATURE_EXAMPLES) + pcoll = pipeline | 'start' >> beam.Create( + SINGLE_FEATURE_EXAMPLES, reshuffle=False) predictions = pcoll | RunInference(engine_handler) assert_that( predictions, @@ -423,7 +424,8 @@ def fake_inference_fn(batch, engine, inference_args=None): 'gs://apache-beam-ml/models/single_tensor_features_engine.trt', inference_fn=fake_inference_fn, large_model=True) - pcoll = pipeline | 'start' >> beam.Create(SINGLE_FEATURE_EXAMPLES) + pcoll = pipeline | 'start' >> beam.Create( + SINGLE_FEATURE_EXAMPLES, reshuffle=False) predictions = pcoll | RunInference(engine_handler) assert_that( predictions, @@ -443,7 +445,7 @@ def test_pipeline_sets_env_vars_correctly(self): self.assertFalse('FOO' in os.environ) _ = ( pipeline - | 'start' >> beam.Create(SINGLE_FEATURE_EXAMPLES) + | 'start' >> beam.Create(SINGLE_FEATURE_EXAMPLES, reshuffle=False) | RunInference(engine_handler)) pipeline.run() self.assertTrue('FOO' in os.environ) @@ -457,7 +459,8 @@ def test_pipeline_multiple_tensor_feature_built_engine(self): max_batch_size=4, engine_path= 'gs://apache-beam-ml/models/multiple_tensor_features_engine.trt') - pcoll = pipeline | 'start' >> beam.Create(TWO_FEATURES_EXAMPLES) + pcoll = pipeline | 'start' >> beam.Create( + TWO_FEATURES_EXAMPLES, reshuffle=False) predictions = pcoll | RunInference(engine_handler) assert_that( predictions, diff --git a/sdks/python/apache_beam/transforms/util_test.py b/sdks/python/apache_beam/transforms/util_test.py index a965ff33d829..63ce42726c1f 100644 --- a/sdks/python/apache_beam/transforms/util_test.py +++ b/sdks/python/apache_beam/transforms/util_test.py @@ -75,6 +75,8 @@ from apache_beam.transforms.util import GcpHsmGeneratedSecret from apache_beam.transforms.util import GcpSecret from apache_beam.transforms.util import Secret +from apache_beam.transforms.util import _BatchSizeEstimator +from apache_beam.transforms.util import _GlobalWindowsBatchingDoFn from apache_beam.transforms.window import FixedWindows from apache_beam.transforms.window import GlobalWindow from apache_beam.transforms.window import GlobalWindows @@ -1258,6 +1260,53 @@ def check_batch_homogeneity(batch): checks = batches | beam.Map(check_batch_homogeneity) assert_that(checks, is_not_empty()) + def test_global_batching_dofn_single_vs_multiple_bundles(self): + # This test directly verifies how bundling affects the batch sizes produced by + # the internal _GlobalWindowsBatchingDoFn of BatchElements. + + # 1. Single Bundle Scenario: + # Four elements processed within the same start_bundle / finish_bundle lifecycle. + # min_batch_size = 2, max_batch_size = 2. + estimator = _BatchSizeEstimator(min_batch_size=2, max_batch_size=2) + dofn = _GlobalWindowsBatchingDoFn(estimator, element_size_fn=lambda x: 1) + + dofn.start_bundle() + outputs = [] + for elem in [1, 2, 3, 4]: + outputs.extend(dofn.process(elem)) + outputs.extend(dofn.finish_bundle() or []) + + # We should get exactly two batches of size 2. + batch_sizes = [len(wv.value) for wv in outputs] + self.assertEqual(batch_sizes, [2, 2]) + + # 2. Multiple Bundles Scenario (simulating elements split due to Reshuffle/GroupByKey): + # The runner splits elements into multiple bundles: + # Bundle 1 gets elements 1, 2, 3. + # Bundle 2 gets element 4. + estimator = _BatchSizeEstimator(min_batch_size=2, max_batch_size=2) + dofn = _GlobalWindowsBatchingDoFn(estimator, element_size_fn=lambda x: 1) + + outputs = [] + # Bundle 1 + dofn.start_bundle() + for elem in [1, 2, 3]: + outputs.extend(dofn.process(elem)) + outputs.extend(dofn.finish_bundle() or []) + + # Bundle 2 + dofn.start_bundle() + for elem in [4]: + outputs.extend(dofn.process(elem)) + outputs.extend(dofn.finish_bundle() or []) + + # The batch sizes will be [2, 1, 1] instead of [2, 2] because of bundle flushes. + # Specifically: + # - Bundle 1 emits a batch of 2, and then the remaining 1 element is flushed at finish_bundle (batch size 1). + # - Bundle 2 emits its 1 element at finish_bundle (batch size 1). + batch_sizes = [len(wv.value) for wv in outputs] + self.assertEqual(batch_sizes, [2, 1, 1]) + class SortAndBatchElementsTest(unittest.TestCase): """Tests for SortAndBatchElements transform.""" From 32dbe8ded4e4b1cbeedd71d079cc6544dd1c0649 Mon Sep 17 00:00:00 2001 From: Aakash Baskaran Date: Sat, 27 Jun 2026 22:20:30 -0400 Subject: [PATCH 489/490] Fix addExperiment() to handle immutable lists and remove redundant defensive copies --- .../beam/runners/dataflow/DataflowPipelineTranslator.java | 4 ---- .../org/apache/beam/runners/dataflow/DataflowRunner.java | 4 ---- .../worker/windmill/client/grpc/GrpcWindmillServer.java | 4 ---- .../org/apache/beam/sdk/options/ExperimentalOptions.java | 5 ++--- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index c64bd4f535d6..f66185c47941 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -403,10 +403,6 @@ public Translator(Pipeline pipeline, DataflowRunner runner, SdkComponents sdkCom * @return a Job definition filled in with the type of job, the environment, and the job steps. */ public Job translate(List packages) { - // Ensure the experiments list is mutable before any experiments are added. - if (options.getExperiments() != null) { - options.setExperiments(new ArrayList<>(options.getExperiments())); - } job.setName(options.getJobName().toLowerCase()); Environment environment = new Environment(); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 659bc22ac06c..73348843497b 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1243,10 +1243,6 @@ private static boolean includesTransformUpgrades(Pipeline pipeline) { @SuppressWarnings("Slf4jFormatShouldBeConst") @Override public DataflowPipelineJob run(Pipeline pipeline) { - // Ensure the experiments list is mutable before any experiments are added. - if (options.getExperiments() != null) { - options.setExperiments(new ArrayList<>(options.getExperiments())); - } // Multi-language pipelines and pipelines that include upgrades should automatically be upgraded // to Dataflow Portable Runner. if (DataflowRunner.isMultiLanguagePipeline(pipeline) || includesTransformUpgrades(pipeline)) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java index 550011fa2a45..2a7823e69091 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java @@ -111,10 +111,6 @@ private static DataflowWorkerHarnessOptions testOptions( boolean enableStreamingEngine, List additionalExperiments) { DataflowWorkerHarnessOptions options = PipelineOptionsFactory.create().as(DataflowWorkerHarnessOptions.class); - // Ensure the experiments list is mutable before any experiments are added. - if (options.getExperiments() != null) { - options.setExperiments(new ArrayList<>(options.getExperiments())); - } options.setProject("project"); options.setJobId("job"); options.setWorkerId("worker"); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java index c0e2e1dcb48f..ee883ead5bb9 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.options; +import java.util.ArrayList; import java.util.List; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.checkerframework.checker.nullness.qual.Nullable; @@ -57,9 +58,7 @@ static boolean hasExperiment(PipelineOptions options, String experiment) { /** Adds experiment to options if not already present. */ static void addExperiment(ExperimentalOptions options, String experiment) { List experiments = options.getExperiments(); - if (experiments == null) { - experiments = Lists.newArrayList(); - } + experiments = (experiments == null) ? Lists.newArrayList() : new ArrayList<>(experiments); if (!experiments.contains(experiment)) { experiments.add(experiment); } From 39a7a74bc8596aad111c55e6da7239c780d84d69 Mon Sep 17 00:00:00 2001 From: Aakash Baskaran Date: Sat, 27 Jun 2026 22:50:00 -0400 Subject: [PATCH 490/490] Restore upload_graph hasExperiment guard to prevent duplicate log message --- .../java/org/apache/beam/runners/dataflow/DataflowRunner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index 73348843497b..e979cc44d827 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -1526,7 +1526,8 @@ public DataflowPipelineJob run(Pipeline pipeline) { // enable upload_graph when the graph is too large byte[] jobGraphBytes = DataflowPipelineTranslator.jobToString(newJob).getBytes(UTF_8); int jobGraphByteSize = jobGraphBytes.length; - if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES && !useUnifiedWorker(options)) { + if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES && !useUnifiedWorker(options) + && !hasExperiment(options, "upload_graph")) { ExperimentalOptions.addExperiment(options, "upload_graph"); LOG.info( "The job graph size ({} in bytes) is larger than {}. Automatically add "